logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
co-authored by Claude Fable 5
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions
+112
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
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
+144
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
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
+119
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
+1
View File
@@ -0,0 +1 @@
pub use language_core::diagnostic::{Diagnostic, DiagnosticSourceKind};
+353
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;
}
}
+158
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
+28
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;
}
+763
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));
}
}
+327
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);
}
}
+649
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
+55
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,
}
+576
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");
}
}
+84
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)
}
}