logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

112
crates/language/Cargo.toml Normal file
View File

@@ -0,0 +1,112 @@
[package]
name = "language"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/language.rs"
doctest = false
[features]
test-support = [
"rand",
"collections/test-support",
"lsp/test-support",
"text/test-support",
"tree-sitter-rust",
"tree-sitter-python",
"tree-sitter-typescript",
"tree-sitter-md",
"settings/test-support",
"util/test-support",
]
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
clock.workspace = true
collections.workspace = true
ec4rs.workspace = true
encoding_rs.workspace = true
fs.workspace = true
futures.workspace = true
futures-lite.workspace = true
fuzzy.workspace = true
globset.workspace = true
gpui.workspace = true
http_client.workspace = true
imara-diff.workspace = true
language_core.workspace = true
itertools.workspace = true
log.workspace = true
lsp.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true
rpc.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
shellexpand.workspace = true
smallvec.workspace = true
streaming-iterator.workspace = true
strsim.workspace = true
sum_tree.workspace = true
task.workspace = true
text.workspace = true
theme.workspace = true
toml.workspace = true
tracing.workspace = true
tree-sitter-md = { workspace = true, optional = true }
tree-sitter-python = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter.workspace = true
unicase = "2.6"
util.workspace = true
watch.workspace = true
zlog.workspace = true
ztracing.workspace = true
diffy = "0.4.2"
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }
ctor.workspace = true
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
lsp = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
tree-sitter-elixir.workspace = true
tree-sitter-embedded-template.workspace = true
tree-sitter-heex.workspace = true
tree-sitter-html.workspace = true
tree-sitter-json.workspace = true
tree-sitter-md.workspace = true
tree-sitter-python.workspace = true
tree-sitter-ruby.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true
toml.workspace = true
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
zlog.workspace = true
criterion.workspace = true
theme_settings.workspace = true
[[bench]]
name = "highlight_map"
harness = false
[package.metadata.cargo-machete]
ignored = ["tracing"]

1
crates/language/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,144 @@
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use gpui::rgba;
use language::build_highlight_map;
use theme::SyntaxTheme;
fn syntax_theme(highlight_names: &[&str]) -> SyntaxTheme {
SyntaxTheme::new(highlight_names.iter().enumerate().map(|(i, name)| {
let r = ((i * 37) % 256) as u8;
let g = ((i * 53) % 256) as u8;
let b = ((i * 71) % 256) as u8;
let color = rgba(u32::from_be_bytes([r, g, b, 0xff]));
(name.to_string(), color.into())
}))
}
static SMALL_THEME_KEYS: &[&str] = &[
"comment", "function", "keyword", "string", "type", "variable",
];
static LARGE_THEME_KEYS: &[&str] = &[
"attribute",
"boolean",
"comment",
"comment.doc",
"constant",
"constant.builtin",
"constructor",
"embedded",
"emphasis",
"emphasis.strong",
"function",
"function.builtin",
"function.method",
"function.method.builtin",
"function.special.definition",
"keyword",
"keyword.control",
"keyword.control.conditional",
"keyword.control.import",
"keyword.control.repeat",
"keyword.control.return",
"keyword.modifier",
"keyword.operator",
"label",
"link_text",
"link_uri",
"number",
"operator",
"property",
"punctuation",
"punctuation.bracket",
"punctuation.delimiter",
"punctuation.list_marker",
"punctuation.special",
"string",
"string.escape",
"string.regex",
"string.special",
"string.special.symbol",
"tag",
"text.literal",
"title",
"type",
"type.builtin",
"type.super",
"variable",
"variable.builtin",
"variable.member",
"variable.parameter",
"variable.special",
];
static SMALL_CAPTURE_NAMES: &[&str] = &[
"function",
"keyword",
"string.escape",
"type.builtin",
"variable.builtin",
];
static LARGE_CAPTURE_NAMES: &[&str] = &[
"attribute",
"boolean",
"comment",
"comment.doc",
"constant",
"constant.builtin",
"constructor",
"function",
"function.builtin",
"function.method",
"keyword",
"keyword.control",
"keyword.control.conditional",
"keyword.control.import",
"keyword.modifier",
"keyword.operator",
"label",
"number",
"operator",
"property",
"punctuation.bracket",
"punctuation.delimiter",
"punctuation.special",
"string",
"string.escape",
"string.regex",
"string.special",
"tag",
"type",
"type.builtin",
"variable",
"variable.builtin",
"variable.member",
"variable.parameter",
];
fn bench_build_highlight_map(c: &mut Criterion) {
let mut group = c.benchmark_group("build_highlight_map");
for (capture_label, capture_names) in [
("small_captures", SMALL_CAPTURE_NAMES as &[&str]),
("large_captures", LARGE_CAPTURE_NAMES as &[&str]),
] {
for (theme_label, theme_keys) in [
("small_theme", SMALL_THEME_KEYS as &[&str]),
("large_theme", LARGE_THEME_KEYS as &[&str]),
] {
let theme = syntax_theme(theme_keys);
group.bench_with_input(
BenchmarkId::new(capture_label, theme_label),
&(capture_names, &theme),
|b, (capture_names, theme)| {
b.iter(|| build_highlight_map(black_box(capture_names), black_box(theme)));
},
);
}
}
group.finish();
}
criterion_group!(benches, bench_build_highlight_map);
criterion_main!(benches);

5
crates/language/build.rs Normal file
View File

@@ -0,0 +1,5 @@
fn main() {
if let Ok(bundled) = std::env::var("ZED_BUNDLE") {
println!("cargo:rustc-env=ZED_BUNDLE={}", bundled);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
//! A row chunk is an exclusive range of rows, [`BufferRow`] within a buffer of a certain version, [`Global`].
//! All but the last chunk are of a constant, given size.
use std::{ops::Range, sync::Arc};
use text::{Anchor, OffsetRangeExt as _, Point};
use util::RangeExt;
use crate::BufferRow;
/// An range of rows, exclusive as [`lsp::Range`] and
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
/// denote.
///
/// Represents an area in a text editor, adjacent to other ones.
/// Together, chunks form entire document at a particular version [`Global`].
/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
#[derive(Clone)]
pub struct RowChunks {
chunks: Arc<[RowChunk]>,
version: clock::Global,
}
impl std::fmt::Debug for RowChunks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RowChunks")
.field("chunks", &self.chunks)
.finish()
}
}
impl RowChunks {
pub fn new(snapshot: &text::BufferSnapshot, max_rows_per_chunk: u32) -> Self {
let buffer_point_range = (0..snapshot.len()).to_point(&snapshot);
let last_row = buffer_point_range.end.row;
let chunks = (buffer_point_range.start.row..=last_row)
.step_by(max_rows_per_chunk as usize)
.collect::<Vec<_>>();
let last_chunk_id = chunks.len() - 1;
let chunks = chunks
.into_iter()
.enumerate()
.map(|(id, chunk_start)| {
let start = Point::new(chunk_start, 0);
let end_exclusive = (chunk_start + max_rows_per_chunk).min(last_row);
let end = if id == last_chunk_id {
Point::new(end_exclusive, snapshot.line_len(end_exclusive))
} else {
Point::new(end_exclusive, 0)
};
RowChunk {
id,
start: chunk_start,
end_exclusive,
start_anchor: snapshot.anchor_before(start),
end_anchor: snapshot.anchor_after(end),
}
})
.collect::<Vec<_>>();
Self {
chunks: Arc::from(chunks),
version: snapshot.version().clone(),
}
}
pub fn version(&self) -> &clock::Global {
&self.version
}
pub fn len(&self) -> usize {
self.chunks.len()
}
pub fn applicable_chunks(&self, ranges: &[Range<Point>]) -> impl Iterator<Item = RowChunk> {
let row_ranges = ranges
.iter()
// Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
// This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
.map(|point_range| point_range.start.row..point_range.end.row + 1)
.collect::<Vec<_>>();
self.chunks
.iter()
.filter(move |chunk| -> bool {
let chunk_range = chunk.row_range().to_inclusive();
row_ranges
.iter()
.any(|row_range| chunk_range.overlaps(&row_range))
})
.copied()
}
pub fn previous_chunk(&self, chunk: RowChunk) -> Option<RowChunk> {
if chunk.id == 0 {
None
} else {
self.chunks.get(chunk.id - 1).copied()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RowChunk {
pub id: usize,
pub start: BufferRow,
pub end_exclusive: BufferRow,
pub start_anchor: Anchor,
pub end_anchor: Anchor,
}
impl RowChunk {
pub fn row_range(&self) -> Range<BufferRow> {
self.start..self.end_exclusive
}
pub fn anchor_range(&self) -> Range<Anchor> {
self.start_anchor..self.end_anchor
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
pub use language_core::diagnostic::{Diagnostic, DiagnosticSourceKind};

View File

@@ -0,0 +1,353 @@
use crate::{Diagnostic, range_to_lsp};
use anyhow::Result;
use collections::HashMap;
use lsp::LanguageServerId;
use serde::Serialize;
use std::{
cmp::{Ordering, Reverse},
iter,
ops::Range,
};
use sum_tree::{self, Bias, SumTree};
use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
/// A set of diagnostics associated with a given buffer, provided
/// by a single language server.
///
/// The diagnostics are stored in a [`SumTree`], which allows this struct
/// to be cheaply copied, and allows for efficient retrieval of the
/// diagnostics that intersect a given range of the buffer.
#[derive(Clone, Debug)]
pub struct DiagnosticSet {
diagnostics: SumTree<DiagnosticEntry<Anchor>>,
}
/// A single diagnostic in a set. Generic over its range type, because
/// the diagnostics are stored internally as [`Anchor`]s, but can be
/// resolved to different coordinates types like [`usize`] byte offsets or
/// [`Point`](gpui::Point)s.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DiagnosticEntry<T> {
/// The range of the buffer where the diagnostic applies.
pub range: Range<T>,
/// The information about the diagnostic.
pub diagnostic: Diagnostic,
}
/// A single diagnostic in a set. Generic over its range type, because
/// the diagnostics are stored internally as [`Anchor`]s, but can be
/// resolved to different coordinates types like [`usize`] byte offsets or
/// [`Point`](gpui::Point)s.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DiagnosticEntryRef<'a, T> {
/// The range of the buffer where the diagnostic applies.
pub range: Range<T>,
/// The information about the diagnostic.
pub diagnostic: &'a Diagnostic,
}
impl<T: PartialEq> PartialEq<DiagnosticEntry<T>> for DiagnosticEntryRef<'_, T> {
fn eq(&self, other: &DiagnosticEntry<T>) -> bool {
self.range == other.range && *self.diagnostic == other.diagnostic
}
}
impl<T: PartialEq> PartialEq<DiagnosticEntryRef<'_, T>> for DiagnosticEntry<T> {
fn eq(&self, other: &DiagnosticEntryRef<'_, T>) -> bool {
self.range == other.range && self.diagnostic == *other.diagnostic
}
}
impl<T: Clone> DiagnosticEntryRef<'_, T> {
pub fn to_owned(&self) -> DiagnosticEntry<T> {
DiagnosticEntry {
range: self.range.clone(),
diagnostic: self.diagnostic.clone(),
}
}
}
impl<'a> DiagnosticEntryRef<'a, Anchor> {
/// Converts the [DiagnosticEntry] to a different buffer coordinate type.
pub fn resolve<O: FromAnchor>(
&self,
buffer: &text::BufferSnapshot,
) -> DiagnosticEntryRef<'a, O> {
DiagnosticEntryRef {
range: O::from_anchor(&self.range.start, buffer)
..O::from_anchor(&self.range.end, buffer),
diagnostic: &self.diagnostic,
}
}
}
/// A group of related diagnostics, ordered by their start position
/// in the buffer.
#[derive(Debug, Serialize)]
pub struct DiagnosticGroup<'a, T> {
/// The diagnostics.
pub entries: Vec<DiagnosticEntryRef<'a, T>>,
/// The index into `entries` where the primary diagnostic is stored.
pub primary_ix: usize,
}
impl<'a> DiagnosticGroup<'a, Anchor> {
/// Converts the entries in this [`DiagnosticGroup`] to a different buffer coordinate type.
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticGroup<'a, O> {
DiagnosticGroup {
entries: self
.entries
.iter()
.map(|entry| entry.resolve(buffer))
.collect(),
primary_ix: self.primary_ix,
}
}
}
#[derive(Clone, Debug)]
pub struct Summary {
start: Anchor,
end: Anchor,
min_start: Anchor,
max_end: Anchor,
count: usize,
}
impl DiagnosticEntry<PointUtf16> {
/// Returns a raw LSP diagnostic used to provide diagnostic context to LSP
/// codeAction request
pub fn to_lsp_diagnostic_stub(&self) -> Result<lsp::Diagnostic> {
let range = range_to_lsp(self.range.clone())?;
Ok(lsp::Diagnostic {
range,
code: self.diagnostic.code.clone(),
severity: Some(self.diagnostic.severity),
source: self.diagnostic.source.clone(),
message: self.diagnostic.message.clone(),
data: self.diagnostic.data.clone(),
..Default::default()
})
}
}
impl DiagnosticEntryRef<'_, PointUtf16> {
/// Returns a raw LSP diagnostic used to provide diagnostic context to LSP
/// codeAction request
pub fn to_lsp_diagnostic_stub(&self) -> Result<lsp::Diagnostic> {
let range = range_to_lsp(self.range.clone())?;
Ok(lsp::Diagnostic {
range,
code: self.diagnostic.code.clone(),
severity: Some(self.diagnostic.severity),
source: self.diagnostic.source.clone(),
message: self.diagnostic.message.clone(),
data: self.diagnostic.data.clone(),
..Default::default()
})
}
}
impl DiagnosticSet {
/// Constructs a [DiagnosticSet] from a sequence of entries, ordered by
/// their position in the buffer.
pub fn from_sorted_entries<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
where
I: IntoIterator<Item = DiagnosticEntry<Anchor>>,
{
Self {
diagnostics: SumTree::from_iter(iter, buffer),
}
}
/// Constructs a [DiagnosticSet] from a sequence of entries in an arbitrary order.
pub fn new<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
where
I: IntoIterator<Item = DiagnosticEntry<PointUtf16>>,
{
let mut entries = iter.into_iter().collect::<Vec<_>>();
entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end)));
Self {
diagnostics: SumTree::from_iter(
entries.into_iter().map(|entry| DiagnosticEntry {
range: buffer.anchor_before(entry.range.start)
..buffer.anchor_before(entry.range.end),
diagnostic: entry.diagnostic,
}),
buffer,
),
}
}
/// Returns the number of diagnostics in the set.
pub fn len(&self) -> usize {
self.diagnostics.summary().count
}
/// Returns true when there are no diagnostics in this diagnostic set
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns an iterator over the diagnostic entries in the set.
pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
self.diagnostics.iter()
}
/// Returns an iterator over the diagnostic entries that intersect the
/// given range of the buffer.
pub fn range<'a, T, O>(
&'a self,
range: Range<T>,
buffer: &'a text::BufferSnapshot,
inclusive: bool,
reversed: bool,
) -> impl 'a + Iterator<Item = DiagnosticEntryRef<'a, O>>
where
T: 'a + ToOffset,
O: FromAnchor,
{
let end_bias = if inclusive { Bias::Right } else { Bias::Left };
let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
let mut cursor = self.diagnostics.filter::<_, ()>(buffer, {
move |summary: &Summary| {
let start_cmp = range.start.cmp(&summary.max_end, buffer);
let end_cmp = range.end.cmp(&summary.min_start, buffer);
if inclusive {
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
} else {
start_cmp == Ordering::Less && end_cmp == Ordering::Greater
}
}
});
if reversed {
cursor.prev();
} else {
cursor.next();
}
iter::from_fn({
move || {
if let Some(diagnostic) = cursor.item() {
if reversed {
cursor.prev();
} else {
cursor.next();
}
Some(diagnostic.resolve(buffer))
} else {
None
}
}
})
}
/// Adds all of this set's diagnostic groups to the given output vector.
pub fn groups<'a>(
&'a self,
language_server_id: LanguageServerId,
output: &mut Vec<(LanguageServerId, DiagnosticGroup<'a, Anchor>)>,
buffer: &text::BufferSnapshot,
) {
let mut groups = HashMap::default();
for entry in self.diagnostics.iter() {
groups
.entry(entry.diagnostic.group_id)
.or_insert(Vec::new())
.push(DiagnosticEntryRef {
range: entry.range.clone(),
diagnostic: &entry.diagnostic,
});
}
let start_ix = output.len();
output.extend(groups.into_values().filter_map(|mut entries| {
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer));
entries
.iter()
.position(|entry| entry.diagnostic.is_primary)
.map(|primary_ix| {
(
language_server_id,
DiagnosticGroup {
entries,
primary_ix,
},
)
})
}));
output[start_ix..].sort_unstable_by(|(id_a, group_a), (id_b, group_b)| {
group_a.entries[group_a.primary_ix]
.range
.start
.cmp(&group_b.entries[group_b.primary_ix].range.start, buffer)
.then_with(|| id_a.cmp(id_b))
});
}
/// Returns all of the diagnostics in a particular diagnostic group,
/// in order of their position in the buffer.
pub fn group<'a, O: FromAnchor>(
&'a self,
group_id: usize,
buffer: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = DiagnosticEntryRef<'a, O>> {
self.iter()
.filter(move |entry| entry.diagnostic.group_id == group_id)
.map(|entry| entry.resolve(buffer))
}
}
impl sum_tree::Item for DiagnosticEntry<Anchor> {
type Summary = Summary;
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
Summary {
start: self.range.start,
end: self.range.end,
min_start: self.range.start,
max_end: self.range.end,
count: 1,
}
}
}
impl DiagnosticEntry<Anchor> {
/// Converts the [DiagnosticEntry] to a different buffer coordinate type.
pub fn resolve<'a, O: FromAnchor>(
&'a self,
buffer: &text::BufferSnapshot,
) -> DiagnosticEntryRef<'a, O> {
DiagnosticEntryRef {
range: O::from_anchor(&self.range.start, buffer)
..O::from_anchor(&self.range.end, buffer),
diagnostic: &self.diagnostic,
}
}
}
impl sum_tree::Summary for Summary {
type Context<'a> = &'a text::BufferSnapshot;
fn zero(buffer: &text::BufferSnapshot) -> Self {
Self {
start: Anchor::min_for_buffer(buffer.remote_id()),
end: Anchor::max_for_buffer(buffer.remote_id()),
min_start: Anchor::max_for_buffer(buffer.remote_id()),
max_end: Anchor::min_for_buffer(buffer.remote_id()),
count: 0,
}
}
fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
if other.min_start.cmp(&self.min_start, buffer).is_lt() {
self.min_start = other.min_start;
}
if other.max_end.cmp(&self.max_end, buffer).is_gt() {
self.max_end = other.max_end;
}
self.start = other.start;
self.end = other.end;
self.count += other.count;
}
}

View File

@@ -0,0 +1,158 @@
pub const FILE_ANALYSIS_BYTES: usize = 1024;
#[derive(Debug, PartialEq)]
pub enum ByteContent {
Utf16Le,
Utf16Be,
Binary,
Unknown,
}
// Heuristic check using null byte distribution plus a generic text-likeness
// heuristic. This prefers UTF-16 when many bytes are NUL and otherwise
// distinguishes between text-like and binary-like content.
pub fn analyze_byte_content(bytes: &[u8]) -> ByteContent {
if bytes.len() < 2 {
return ByteContent::Unknown;
}
if is_known_binary_header(bytes) {
return ByteContent::Binary;
}
let limit = bytes.len().min(FILE_ANALYSIS_BYTES);
let mut even_null_count = 0usize;
let mut odd_null_count = 0usize;
let mut non_text_like_count = 0usize;
for (i, &byte) in bytes[..limit].iter().enumerate() {
if byte == 0 {
if i % 2 == 0 {
even_null_count += 1;
} else {
odd_null_count += 1;
}
non_text_like_count += 1;
continue;
}
let is_text_like = match byte {
b'\t' | b'\n' | b'\r' | 0x0C => true,
0x20..=0x7E => true,
// Treat bytes that are likely part of UTF-8 or single-byte encodings as text-like.
0x80..=0xBF | 0xC2..=0xF4 => true,
_ => false,
};
if !is_text_like {
non_text_like_count += 1;
}
}
let total_null_count = even_null_count + odd_null_count;
// If there are no NUL bytes at all, this is overwhelmingly likely to be text.
if total_null_count == 0 {
return ByteContent::Unknown;
}
let has_significant_nulls = total_null_count >= limit / 16;
let nulls_skew_to_even = even_null_count > odd_null_count * 4;
let nulls_skew_to_odd = odd_null_count > even_null_count * 4;
if has_significant_nulls {
let sample = &bytes[..limit];
// UTF-16BE ASCII: [0x00, char] — nulls at even positions (high byte first)
// UTF-16LE ASCII: [char, 0x00] — nulls at odd positions (low byte first)
if nulls_skew_to_even && is_plausible_utf16_text(sample, false) {
return ByteContent::Utf16Be;
}
if nulls_skew_to_odd && is_plausible_utf16_text(sample, true) {
return ByteContent::Utf16Le;
}
return ByteContent::Binary;
}
if non_text_like_count * 100 < limit * 8 {
ByteContent::Unknown
} else {
ByteContent::Binary
}
}
fn is_known_binary_header(bytes: &[u8]) -> bool {
bytes.starts_with(b"%PDF-") // PDF
|| bytes.starts_with(b"PK\x03\x04") // ZIP local header
|| bytes.starts_with(b"PK\x05\x06") // ZIP end of central directory
|| bytes.starts_with(b"PK\x07\x08") // ZIP spanning/splitting
|| bytes.starts_with(b"\x89PNG\r\n\x1a\n") // PNG
|| bytes.starts_with(b"\xFF\xD8\xFF") // JPEG
|| bytes.starts_with(b"GIF87a") // GIF87a
|| bytes.starts_with(b"GIF89a") // GIF89a
|| bytes.starts_with(b"IWAD") // Doom IWAD archive
|| bytes.starts_with(b"PWAD") // Doom PWAD archive
|| bytes.starts_with(b"RIFF") // WAV, AVI, WebP
|| bytes.starts_with(b"OggS") // OGG (Vorbis, Opus, FLAC)
|| bytes.starts_with(b"fLaC") // FLAC
|| bytes.starts_with(b"ID3") // MP3 with ID3v2 tag
|| bytes.starts_with(b"\xFF\xFB") // MP3 frame sync (MPEG1 Layer3)
|| bytes.starts_with(b"\xFF\xFA") // MP3 frame sync (MPEG1 Layer3)
|| bytes.starts_with(b"\xFF\xF3") // MP3 frame sync (MPEG2 Layer3)
|| bytes.starts_with(b"\xFF\xF2") // MP3 frame sync (MPEG2 Layer3)
}
// Null byte skew alone is not enough to identify UTF-16 -- binary formats with
// small 16-bit values (like PCM audio) produce the same pattern. Decode the
// bytes as UTF-16 and reject if too many code units land in control character
// ranges or form unpaired surrogates, which real text almost never contains.
fn is_plausible_utf16_text(bytes: &[u8], little_endian: bool) -> bool {
let mut suspicious_count = 0usize;
let mut total = 0usize;
let mut i = 0;
while let Some(code_unit) = read_u16(bytes, i, little_endian) {
total += 1;
match code_unit {
0x0009 | 0x000A | 0x000C | 0x000D => {}
// C0/C1 control characters and non-characters
0x0000..=0x001F | 0x007F..=0x009F | 0xFFFE | 0xFFFF => suspicious_count += 1,
0xD800..=0xDBFF => {
let next_offset = i + 2;
let has_low_surrogate = read_u16(bytes, next_offset, little_endian)
.is_some_and(|next| (0xDC00..=0xDFFF).contains(&next));
if has_low_surrogate {
total += 1;
i += 2;
} else {
suspicious_count += 1;
}
}
// Lone low surrogate without a preceding high surrogate
0xDC00..=0xDFFF => suspicious_count += 1,
_ => {}
}
i += 2;
}
if total == 0 {
return false;
}
// Real UTF-16 text has near-zero control characters; binary data with
// small 16-bit values typically exceeds 5%. 2% provides a safe margin.
suspicious_count * 100 < total * 2
}
fn read_u16(bytes: &[u8], offset: usize, little_endian: bool) -> Option<u16> {
let pair = [*bytes.get(offset)?, *bytes.get(offset + 1)?];
if little_endian {
return Some(u16::from_le_bytes(pair));
}
Some(u16::from_be_bytes(pair))
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
use std::sync::Arc;
use settings::WorktreeId;
use util::rel_path::RelPath;
// Re-export ManifestName from language_core.
pub use language_core::ManifestName;
/// Represents a manifest query; given a path to a file, the manifest provider is tasked with finding a path to the directory containing the manifest for that file.
///
/// Since parts of the path might have already been explored, there's an additional `depth` parameter that indicates to what ancestry level a given path should be explored.
/// For example, given a path like `foo/bar/baz`, a depth of 2 would explore `foo/bar/baz` and `foo/bar`, but not `foo`.
pub struct ManifestQuery {
/// Path to the file, relative to worktree root.
pub path: Arc<RelPath>,
pub depth: usize,
pub delegate: Arc<dyn ManifestDelegate>,
}
pub trait ManifestProvider {
fn name(&self) -> ManifestName;
fn search(&self, query: ManifestQuery) -> Option<Arc<RelPath>>;
}
pub trait ManifestDelegate: Send + Sync {
fn worktree_id(&self) -> WorktreeId;
fn exists(&self, path: &RelPath, is_dir: Option<bool>) -> bool;
}

View File

@@ -0,0 +1,763 @@
use regex::Regex;
use std::{num::NonZeroU32, sync::LazyLock};
/// The settings extracted from an emacs/vim modelines.
///
/// The parsing tries to best match the modeline directives and
/// variables to Zed, matching LanguageSettings fields.
/// The mode mapping is done later thanks to the LanguageRegistry.
///
/// It is not exhaustive, but covers the most common settings.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ModelineSettings {
/// The emacs mode or vim filetype.
pub mode: Option<String>,
/// How many columns a tab should occupy.
pub tab_size: Option<NonZeroU32>,
/// Whether to indent lines using tab characters, as opposed to multiple
/// spaces.
pub hard_tabs: Option<bool>,
/// The number of bytes that comprise the indentation.
pub indent_size: Option<NonZeroU32>,
/// Whether to auto-indent lines.
pub auto_indent: Option<bool>,
/// The column at which to soft-wrap lines.
pub preferred_line_length: Option<NonZeroU32>,
/// Whether to ensure a final newline at the end of the file.
pub ensure_final_newline: Option<bool>,
/// Whether to show trailing whitespace on the editor.
pub show_trailing_whitespace: Option<bool>,
/// Emacs modeline variables that were parsed but not mapped to Zed settings.
/// Stored as (variable-name, value) pairs.
pub emacs_extra_variables: Vec<(String, String)>,
/// Vim modeline options that were parsed but not mapped to Zed settings.
/// Stored as (option-name, value) pairs.
pub vim_extra_variables: Vec<(String, Option<String>)>,
}
impl ModelineSettings {
fn has_settings(&self) -> bool {
self != &Self::default()
}
}
/// Parse modelines from file content.
///
/// Supports:
/// - Emacs modelines: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- and "Local Variables"
/// - Vim modelines: vim: set ft=rust ts=4 sw=4 et:
pub fn parse_modeline(first_lines: &[&str], last_lines: &[&str]) -> Option<ModelineSettings> {
let mut settings = ModelineSettings::default();
parse_modelines(first_lines, &mut settings);
// Parse Emacs Local Variables in last lines
parse_emacs_local_variables(last_lines, &mut settings);
// Also check for vim modelines in last lines if we don't have settings yet
if !settings.has_settings() {
parse_vim_modelines(last_lines, &mut settings);
}
Some(settings).filter(|s| s.has_settings())
}
fn parse_modelines(modelines: &[&str], settings: &mut ModelineSettings) {
for line in modelines {
parse_emacs_modeline(line, settings);
// if emacs is set, do not check for vim modelines
if settings.has_settings() {
return;
}
}
parse_vim_modelines(modelines, settings);
}
static EMACS_MODELINE_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"-\*-\s*(.+?)\s*-\*-").expect("valid regex"));
/// Parse Emacs-style modelines
/// Format: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*-
/// See Emacs (set-auto-mode)
fn parse_emacs_modeline(line: &str, settings: &mut ModelineSettings) {
let Some(captures) = EMACS_MODELINE_RE.captures(line) else {
return;
};
let Some(modeline_content) = captures.get(1).map(|m| m.as_str()) else {
return;
};
for part in modeline_content.split(';') {
parse_emacs_key_value(part, settings, true);
}
}
/// Parse Emacs-style Local Variables block
///
/// Emacs supports a "Local Variables" block at the end of files:
/// ```text
/// /* Local Variables: */
/// /* mode: c */
/// /* tab-width: 4 */
/// /* End: */
/// ```
///
/// Emacs related code is hack-local-variables--find-variables in
/// https://cgit.git.savannah.gnu.org/cgit/emacs.git/tree/lisp/files.el#n4346
fn parse_emacs_local_variables(lines: &[&str], settings: &mut ModelineSettings) {
const LOCAL_VARIABLES: &str = "Local Variables:";
let Some((start_idx, prefix, suffix)) = lines.iter().enumerate().find_map(|(i, line)| {
let prefix_len = line.find(LOCAL_VARIABLES)?;
let suffix_start = prefix_len + LOCAL_VARIABLES.len();
Some((i, line.get(..prefix_len)?, line.get(suffix_start..)?))
}) else {
return;
};
let mut continuation = String::new();
for line in &lines[start_idx + 1..] {
let Some(content) = line
.strip_prefix(prefix)
.and_then(|l| l.strip_suffix(suffix))
.map(str::trim)
else {
return;
};
if let Some(continued) = content.strip_suffix('\\') {
continuation.push_str(continued);
continue;
}
let to_parse = if continuation.is_empty() {
content
} else {
continuation.push_str(content);
&continuation
};
if to_parse == "End:" {
return;
}
parse_emacs_key_value(to_parse, settings, false);
continuation.clear();
}
}
fn parse_emacs_key_value(part: &str, settings: &mut ModelineSettings, bare: bool) {
let part = part.trim();
if part.is_empty() {
return;
}
if let Some((key, value)) = part.split_once(':') {
let key = key.trim();
let value = value.trim();
match key.to_lowercase().as_str() {
"mode" => {
settings.mode = Some(value.to_string());
}
"c-basic-offset" | "python-indent-offset" => {
if let Ok(size) = value.parse::<NonZeroU32>() {
settings.indent_size = Some(size);
}
}
"fill-column" => {
if let Ok(size) = value.parse::<NonZeroU32>() {
settings.preferred_line_length = Some(size);
}
}
"tab-width" => {
if let Ok(size) = value.parse::<NonZeroU32>() {
settings.tab_size = Some(size);
}
}
"indent-tabs-mode" => {
settings.hard_tabs = Some(value != "nil");
}
"electric-indent-mode" => {
settings.auto_indent = Some(value != "nil");
}
"require-final-newline" => {
settings.ensure_final_newline = Some(value != "nil");
}
"show-trailing-whitespace" => {
settings.show_trailing_whitespace = Some(value != "nil");
}
key => settings
.emacs_extra_variables
.push((key.to_string(), value.to_string())),
}
} else if bare {
// Handle bare mode specification (e.g., -*- rust -*-)
settings.mode = Some(part.to_string());
}
}
fn parse_vim_modelines(modelines: &[&str], settings: &mut ModelineSettings) {
for line in modelines {
parse_vim_modeline(line, settings);
}
}
static VIM_MODELINE_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
[
// Second form: [text{white}]{vi:vim:Vim:}[white]se[t] {options}:[text]
// Allow escaped colons in options: match non-colon chars or backslash followed by any char
r"(?:^|\s)(vi|vim|Vim):(?:\s*)se(?:t)?\s+((?:[^\\:]|\\.)*):",
// First form: [text{white}]{vi:vim:}[white]{options}
r"(?:^|\s+)(vi|vim):(?:\s*(.+))",
]
.iter()
.map(|pattern| Regex::new(pattern).expect("valid regex"))
.collect()
});
/// Parse Vim-style modelines
/// Supports both forms:
/// 1. First form: vi:noai:sw=3 ts=6
/// 2. Second form: vim: set ft=rust ts=4 sw=4 et:
fn parse_vim_modeline(line: &str, settings: &mut ModelineSettings) {
for re in VIM_MODELINE_PATTERNS.iter() {
if let Some(captures) = re.captures(line) {
if let Some(options) = captures.get(2) {
parse_vim_settings(options.as_str().trim(), settings);
break;
}
}
}
}
fn parse_vim_settings(content: &str, settings: &mut ModelineSettings) {
fn split_colon_unescape(input: &str) -> Vec<String> {
let mut split = Vec::new();
let mut str = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some(escaped_char) => str.push(escaped_char),
None => str.push('\\'),
}
} else if c == ':' {
split.push(std::mem::take(&mut str));
} else {
str.push(c);
}
}
split.push(str);
split
}
let parts = split_colon_unescape(content);
for colon_part in parts {
let colon_part = colon_part.trim();
if colon_part.is_empty() {
continue;
}
// Each colon part might contain space-separated options
for part in colon_part.split_whitespace() {
if let Some((key, value)) = part.split_once('=') {
match key {
"ft" | "filetype" => {
settings.mode = Some(value.to_string());
}
"ts" | "tabstop" => {
if let Ok(size) = value.parse::<NonZeroU32>() {
settings.tab_size = Some(size);
}
}
"sw" | "shiftwidth" => {
if let Ok(size) = value.parse::<NonZeroU32>() {
settings.indent_size = Some(size);
}
}
"tw" | "textwidth" => {
if let Ok(size) = value.parse::<NonZeroU32>() {
settings.preferred_line_length = Some(size);
}
}
_ => {
settings
.vim_extra_variables
.push((key.to_string(), Some(value.to_string())));
}
}
} else {
match part {
"ai" | "autoindent" => {
settings.auto_indent = Some(true);
}
"noai" | "noautoindent" => {
settings.auto_indent = Some(false);
}
"et" | "expandtab" => {
settings.hard_tabs = Some(false);
}
"noet" | "noexpandtab" => {
settings.hard_tabs = Some(true);
}
"eol" | "endofline" => {
settings.ensure_final_newline = Some(true);
}
"noeol" | "noendofline" => {
settings.ensure_final_newline = Some(false);
}
"set" => {
// Ignore the "set" keyword itself
}
_ => {
settings.vim_extra_variables.push((part.to_string(), None));
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_no_modeline() {
let content = "This is just regular content\nwith no modeline";
assert!(parse_modeline(&[content], &[content]).is_none());
}
#[test]
fn test_emacs_bare_mode() {
let content = "/* -*- rust -*- */";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("rust".to_string()),
..Default::default()
}
);
}
#[test]
fn test_emacs_modeline_parsing() {
let content = "/* -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- */";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("rust".to_string()),
tab_size: Some(NonZeroU32::new(4).unwrap()),
hard_tabs: Some(false),
..Default::default()
}
);
}
#[test]
fn test_emacs_last_line_parsing() {
let content = indoc! {r#"
# Local Variables:
# compile-command: "cc foo.c -Dfoo=bar -Dhack=whatever \
# -Dmumble=blaah"
# End:
"#}
.lines()
.collect::<Vec<_>>();
let settings = parse_modeline(&[], &content).unwrap();
assert_eq!(
settings,
ModelineSettings {
emacs_extra_variables: vec![(
"compile-command".to_string(),
"\"cc foo.c -Dfoo=bar -Dhack=whatever -Dmumble=blaah\"".to_string()
),],
..Default::default()
}
);
let content = indoc! {"
foo
/* Local Variables: */
/* eval: (font-lock-mode -1) */
/* mode: old-c */
/* mode: c */
/* End: */
/* mode: ignored */
"}
.lines()
.collect::<Vec<_>>();
let settings = parse_modeline(&[], &content).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("c".to_string()),
emacs_extra_variables: vec![(
"eval".to_string(),
"(font-lock-mode -1)".to_string()
),],
..Default::default()
}
);
}
#[test]
fn test_vim_modeline_parsing() {
// Test second form (set format)
let content = "// vim: set ft=rust ts=4 sw=4 et:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("rust".to_string()),
tab_size: Some(NonZeroU32::new(4).unwrap()),
hard_tabs: Some(false),
indent_size: Some(NonZeroU32::new(4).unwrap()),
..Default::default()
}
);
// Test first form (colon-separated)
let content = "vi:noai:sw=3:ts=6";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
tab_size: Some(NonZeroU32::new(6).unwrap()),
auto_indent: Some(false),
indent_size: Some(NonZeroU32::new(3).unwrap()),
..Default::default()
}
);
}
#[test]
fn test_vim_modeline_first_form() {
// Examples from vim specification: vi:noai:sw=3 ts=6
let content = " vi:noai:sw=3 ts=6 ";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
tab_size: Some(NonZeroU32::new(6).unwrap()),
auto_indent: Some(false),
indent_size: Some(NonZeroU32::new(3).unwrap()),
..Default::default()
}
);
// Test with filetype
let content = "vim:ft=python:ts=8:noet";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("python".to_string()),
tab_size: Some(NonZeroU32::new(8).unwrap()),
hard_tabs: Some(true),
..Default::default()
}
);
}
#[test]
fn test_vim_modeline_second_form() {
// Examples from vim specification: /* vim: set ai tw=75: */
let content = "/* vim: set ai tw=75: */";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
auto_indent: Some(true),
preferred_line_length: Some(NonZeroU32::new(75).unwrap()),
..Default::default()
}
);
// Test with 'Vim:' (capital V)
let content = "/* Vim: set ai tw=75: */";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
auto_indent: Some(true),
preferred_line_length: Some(NonZeroU32::new(75).unwrap()),
..Default::default()
}
);
// Test 'se' shorthand
let content = "// vi: se ft=c ts=4:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("c".to_string()),
tab_size: Some(NonZeroU32::new(4).unwrap()),
..Default::default()
}
);
// Test complex modeline with encoding
let content = "# vim: set ft=python ts=4 sw=4 et encoding=utf-8:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("python".to_string()),
tab_size: Some(NonZeroU32::new(4).unwrap()),
hard_tabs: Some(false),
indent_size: Some(NonZeroU32::new(4).unwrap()),
vim_extra_variables: vec![("encoding".to_string(), Some("utf-8".to_string()))],
..Default::default()
}
);
}
#[test]
fn test_vim_modeline_edge_cases() {
// Test modeline at start of line (compatibility with version 3.0)
let content = "vi:ts=2:et";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
tab_size: Some(NonZeroU32::new(2).unwrap()),
hard_tabs: Some(false),
..Default::default()
}
);
// Test vim at start of line
let content = "vim:ft=rust:noet";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("rust".to_string()),
hard_tabs: Some(true),
..Default::default()
}
);
// Test mixed boolean flags
let content = "vim: set wrap noet ts=8:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
tab_size: Some(NonZeroU32::new(8).unwrap()),
hard_tabs: Some(true),
vim_extra_variables: vec![("wrap".to_string(), None)],
..Default::default()
}
);
}
#[test]
fn test_vim_modeline_invalid_cases() {
// Test malformed options are ignored gracefully
let content = "vim: set ts=invalid ft=rust:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(
settings,
ModelineSettings {
mode: Some("rust".to_string()),
..Default::default()
}
);
// Test empty modeline content - this should still work as there might be options
let content = "vim: set :";
// This should return None because there are no actual options
let result = parse_modeline(&[content], &[]);
assert!(result.is_none(), "Expected None but got: {:?}", result);
// Test modeline without proper format
let content = "not a modeline";
assert!(parse_modeline(&[content], &[]).is_none());
// Test word that looks like modeline but isn't
let content = "example: this could be confused with ex:";
assert!(parse_modeline(&[content], &[]).is_none());
}
#[test]
fn test_vim_language_mapping() {
// Test vim-specific language mappings
let content = "vim: set ft=sh:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(settings.mode, Some("sh".to_string()));
let content = "vim: set ft=golang:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(settings.mode, Some("golang".to_string()));
let content = "vim: set filetype=js:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(settings.mode, Some("js".to_string()));
}
#[test]
fn test_vim_extra_variables() {
// Test that unknown vim options are stored as extra variables
let content = "vim: set foldmethod=marker conceallevel=2 custom=value:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert!(
settings
.vim_extra_variables
.contains(&("foldmethod".to_string(), Some("marker".to_string())))
);
assert!(
settings
.vim_extra_variables
.contains(&("conceallevel".to_string(), Some("2".to_string())))
);
assert!(
settings
.vim_extra_variables
.contains(&("custom".to_string(), Some("value".to_string())))
);
}
#[test]
fn test_modeline_position() {
// Test modeline in first lines
let first_lines = ["#!/bin/bash", "# vim: set ft=bash ts=4:"];
let settings = parse_modeline(&first_lines, &[]).unwrap();
assert_eq!(settings.mode, Some("bash".to_string()));
// Test modeline in last lines
let last_lines = ["", "/* vim: set ft=c: */"];
let settings = parse_modeline(&[], &last_lines).unwrap();
assert_eq!(settings.mode, Some("c".to_string()));
// Test no modeline found
let content = ["regular content", "no modeline here"];
assert!(parse_modeline(&content, &content).is_none());
}
#[test]
fn test_vim_modeline_version_checks() {
// Note: Current implementation doesn't support version checks yet
// These are tests for future implementation based on vim spec
// Test version-specific modelines (currently ignored in our implementation)
let content = "/* vim700: set foldmethod=marker */";
// Should be ignored for now since we don't support version checks
assert!(parse_modeline(&[content], &[]).is_none());
let content = "/* vim>702: set cole=2: */";
// Should be ignored for now since we don't support version checks
assert!(parse_modeline(&[content], &[]).is_none());
}
#[test]
fn test_vim_modeline_colon_escaping() {
// Test colon escaping as mentioned in vim spec
// According to vim spec: "if you want to include a ':' in a set command precede it with a '\'"
let content = r#"/* vim: set fdm=expr fde=getline(v\:lnum)=~'{'?'>1'\:'1': */"#;
let result = parse_modeline(&[content], &[]).unwrap();
// The modeline should parse fdm=expr and fde=getline(v:lnum)=~'{'?'>1':'1'
// as extra variables since they're not recognized settings
assert_eq!(result.vim_extra_variables.len(), 2);
assert_eq!(
result.vim_extra_variables[0],
("fdm".to_string(), Some("expr".to_string()))
);
assert_eq!(
result.vim_extra_variables[1],
(
"fde".to_string(),
Some("getline(v:lnum)=~'{'?'>1':'1'".to_string())
)
);
}
#[test]
fn test_vim_modeline_whitespace_requirements() {
// Test whitespace requirements from vim spec
// Valid: whitespace before vi/vim
let content = " vim: set ft=rust:";
assert!(parse_modeline(&[content], &[]).is_some());
// Valid: tab before vi/vim
let content = "\tvim: set ft=rust:";
assert!(parse_modeline(&[content], &[]).is_some());
// Valid: vi/vim at start of line (compatibility)
let content = "vim: set ft=rust:";
assert!(parse_modeline(&[content], &[]).is_some());
}
#[test]
fn test_vim_modeline_comprehensive_examples() {
// Real-world examples from vim documentation and common usage
// Python example
let content = "# vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(settings.hard_tabs, Some(false));
assert_eq!(settings.tab_size, Some(NonZeroU32::new(4).unwrap()));
// C example with multiple options
let content = "/* vim: set ts=8 sw=8 noet ai cindent: */";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(settings.tab_size, Some(NonZeroU32::new(8).unwrap()));
assert_eq!(settings.hard_tabs, Some(true));
assert!(
settings
.vim_extra_variables
.contains(&("cindent".to_string(), None))
);
// Shell script example
let content = "# vi: set ft=sh ts=2 sw=2 et:";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(settings.mode, Some("sh".to_string()));
assert_eq!(settings.tab_size, Some(NonZeroU32::new(2).unwrap()));
assert_eq!(settings.hard_tabs, Some(false));
// First form colon-separated
let content = "vim:ft=xml:ts=2:sw=2:et";
let settings = parse_modeline(&[content], &[]).unwrap();
assert_eq!(settings.mode, Some("xml".to_string()));
assert_eq!(settings.tab_size, Some(NonZeroU32::new(2).unwrap()));
assert_eq!(settings.hard_tabs, Some(false));
}
#[test]
fn test_combined_emacs_vim_detection() {
// Test that both emacs and vim modelines can be detected in the same file
let first_lines = [
"#!/usr/bin/env python3",
"# -*- require-final-newline: t; -*-",
"# vim: set ft=python ts=4 sw=4 et:",
];
// Should find the emacs modeline first (with coding)
let settings = parse_modeline(&first_lines, &[]).unwrap();
assert_eq!(settings.ensure_final_newline, Some(true));
assert_eq!(settings.tab_size, None);
// Test vim-only content
let vim_only = ["# vim: set ft=python ts=4 sw=4 et:"];
let settings = parse_modeline(&vim_only, &[]).unwrap();
assert_eq!(settings.mode, Some("python".to_string()));
assert_eq!(settings.tab_size, Some(NonZeroU32::new(4).unwrap()));
assert_eq!(settings.hard_tabs, Some(false));
}
}

View File

@@ -0,0 +1,327 @@
use crate::{BufferSnapshot, Point, ToPoint, ToTreeSitterPoint};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{BackgroundExecutor, HighlightStyle};
use std::ops::Range;
/// An outline of all the symbols contained in a buffer.
#[derive(Debug)]
pub struct Outline<T> {
pub items: Vec<OutlineItem<T>>,
candidates: Vec<StringMatchCandidate>,
pub path_candidates: Vec<StringMatchCandidate>,
path_candidate_prefixes: Vec<usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct OutlineItem<T> {
pub depth: usize,
pub range: Range<T>,
pub source_range_for_text: Range<T>,
pub text: String,
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
pub name_ranges: Vec<Range<usize>>,
pub body_range: Option<Range<T>>,
pub annotation_range: Option<Range<T>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SymbolPath(pub String);
impl<T: ToPoint> OutlineItem<T> {
/// Converts to an equivalent outline item, but with parameterized over Points.
pub fn to_point(&self, buffer: &BufferSnapshot) -> OutlineItem<Point> {
OutlineItem {
depth: self.depth,
range: self.range.start.to_point(buffer)..self.range.end.to_point(buffer),
source_range_for_text: self.source_range_for_text.start.to_point(buffer)
..self.source_range_for_text.end.to_point(buffer),
text: self.text.clone(),
highlight_ranges: self.highlight_ranges.clone(),
name_ranges: self.name_ranges.clone(),
body_range: self
.body_range
.as_ref()
.map(|r| r.start.to_point(buffer)..r.end.to_point(buffer)),
annotation_range: self
.annotation_range
.as_ref()
.map(|r| r.start.to_point(buffer)..r.end.to_point(buffer)),
}
}
pub fn body_range(&self, buffer: &BufferSnapshot) -> Option<Range<Point>> {
if let Some(range) = self.body_range.as_ref() {
return Some(range.start.to_point(buffer)..range.end.to_point(buffer));
}
let range = self.range.start.to_point(buffer)..self.range.end.to_point(buffer);
let start_indent = buffer.indent_size_for_line(range.start.row);
let node = buffer.syntax_ancestor(range.clone())?;
let mut cursor = node.walk();
loop {
let node = cursor.node();
if node.start_position() >= range.start.to_ts_point()
&& node.end_position() <= range.end.to_ts_point()
{
break;
}
cursor.goto_first_child_for_point(range.start.to_ts_point());
}
if !cursor.goto_last_child() {
return None;
}
let body_node = loop {
let node = cursor.node();
if node.child_count() > 0 {
break node;
}
if !cursor.goto_previous_sibling() {
return None;
}
};
let mut start_row = body_node.start_position().row as u32;
let mut end_row = body_node.end_position().row as u32;
while start_row < end_row && buffer.indent_size_for_line(start_row) == start_indent {
start_row += 1;
}
while start_row < end_row && buffer.indent_size_for_line(end_row - 1) == start_indent {
end_row -= 1;
}
if start_row < end_row {
return Some(Point::new(start_row, 0)..Point::new(end_row, 0));
}
None
}
}
impl<T> Outline<T> {
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
let mut candidates = Vec::new();
let mut path_candidates = Vec::new();
let mut path_candidate_prefixes = Vec::new();
let mut path_text = String::new();
let mut path_stack = Vec::new();
for (id, item) in items.iter().enumerate() {
if item.depth < path_stack.len() {
path_stack.truncate(item.depth);
path_text.truncate(path_stack.last().copied().unwrap_or(0));
}
if !path_text.is_empty() {
path_text.push(' ');
}
path_candidate_prefixes.push(path_text.len());
path_text.push_str(&item.text);
path_stack.push(path_text.len());
let candidate_text = item
.name_ranges
.iter()
.map(|range| &item.text[range.start..range.end])
.collect::<String>();
path_candidates.push(StringMatchCandidate::new(id, &path_text));
candidates.push(StringMatchCandidate::new(id, &candidate_text));
}
Self {
candidates,
path_candidates,
path_candidate_prefixes,
items,
}
}
/// Find the most similar symbol to the provided query using normalized Levenshtein distance.
pub fn find_most_similar(&self, query: &str) -> Option<(SymbolPath, &OutlineItem<T>)> {
const SIMILARITY_THRESHOLD: f64 = 0.6;
let (position, similarity) = self
.path_candidates
.iter()
.enumerate()
.map(|(index, candidate)| {
let similarity = strsim::normalized_levenshtein(&candidate.string, query);
(index, similarity)
})
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())?;
if similarity >= SIMILARITY_THRESHOLD {
self.path_candidates
.get(position)
.map(|candidate| SymbolPath(candidate.string.clone()))
.zip(self.items.get(position))
} else {
None
}
}
/// Find all outline symbols according to a longest subsequence match with the query, ordered descending by match score.
pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
let query = query.trim_start();
let is_path_query = query.contains(' ');
let smart_case = query.chars().any(|c| c.is_uppercase());
let mut matches = fuzzy::match_strings(
if is_path_query {
&self.path_candidates
} else {
&self.candidates
},
query,
smart_case,
true,
100,
&Default::default(),
executor.clone(),
)
.await;
matches.sort_unstable_by_key(|m| m.candidate_id);
let mut tree_matches = Vec::new();
let mut prev_item_ix = 0;
for mut string_match in matches {
let outline_match = &self.items[string_match.candidate_id];
string_match.string.clone_from(&outline_match.text);
if is_path_query {
let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
string_match
.positions
.retain(|position| *position >= prefix_len);
for position in &mut string_match.positions {
*position -= prefix_len;
}
} else {
let mut name_ranges = outline_match.name_ranges.iter();
let Some(mut name_range) = name_ranges.next() else {
continue;
};
let mut preceding_ranges_len = 0;
for position in &mut string_match.positions {
while *position >= preceding_ranges_len + name_range.len() {
preceding_ranges_len += name_range.len();
name_range = name_ranges.next().unwrap();
}
*position = name_range.start + (*position - preceding_ranges_len);
}
}
let insertion_ix = tree_matches.len();
let mut cur_depth = outline_match.depth;
for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
.iter()
.enumerate()
.rev()
{
if cur_depth == 0 {
break;
}
let candidate_index = ix + prev_item_ix;
if item.depth == cur_depth - 1 {
tree_matches.insert(
insertion_ix,
StringMatch {
candidate_id: candidate_index,
score: Default::default(),
positions: Default::default(),
string: Default::default(),
},
);
cur_depth -= 1;
}
}
prev_item_ix = string_match.candidate_id + 1;
tree_matches.push(string_match);
}
tree_matches
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
#[gpui::test]
async fn test_entries_with_no_names(cx: &mut TestAppContext) {
let outline = Outline::new(vec![
OutlineItem {
depth: 0,
range: Point::new(0, 0)..Point::new(5, 0),
source_range_for_text: Point::new(0, 0)..Point::new(0, 9),
text: "class Foo".to_string(),
highlight_ranges: vec![],
name_ranges: vec![6..9],
body_range: None,
annotation_range: None,
},
OutlineItem {
depth: 0,
range: Point::new(2, 0)..Point::new(2, 7),
source_range_for_text: Point::new(0, 0)..Point::new(0, 7),
text: "private".to_string(),
highlight_ranges: vec![],
name_ranges: vec![],
body_range: None,
annotation_range: None,
},
]);
assert_eq!(
outline
.search(" ", cx.executor())
.await
.into_iter()
.map(|mat| mat.string)
.collect::<Vec<String>>(),
vec!["class Foo".to_string()]
);
}
#[test]
fn test_find_most_similar_with_low_similarity() {
let outline = Outline::new(vec![
OutlineItem {
depth: 0,
range: Point::new(0, 0)..Point::new(5, 0),
source_range_for_text: Point::new(0, 0)..Point::new(0, 10),
text: "fn process".to_string(),
highlight_ranges: vec![],
name_ranges: vec![3..10],
body_range: None,
annotation_range: None,
},
OutlineItem {
depth: 0,
range: Point::new(7, 0)..Point::new(12, 0),
source_range_for_text: Point::new(0, 0)..Point::new(0, 20),
text: "struct DataProcessor".to_string(),
highlight_ranges: vec![],
name_ranges: vec![7..20],
body_range: None,
annotation_range: None,
},
]);
assert_eq!(
outline.find_most_similar("pub fn process"),
Some((SymbolPath("fn process".into()), &outline.items[0]))
);
assert_eq!(
outline.find_most_similar("async fn process"),
Some((SymbolPath("fn process".into()), &outline.items[0])),
);
assert_eq!(
outline.find_most_similar("struct Processor"),
Some((SymbolPath("struct DataProcessor".into()), &outline.items[1]))
);
assert_eq!(outline.find_most_similar("struct User"), None);
assert_eq!(outline.find_most_similar("struct"), None);
}
}

View File

@@ -0,0 +1,649 @@
//! Handles conversions of `language` items to and from the [`rpc`] protocol.
use crate::{CursorShape, Diagnostic, DiagnosticSourceKind, diagnostic_set::DiagnosticEntry};
use anyhow::{Context as _, Result};
use clock::ReplicaId;
use gpui::SharedString;
use lsp::{DiagnosticSeverity, LanguageServerId};
use rpc::proto;
use serde_json::Value;
use std::{ops::Range, str::FromStr, sync::Arc};
use text::*;
pub use proto::{BufferState, File, Operation};
use super::{point_from_lsp, point_to_lsp};
/// Deserializes a `[text::LineEnding]` from the RPC representation.
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
match message {
proto::LineEnding::Unix => text::LineEnding::Unix,
proto::LineEnding::Windows => text::LineEnding::Windows,
}
}
/// Serializes a [`text::LineEnding`] to be sent over RPC.
pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding {
match message {
text::LineEnding::Unix => proto::LineEnding::Unix,
text::LineEnding::Windows => proto::LineEnding::Windows,
}
}
/// Serializes a [`crate::Operation`] to be sent over RPC.
pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
proto::Operation {
variant: Some(match operation {
crate::Operation::Buffer(text::Operation::Edit(edit)) => {
proto::operation::Variant::Edit(serialize_edit_operation(edit))
}
crate::Operation::Buffer(text::Operation::Undo(undo)) => {
proto::operation::Variant::Undo(proto::operation::Undo {
replica_id: undo.timestamp.replica_id.as_u16() as u32,
lamport_timestamp: undo.timestamp.value,
version: serialize_version(&undo.version),
counts: undo
.counts
.iter()
.map(|(edit_id, count)| proto::UndoCount {
replica_id: edit_id.replica_id.as_u16() as u32,
lamport_timestamp: edit_id.value,
count: *count,
})
.collect(),
})
}
crate::Operation::UpdateSelections {
selections,
line_mode,
lamport_timestamp,
cursor_shape,
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
replica_id: lamport_timestamp.replica_id.as_u16() as u32,
lamport_timestamp: lamport_timestamp.value,
selections: serialize_selections(selections),
line_mode: *line_mode,
cursor_shape: serialize_cursor_shape(cursor_shape) as i32,
}),
crate::Operation::UpdateDiagnostics {
lamport_timestamp,
server_id,
diagnostics,
} => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
replica_id: lamport_timestamp.replica_id.as_u16() as u32,
lamport_timestamp: lamport_timestamp.value,
server_id: server_id.0 as u64,
diagnostics: serialize_diagnostics(diagnostics.iter()),
}),
crate::Operation::UpdateCompletionTriggers {
triggers,
lamport_timestamp,
server_id,
} => proto::operation::Variant::UpdateCompletionTriggers(
proto::operation::UpdateCompletionTriggers {
replica_id: lamport_timestamp.replica_id.as_u16() as u32,
lamport_timestamp: lamport_timestamp.value,
triggers: triggers.clone(),
language_server_id: server_id.to_proto(),
},
),
crate::Operation::UpdateLineEnding {
line_ending,
lamport_timestamp,
} => proto::operation::Variant::UpdateLineEnding(proto::operation::UpdateLineEnding {
replica_id: lamport_timestamp.replica_id.as_u16() as u32,
lamport_timestamp: lamport_timestamp.value,
line_ending: serialize_line_ending(*line_ending) as i32,
}),
}),
}
}
/// Serializes an [`EditOperation`] to be sent over RPC.
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
proto::operation::Edit {
replica_id: operation.timestamp.replica_id.as_u16() as u32,
lamport_timestamp: operation.timestamp.value,
version: serialize_version(&operation.version),
ranges: operation.ranges.iter().map(serialize_range).collect(),
new_text: operation
.new_text
.iter()
.map(|text| text.to_string())
.collect(),
}
}
/// Serializes an entry in the undo map to be sent over RPC.
pub fn serialize_undo_map_entry(
(edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]),
) -> proto::UndoMapEntry {
proto::UndoMapEntry {
replica_id: edit_id.replica_id.as_u16() as u32,
local_timestamp: edit_id.value,
counts: counts
.iter()
.map(|(undo_id, count)| proto::UndoCount {
replica_id: undo_id.replica_id.as_u16() as u32,
lamport_timestamp: undo_id.value,
count: *count,
})
.collect(),
}
}
/// Splits the given list of operations into chunks.
pub fn split_operations(
mut operations: Vec<proto::Operation>,
) -> impl Iterator<Item = Vec<proto::Operation>> {
#[cfg(any(test, feature = "test-support"))]
const CHUNK_SIZE: usize = 5;
#[cfg(not(any(test, feature = "test-support")))]
const CHUNK_SIZE: usize = 100;
let mut done = false;
std::iter::from_fn(move || {
if done {
return None;
}
let operations = operations
.drain(..std::cmp::min(CHUNK_SIZE, operations.len()))
.collect::<Vec<_>>();
if operations.is_empty() {
done = true;
}
Some(operations)
})
}
/// Serializes selections to be sent over RPC.
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
selections.iter().map(serialize_selection).collect()
}
/// Serializes a [`Selection`] to be sent over RPC.
pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
start: Some(proto::EditorAnchor {
anchor: Some(serialize_anchor(&selection.start)),
excerpt_id: None,
}),
end: Some(proto::EditorAnchor {
anchor: Some(serialize_anchor(&selection.end)),
excerpt_id: None,
}),
reversed: selection.reversed,
}
}
/// Serializes a [`CursorShape`] to be sent over RPC.
pub fn serialize_cursor_shape(cursor_shape: &CursorShape) -> proto::CursorShape {
match cursor_shape {
CursorShape::Bar => proto::CursorShape::CursorBar,
CursorShape::Block => proto::CursorShape::CursorBlock,
CursorShape::Underline => proto::CursorShape::CursorUnderscore,
CursorShape::Hollow => proto::CursorShape::CursorHollow,
}
}
/// Deserializes a [`CursorShape`] from the RPC representation.
pub fn deserialize_cursor_shape(cursor_shape: proto::CursorShape) -> CursorShape {
match cursor_shape {
proto::CursorShape::CursorBar => CursorShape::Bar,
proto::CursorShape::CursorBlock => CursorShape::Block,
proto::CursorShape::CursorUnderscore => CursorShape::Underline,
proto::CursorShape::CursorHollow => CursorShape::Hollow,
}
}
/// Serializes a list of diagnostics to be sent over RPC.
pub fn serialize_diagnostics<'a>(
diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<Anchor>>,
) -> Vec<proto::Diagnostic> {
diagnostics
.into_iter()
.map(|entry| proto::Diagnostic {
source: entry.diagnostic.source.clone(),
source_kind: match entry.diagnostic.source_kind {
DiagnosticSourceKind::Pulled => proto::diagnostic::SourceKind::Pulled,
DiagnosticSourceKind::Pushed => proto::diagnostic::SourceKind::Pushed,
DiagnosticSourceKind::Other => proto::diagnostic::SourceKind::Other,
} as i32,
start: Some(serialize_anchor(&entry.range.start)),
end: Some(serialize_anchor(&entry.range.end)),
message: entry.diagnostic.message.clone(),
markdown: entry.diagnostic.markdown.clone(),
severity: match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
_ => proto::diagnostic::Severity::None,
} as i32,
group_id: entry.diagnostic.group_id as u64,
is_primary: entry.diagnostic.is_primary,
underline: entry.diagnostic.underline,
code: entry.diagnostic.code.as_ref().map(|s| s.to_string()),
code_description: entry
.diagnostic
.code_description
.as_ref()
.map(|s| s.to_string()),
is_disk_based: entry.diagnostic.is_disk_based,
is_unnecessary: entry.diagnostic.is_unnecessary,
data: entry.diagnostic.data.as_ref().map(|data| data.to_string()),
registration_id: entry
.diagnostic
.registration_id
.as_ref()
.map(ToString::to_string),
})
.collect()
}
/// Serializes an [`Anchor`] to be sent over RPC.
pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
let timestamp = anchor.timestamp();
proto::Anchor {
replica_id: timestamp.replica_id.as_u16() as u32,
timestamp: timestamp.value,
offset: anchor.offset as u64,
bias: match anchor.bias {
Bias::Left => proto::Bias::Left as i32,
Bias::Right => proto::Bias::Right as i32,
},
buffer_id: Some(anchor.buffer_id.into()),
}
}
pub fn serialize_anchor_range(range: Range<Anchor>) -> proto::AnchorRange {
proto::AnchorRange {
start: Some(serialize_anchor(&range.start)),
end: Some(serialize_anchor(&range.end)),
}
}
/// Deserializes an [`Range<Anchor>`] from the RPC representation.
pub fn deserialize_anchor_range(range: proto::AnchorRange) -> Result<Range<Anchor>> {
Ok(
deserialize_anchor(range.start.context("invalid anchor")?).context("invalid anchor")?
..deserialize_anchor(range.end.context("invalid anchor")?).context("invalid anchor")?,
)
}
// This behavior is currently copied in the collab database, for snapshotting channel notes
/// Deserializes an [`crate::Operation`] from the RPC representation.
pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operation> {
Ok(
match message.variant.context("missing operation variant")? {
proto::operation::Variant::Edit(edit) => {
crate::Operation::Buffer(text::Operation::Edit(deserialize_edit_operation(edit)))
}
proto::operation::Variant::Undo(undo) => {
crate::Operation::Buffer(text::Operation::Undo(UndoOperation {
timestamp: clock::Lamport {
replica_id: ReplicaId::new(undo.replica_id as u16),
value: undo.lamport_timestamp,
},
version: deserialize_version(&undo.version),
counts: undo
.counts
.into_iter()
.map(|c| {
(
clock::Lamport {
replica_id: ReplicaId::new(c.replica_id as u16),
value: c.lamport_timestamp,
},
c.count,
)
})
.collect(),
}))
}
proto::operation::Variant::UpdateSelections(message) => {
let selections = message
.selections
.into_iter()
.filter_map(|selection| {
Some(Selection {
id: selection.id as usize,
start: deserialize_anchor(selection.start?.anchor?)?,
end: deserialize_anchor(selection.end?.anchor?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
})
})
.collect::<Vec<_>>();
crate::Operation::UpdateSelections {
lamport_timestamp: clock::Lamport {
replica_id: ReplicaId::new(message.replica_id as u16),
value: message.lamport_timestamp,
},
selections: Arc::from(selections),
line_mode: message.line_mode,
cursor_shape: deserialize_cursor_shape(
proto::CursorShape::from_i32(message.cursor_shape)
.context("Missing cursor shape")?,
),
}
}
proto::operation::Variant::UpdateDiagnostics(message) => {
crate::Operation::UpdateDiagnostics {
lamport_timestamp: clock::Lamport {
replica_id: ReplicaId::new(message.replica_id as u16),
value: message.lamport_timestamp,
},
server_id: LanguageServerId(message.server_id as usize),
diagnostics: deserialize_diagnostics(message.diagnostics),
}
}
proto::operation::Variant::UpdateCompletionTriggers(message) => {
crate::Operation::UpdateCompletionTriggers {
triggers: message.triggers,
lamport_timestamp: clock::Lamport {
replica_id: ReplicaId::new(message.replica_id as u16),
value: message.lamport_timestamp,
},
server_id: LanguageServerId::from_proto(message.language_server_id),
}
}
proto::operation::Variant::UpdateLineEnding(message) => {
crate::Operation::UpdateLineEnding {
lamport_timestamp: clock::Lamport {
replica_id: ReplicaId::new(message.replica_id as u16),
value: message.lamport_timestamp,
},
line_ending: deserialize_line_ending(
proto::LineEnding::from_i32(message.line_ending)
.context("missing line_ending")?,
),
}
}
},
)
}
/// Deserializes an [`EditOperation`] from the RPC representation.
pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
EditOperation {
timestamp: clock::Lamport {
replica_id: ReplicaId::new(edit.replica_id as u16),
value: edit.lamport_timestamp,
},
version: deserialize_version(&edit.version),
ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
new_text: edit.new_text.into_iter().map(Arc::from).collect(),
}
}
/// Deserializes an entry in the undo map from the RPC representation.
pub fn deserialize_undo_map_entry(
entry: proto::UndoMapEntry,
) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) {
(
clock::Lamport {
replica_id: ReplicaId::new(entry.replica_id as u16),
value: entry.local_timestamp,
},
entry
.counts
.into_iter()
.map(|undo_count| {
(
clock::Lamport {
replica_id: ReplicaId::new(undo_count.replica_id as u16),
value: undo_count.lamport_timestamp,
},
undo_count.count,
)
})
.collect(),
)
}
/// Deserializes selections from the RPC representation.
pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
selections
.into_iter()
.filter_map(deserialize_selection)
.collect()
}
/// Deserializes a [`Selection`] from the RPC representation.
pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
Some(Selection {
id: selection.id as usize,
start: deserialize_anchor(selection.start?.anchor?)?,
end: deserialize_anchor(selection.end?.anchor?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
})
}
/// Deserializes a list of diagnostics from the RPC representation.
pub fn deserialize_diagnostics(
diagnostics: Vec<proto::Diagnostic>,
) -> Arc<[DiagnosticEntry<Anchor>]> {
diagnostics
.into_iter()
.filter_map(|diagnostic| {
let data = if let Some(data) = diagnostic.data {
Some(Value::from_str(&data).ok()?)
} else {
None
};
Some(DiagnosticEntry {
range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?,
diagnostic: Diagnostic {
source: diagnostic.source,
severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? {
proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION,
proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
proto::diagnostic::Severity::None => return None,
},
message: diagnostic.message,
markdown: diagnostic.markdown,
group_id: diagnostic.group_id as usize,
code: diagnostic.code.map(lsp::NumberOrString::from_string),
code_description: diagnostic
.code_description
.and_then(|s| lsp::Uri::from_str(&s).ok()),
is_primary: diagnostic.is_primary,
is_disk_based: diagnostic.is_disk_based,
is_unnecessary: diagnostic.is_unnecessary,
underline: diagnostic.underline,
registration_id: diagnostic.registration_id.map(SharedString::from),
source_kind: match proto::diagnostic::SourceKind::from_i32(
diagnostic.source_kind,
)? {
proto::diagnostic::SourceKind::Pulled => DiagnosticSourceKind::Pulled,
proto::diagnostic::SourceKind::Pushed => DiagnosticSourceKind::Pushed,
proto::diagnostic::SourceKind::Other => DiagnosticSourceKind::Other,
},
data,
},
})
})
.collect()
}
/// Deserializes an [`Anchor`] from the RPC representation.
pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
let buffer_id = if let Some(id) = anchor.buffer_id {
Some(BufferId::new(id).ok()?)
} else {
None
};
let timestamp = clock::Lamport {
replica_id: ReplicaId::new(anchor.replica_id as u16),
value: anchor.timestamp,
};
let bias = match proto::Bias::from_i32(anchor.bias)? {
proto::Bias::Left => Bias::Left,
proto::Bias::Right => Bias::Right,
};
Some(Anchor::new(
timestamp,
anchor.offset as u32,
bias,
buffer_id?,
))
}
/// Returns a `[clock::Lamport`] timestamp for the given [`proto::Operation`].
pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<clock::Lamport> {
let replica_id;
let value;
match operation.variant.as_ref()? {
proto::operation::Variant::Edit(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::Undo(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateDiagnostics(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateSelections(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateCompletionTriggers(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateLineEnding(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
}
Some(clock::Lamport {
replica_id: ReplicaId::new(replica_id as u16),
value,
})
}
/// Serializes a [`Transaction`] to be sent over RPC.
pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
proto::Transaction {
id: Some(serialize_timestamp(transaction.id)),
edit_ids: transaction
.edit_ids
.iter()
.copied()
.map(serialize_timestamp)
.collect(),
start: serialize_version(&transaction.start),
}
}
/// Deserializes a [`Transaction`] from the RPC representation.
pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transaction> {
Ok(Transaction {
id: deserialize_timestamp(transaction.id.context("missing transaction id")?),
edit_ids: transaction
.edit_ids
.into_iter()
.map(deserialize_timestamp)
.collect(),
start: deserialize_version(&transaction.start),
})
}
/// Serializes a [`clock::Lamport`] timestamp to be sent over RPC.
pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp {
proto::LamportTimestamp {
replica_id: timestamp.replica_id.as_u16() as u32,
value: timestamp.value,
}
}
/// Deserializes a [`clock::Lamport`] timestamp from the RPC representation.
pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport {
clock::Lamport {
replica_id: ReplicaId::new(timestamp.replica_id as u16),
value: timestamp.value,
}
}
/// Serializes a range of [`FullOffset`]s to be sent over RPC.
pub fn serialize_range(range: &Range<FullOffset>) -> proto::Range {
proto::Range {
start: range.start.0 as u64,
end: range.end.0 as u64,
}
}
/// Deserializes a range of [`FullOffset`]s from the RPC representation.
pub fn deserialize_range(range: proto::Range) -> Range<FullOffset> {
FullOffset(range.start as usize)..FullOffset(range.end as usize)
}
/// Deserializes a clock version from the RPC representation.
pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global {
let mut version = clock::Global::new();
for entry in message {
version.observe(clock::Lamport {
replica_id: ReplicaId::new(entry.replica_id as u16),
value: entry.timestamp,
});
}
version
}
/// Serializes a clock version to be sent over RPC.
pub fn serialize_version(version: &clock::Global) -> Vec<proto::VectorClockEntry> {
version
.iter()
.map(|entry| proto::VectorClockEntry {
replica_id: entry.replica_id.as_u16() as u32,
timestamp: entry.value,
})
.collect()
}
pub fn serialize_lsp_edit(edit: lsp::TextEdit) -> proto::TextEdit {
let start = point_from_lsp(edit.range.start).0;
let end = point_from_lsp(edit.range.end).0;
proto::TextEdit {
new_text: edit.new_text,
lsp_range_start: Some(proto::PointUtf16 {
row: start.row,
column: start.column,
}),
lsp_range_end: Some(proto::PointUtf16 {
row: end.row,
column: end.column,
}),
}
}
pub fn deserialize_lsp_edit(edit: proto::TextEdit) -> Option<lsp::TextEdit> {
let start = edit.lsp_range_start?;
let start = PointUtf16::new(start.row, start.column);
let end = edit.lsp_range_end?;
let end = PointUtf16::new(end.row, end.column);
Some(lsp::TextEdit {
range: lsp::Range {
start: point_to_lsp(start),
end: point_to_lsp(end),
},
new_text: edit.new_text,
})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
use std::{ops::Range, path::PathBuf, sync::Arc};
use crate::{Buffer, LanguageToolchainStore, Location, Runnable};
use anyhow::Result;
use collections::HashMap;
use fs::Fs;
use gpui::{App, Entity, Task};
use lsp::LanguageServerName;
use task::{TaskTemplates, TaskVariables};
use text::BufferId;
pub struct RunnableRange {
pub buffer_id: BufferId,
pub run_range: Range<usize>,
pub full_range: Range<usize>,
pub runnable: Runnable,
pub extra_captures: HashMap<String, String>,
}
/// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
///
/// The context will be used to fill data for the tasks, and filter out the ones that do not have the variables required.
pub trait ContextProvider: Send + Sync {
/// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
fn build_context(
&self,
_variables: &TaskVariables,
_location: ContextLocation<'_>,
_project_env: Option<HashMap<String, String>>,
_toolchains: Arc<dyn LanguageToolchainStore>,
_cx: &mut App,
) -> Task<Result<TaskVariables>> {
let _ = _location;
Task::ready(Ok(TaskVariables::default()))
}
/// Provides all tasks, associated with the current language.
fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
Task::ready(None)
}
/// A language server name, that can return tasks using LSP (ext) for this language.
fn lsp_task_source(&self) -> Option<LanguageServerName> {
None
}
}
/// Metadata about the place in the project we gather the context for.
pub struct ContextLocation<'a> {
pub fs: Option<Arc<dyn Fs>>,
pub worktree_root: Option<PathBuf>,
pub file_location: &'a Location,
}

View File

@@ -0,0 +1,576 @@
use crate::{CharClassifier, CharKind, CharScopeContext, LanguageScope};
use anyhow::{Context, anyhow};
use imara_diff::{
Algorithm, Sink, diff,
intern::{InternedInput, Interner, Token},
sources::lines_with_terminator,
};
use std::{fmt::Write, iter, ops::Range, sync::Arc};
const MAX_WORD_DIFF_LEN: usize = 512;
const MAX_WORD_DIFF_LINE_COUNT: usize = 8;
/// Computes a diff between two strings, returning a unified diff string.
pub fn unified_diff(old_text: &str, new_text: &str) -> String {
unified_diff_with_offsets(old_text, new_text, 0, 0)
}
/// Computes a diff between two strings, returning a unified diff string with
/// hunk headers adjusted to reflect the given starting line numbers (zero-indexed).
pub fn unified_diff_with_offsets(
old_text: &str,
new_text: &str,
old_start_line: u32,
new_start_line: u32,
) -> String {
unified_diff_with_context(old_text, new_text, old_start_line, new_start_line, 3)
}
/// Computes a diff between two strings, returning a unified diff string with
/// hunk headers adjusted to reflect the given starting line numbers (zero-indexed),
/// and a configurable number of context lines around changes.
pub fn unified_diff_with_context(
old_text: &str,
new_text: &str,
old_start_line: u32,
new_start_line: u32,
context_lines: u32,
) -> String {
let input = InternedInput::new(old_text, new_text);
diff(
Algorithm::Histogram,
&input,
OffsetUnifiedDiffBuilder::new(&input, old_start_line, new_start_line, context_lines),
)
}
/// A unified diff builder that applies line number offsets to hunk headers.
struct OffsetUnifiedDiffBuilder<'a> {
before: &'a [Token],
after: &'a [Token],
interner: &'a Interner<&'a str>,
pos: u32,
before_hunk_start: u32,
after_hunk_start: u32,
before_hunk_len: u32,
after_hunk_len: u32,
old_line_offset: u32,
new_line_offset: u32,
context_lines: u32,
buffer: String,
dst: String,
}
impl<'a> OffsetUnifiedDiffBuilder<'a> {
fn new(
input: &'a InternedInput<&'a str>,
old_line_offset: u32,
new_line_offset: u32,
context_lines: u32,
) -> Self {
Self {
before_hunk_start: 0,
after_hunk_start: 0,
before_hunk_len: 0,
after_hunk_len: 0,
old_line_offset,
new_line_offset,
context_lines,
buffer: String::with_capacity(8),
dst: String::new(),
interner: &input.interner,
before: &input.before,
after: &input.after,
pos: 0,
}
}
fn print_tokens(&mut self, tokens: &[Token], prefix: char) {
for &token in tokens {
writeln!(&mut self.buffer, "{prefix}{}", self.interner[token]).unwrap();
}
}
fn flush(&mut self) {
if self.before_hunk_len == 0 && self.after_hunk_len == 0 {
return;
}
let end = (self.pos + self.context_lines).min(self.before.len() as u32);
self.update_pos(end, end);
writeln!(
&mut self.dst,
"@@ -{},{} +{},{} @@",
self.before_hunk_start + 1 + self.old_line_offset,
self.before_hunk_len,
self.after_hunk_start + 1 + self.new_line_offset,
self.after_hunk_len,
)
.unwrap();
write!(&mut self.dst, "{}", &self.buffer).unwrap();
self.buffer.clear();
self.before_hunk_len = 0;
self.after_hunk_len = 0;
}
fn update_pos(&mut self, print_to: u32, move_to: u32) {
self.print_tokens(&self.before[self.pos as usize..print_to as usize], ' ');
let len = print_to - self.pos;
self.pos = move_to;
self.before_hunk_len += len;
self.after_hunk_len += len;
}
}
impl Sink for OffsetUnifiedDiffBuilder<'_> {
type Out = String;
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
if before.start - self.pos > self.context_lines * 2 {
self.flush();
}
if self.before_hunk_len == 0 && self.after_hunk_len == 0 {
self.pos = before.start.saturating_sub(self.context_lines);
self.before_hunk_start = self.pos;
self.after_hunk_start = after.start.saturating_sub(self.context_lines);
}
self.update_pos(before.start, before.end);
self.before_hunk_len += before.end - before.start;
self.after_hunk_len += after.end - after.start;
self.print_tokens(
&self.before[before.start as usize..before.end as usize],
'-',
);
self.print_tokens(&self.after[after.start as usize..after.end as usize], '+');
}
fn finish(mut self) -> Self::Out {
self.flush();
self.dst
}
}
/// Computes a diff between two strings, returning a vector of old and new row
/// ranges.
pub fn line_diff(old_text: &str, new_text: &str) -> Vec<(Range<u32>, Range<u32>)> {
let mut edits = Vec::new();
let input = InternedInput::new(
lines_with_terminator(old_text),
lines_with_terminator(new_text),
);
diff_internal(&input, &mut |_, _, old_rows, new_rows| {
edits.push((old_rows, new_rows));
});
edits
}
/// Computes a diff between two strings, returning a vector of edits.
///
/// The edits are represented as tuples of byte ranges and replacement strings.
///
/// Internally, this function first performs a line-based diff, and then performs a second
/// word-based diff within hunks that replace small numbers of lines.
pub fn text_diff(old_text: &str, new_text: &str) -> Vec<(Range<usize>, Arc<str>)> {
text_diff_with_options(old_text, new_text, DiffOptions::default())
}
/// Computes word-level diff ranges between two strings.
///
/// Returns a tuple of (old_ranges, new_ranges) where each vector contains
/// the byte ranges of changed words in the respective text.
pub fn word_diff_ranges(
old_text: &str,
new_text: &str,
options: DiffOptions,
) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
let mut input: InternedInput<&str> = InternedInput::default();
input.update_before(tokenize(old_text, options.language_scope.clone()));
input.update_after(tokenize(new_text, options.language_scope));
let mut old_ranges: Vec<Range<usize>> = Vec::new();
let mut new_ranges: Vec<Range<usize>> = Vec::new();
diff_internal(&input, &mut |old_byte_range, new_byte_range, _, _| {
if !old_byte_range.is_empty() {
if let Some(last) = old_ranges.last_mut()
&& last.end >= old_byte_range.start
{
last.end = old_byte_range.end;
} else {
old_ranges.push(old_byte_range);
}
}
if !new_byte_range.is_empty() {
if let Some(last) = new_ranges.last_mut()
&& last.end >= new_byte_range.start
{
last.end = new_byte_range.end;
} else {
new_ranges.push(new_byte_range);
}
}
});
(old_ranges, new_ranges)
}
/// Computes character-level diff between two strings.
///
/// Usually, you should use `text_diff`, which performs a word-wise diff.
pub fn char_diff<'a>(old_text: &'a str, new_text: &'a str) -> Vec<(Range<usize>, &'a str)> {
let mut input: InternedInput<&str> = InternedInput::default();
input.update_before(tokenize_chars(old_text));
input.update_after(tokenize_chars(new_text));
let mut edits: Vec<(Range<usize>, &str)> = Vec::new();
diff_internal(&input, &mut |old_byte_range, new_byte_range, _, _| {
let replacement = if new_byte_range.is_empty() {
""
} else {
&new_text[new_byte_range]
};
edits.push((old_byte_range, replacement));
});
edits
}
pub struct DiffOptions {
pub language_scope: Option<LanguageScope>,
pub max_word_diff_len: usize,
pub max_word_diff_line_count: usize,
}
impl Default for DiffOptions {
fn default() -> Self {
Self {
language_scope: Default::default(),
max_word_diff_len: MAX_WORD_DIFF_LEN,
max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
}
}
}
/// Computes a diff between two strings, using a specific language scope's
/// word characters for word-level diffing.
pub fn text_diff_with_options(
old_text: &str,
new_text: &str,
options: DiffOptions,
) -> Vec<(Range<usize>, Arc<str>)> {
let empty: Arc<str> = Arc::default();
let mut edits = Vec::new();
let mut hunk_input = InternedInput::default();
let input = InternedInput::new(
lines_with_terminator(old_text),
lines_with_terminator(new_text),
);
diff_internal(&input, &mut |old_byte_range,
new_byte_range,
old_rows,
new_rows| {
if should_perform_word_diff_within_hunk(
&old_rows,
&old_byte_range,
&new_rows,
&new_byte_range,
&options,
) {
let old_offset = old_byte_range.start;
let new_offset = new_byte_range.start;
hunk_input.clear();
hunk_input.update_before(tokenize(
&old_text[old_byte_range],
options.language_scope.clone(),
));
hunk_input.update_after(tokenize(
&new_text[new_byte_range],
options.language_scope.clone(),
));
diff_internal(&hunk_input, &mut |old_byte_range, new_byte_range, _, _| {
let old_byte_range =
old_offset + old_byte_range.start..old_offset + old_byte_range.end;
let new_byte_range =
new_offset + new_byte_range.start..new_offset + new_byte_range.end;
let replacement_text = if new_byte_range.is_empty() {
empty.clone()
} else {
new_text[new_byte_range].into()
};
edits.push((old_byte_range, replacement_text));
});
} else {
let replacement_text = if new_byte_range.is_empty() {
empty.clone()
} else {
new_text[new_byte_range].into()
};
edits.push((old_byte_range, replacement_text));
}
});
edits
}
pub fn apply_diff_patch(base_text: &str, patch: &str) -> Result<String, anyhow::Error> {
let patch = diffy::Patch::from_str(patch).context("Failed to parse patch")?;
let result = diffy::apply(base_text, &patch);
result.map_err(|err| anyhow!(err))
}
pub fn apply_reversed_diff_patch(base_text: &str, patch: &str) -> Result<String, anyhow::Error> {
let patch = diffy::Patch::from_str(patch).context("Failed to parse patch")?;
let reversed = patch.reverse();
diffy::apply(base_text, &reversed).map_err(|err| anyhow!(err))
}
fn should_perform_word_diff_within_hunk(
old_row_range: &Range<u32>,
old_byte_range: &Range<usize>,
new_row_range: &Range<u32>,
new_byte_range: &Range<usize>,
options: &DiffOptions,
) -> bool {
!old_byte_range.is_empty()
&& !new_byte_range.is_empty()
&& old_byte_range.len() <= options.max_word_diff_len
&& new_byte_range.len() <= options.max_word_diff_len
&& old_row_range.len() <= options.max_word_diff_line_count
&& new_row_range.len() <= options.max_word_diff_line_count
}
fn diff_internal(
input: &InternedInput<&str>,
on_change: &mut dyn FnMut(Range<usize>, Range<usize>, Range<u32>, Range<u32>),
) {
let mut old_offset = 0;
let mut new_offset = 0;
let mut old_token_ix = 0;
let mut new_token_ix = 0;
diff(
Algorithm::Histogram,
input,
|old_tokens: Range<u32>, new_tokens: Range<u32>| {
old_offset += token_len(
input,
&input.before[old_token_ix as usize..old_tokens.start as usize],
);
new_offset += token_len(
input,
&input.after[new_token_ix as usize..new_tokens.start as usize],
);
let old_len = token_len(
input,
&input.before[old_tokens.start as usize..old_tokens.end as usize],
);
let new_len = token_len(
input,
&input.after[new_tokens.start as usize..new_tokens.end as usize],
);
let old_byte_range = old_offset..old_offset + old_len;
let new_byte_range = new_offset..new_offset + new_len;
old_token_ix = old_tokens.end;
new_token_ix = new_tokens.end;
old_offset = old_byte_range.end;
new_offset = new_byte_range.end;
on_change(old_byte_range, new_byte_range, old_tokens, new_tokens);
},
);
}
fn tokenize_chars(text: &str) -> impl Iterator<Item = &str> {
let mut chars = text.char_indices().peekable();
iter::from_fn(move || {
let (start, c) = chars.next()?;
Some(&text[start..start + c.len_utf8()])
})
}
fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator<Item = &str> {
let classifier =
CharClassifier::new(language_scope).scope_context(Some(CharScopeContext::Completion));
let mut chars = text.char_indices();
let mut prev = None;
let mut start_ix = 0;
iter::from_fn(move || {
for (ix, c) in chars.by_ref() {
let mut token = None;
let kind = classifier.kind(c);
if let Some((prev_char, prev_kind)) = prev
&& (kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char))
{
token = Some(&text[start_ix..ix]);
start_ix = ix;
}
prev = Some((c, kind));
if token.is_some() {
return token;
}
}
if start_ix < text.len() {
let token = &text[start_ix..];
start_ix = text.len();
return Some(token);
}
None
})
}
fn token_len(input: &InternedInput<&str>, tokens: &[Token]) -> usize {
tokens
.iter()
.map(|token| input.interner[*token].len())
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize() {
let text = "";
assert_eq!(tokenize(text, None).collect::<Vec<_>>(), Vec::<&str>::new());
let text = " ";
assert_eq!(tokenize(text, None).collect::<Vec<_>>(), vec![" "]);
let text = "one";
assert_eq!(tokenize(text, None).collect::<Vec<_>>(), vec!["one"]);
let text = "one\n";
assert_eq!(tokenize(text, None).collect::<Vec<_>>(), vec!["one", "\n"]);
let text = "one.two(three)";
assert_eq!(
tokenize(text, None).collect::<Vec<_>>(),
vec!["one", ".", "two", "(", "three", ")"]
);
let text = "one two three()";
assert_eq!(
tokenize(text, None).collect::<Vec<_>>(),
vec!["one", " ", "two", " ", "three", "(", ")"]
);
let text = " one\n two three";
assert_eq!(
tokenize(text, None).collect::<Vec<_>>(),
vec![" ", "one", "\n ", "two", " ", "three"]
);
}
#[test]
fn test_text_diff() {
let old_text = "one two three";
let new_text = "one TWO three";
assert_eq!(text_diff(old_text, new_text), [(4..7, "TWO".into()),]);
let old_text = "one\ntwo\nthree\n";
let new_text = "one\ntwo\nAND\nTHEN\nthree\n";
assert_eq!(
text_diff(old_text, new_text),
[(8..8, "AND\nTHEN\n".into()),]
);
let old_text = "one two\nthree four five\nsix seven eight nine\nten\n";
let new_text = "one two\nthree FOUR five\nsix SEVEN eight nine\nten\nELEVEN\n";
assert_eq!(
text_diff(old_text, new_text),
[
(14..18, "FOUR".into()),
(28..33, "SEVEN".into()),
(49..49, "ELEVEN\n".into())
]
);
}
#[test]
fn test_apply_diff_patch() {
let old_text = "one two\nthree four five\nsix seven eight nine\nten\n";
let new_text = "one two\nthree FOUR five\nsix SEVEN eight nine\nten\nELEVEN\n";
let patch = unified_diff(old_text, new_text);
assert_eq!(apply_diff_patch(old_text, &patch).unwrap(), new_text);
}
#[test]
fn test_apply_reversed_diff_patch() {
let old_text = "one two\nthree four five\nsix seven eight nine\nten\n";
let new_text = "one two\nthree FOUR five\nsix SEVEN eight nine\nten\nELEVEN\n";
let patch = unified_diff(old_text, new_text);
assert_eq!(
apply_reversed_diff_patch(new_text, &patch).unwrap(),
old_text
);
}
#[test]
fn test_char_diff() {
assert_eq!(char_diff("", ""), vec![]);
assert_eq!(char_diff("", "abc"), vec![(0..0, "abc")]);
assert_eq!(char_diff("abc", ""), vec![(0..3, "")]);
assert_eq!(char_diff("ac", "abc"), vec![(1..1, "b")]); // "b" inserted
assert_eq!(char_diff("abc", "ac"), vec![(1..2, "")]); // "b" deleted
assert_eq!(char_diff("abc", "adc"), vec![(1..2, "d")]); // "b" replaced with "d"
assert_eq!(char_diff("", "日本語"), vec![(3..3, "本語")]); // "本語" inserted
assert_eq!(char_diff("日本語", ""), vec![(3..9, "")]); // "本語" deleted
assert_eq!(char_diff("🎉", "🎉🎊🎈"), vec![(4..4, "🎊🎈")]); // "🎊🎈" inserted
assert_eq!(
char_diff("test日本", "test日本語です"),
vec![(10..10, "語です")]
);
}
#[test]
fn test_unified_diff_with_offsets() {
let old_text = "foo\nbar\nbaz\n";
let new_text = "foo\nBAR\nbaz\n";
let expected_diff_body = " foo\n-bar\n+BAR\n baz\n";
let diff_no_offset = unified_diff(old_text, new_text);
assert_eq!(
diff_no_offset,
format!("@@ -1,3 +1,3 @@\n{}", expected_diff_body)
);
let diff_with_offset = unified_diff_with_offsets(old_text, new_text, 9, 11);
assert_eq!(
diff_with_offset,
format!("@@ -10,3 +12,3 @@\n{}", expected_diff_body)
);
let diff_with_offset = unified_diff_with_offsets(old_text, new_text, 99, 104);
assert_eq!(
diff_with_offset,
format!("@@ -100,3 +105,3 @@\n{}", expected_diff_body)
);
}
#[test]
fn test_unified_diff_with_context() {
// Test that full context includes all lines from the start
let old_text = "line1\nline2\nline3\nline4\nline5\nCHANGE_ME\nline7\nline8\n";
let new_text = "line1\nline2\nline3\nline4\nline5\nCHANGED\nline7\nline8\n";
// With default 3 lines of context, the diff starts at line 3
let diff_default = unified_diff_with_offsets(old_text, new_text, 0, 0);
assert_eq!(
diff_default,
"@@ -3,6 +3,6 @@\n line3\n line4\n line5\n-CHANGE_ME\n+CHANGED\n line7\n line8\n"
);
// With full context (8 lines), the diff starts at line 1
let diff_full_context = unified_diff_with_context(old_text, new_text, 0, 0, 8);
assert_eq!(
diff_full_context,
"@@ -1,8 +1,8 @@\n line1\n line2\n line3\n line4\n line5\n-CHANGE_ME\n+CHANGED\n line7\n line8\n"
);
// With 0 context, only the changed line is shown
let diff_no_context = unified_diff_with_context(old_text, new_text, 0, 0, 0);
assert_eq!(diff_no_context, "@@ -6,1 +6,1 @@\n-CHANGE_ME\n+CHANGED\n");
}
}

View File

@@ -0,0 +1,84 @@
//! Provides support for language toolchains.
//!
//! A language can have associated toolchains,
//! which is a set of tools used to interact with the projects written in said language.
//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use collections::HashMap;
use futures::future::BoxFuture;
use gpui::{App, AsyncApp};
use settings::WorktreeId;
use task::ShellKind;
use util::rel_path::RelPath;
use crate::LanguageName;
// Re-export core data types from language_core.
pub use language_core::{Toolchain, ToolchainList, ToolchainMetadata, ToolchainScope};
#[async_trait]
pub trait ToolchainLister: Send + Sync + 'static {
/// List all available toolchains for a given path.
async fn list(
&self,
worktree_root: PathBuf,
subroot_relative_path: Arc<RelPath>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
/// Given a user-created toolchain, resolve lister-specific details.
/// Put another way: fill in the details of the toolchain so the user does not have to.
async fn resolve(
&self,
path: PathBuf,
project_env: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain>;
fn activation_script(
&self,
toolchain: &Toolchain,
shell: ShellKind,
cx: &App,
) -> BoxFuture<'static, Vec<String>>;
/// Returns various "static" bits of information about this toolchain lister. This function should be pure.
fn meta(&self) -> ToolchainMetadata;
}
#[async_trait(?Send)]
pub trait LanguageToolchainStore: Send + Sync + 'static {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
relative_path: Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain>;
}
pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
relative_path: &Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain>;
}
#[async_trait(?Send)]
impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
relative_path: Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain> {
self.active_toolchain(worktree_id, &relative_path, language_name, cx)
}
}