logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
112
crates/language/Cargo.toml
Normal file
112
crates/language/Cargo.toml
Normal file
@@ -0,0 +1,112 @@
|
||||
[package]
|
||||
name = "language"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/language.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"rand",
|
||||
"collections/test-support",
|
||||
"lsp/test-support",
|
||||
"text/test-support",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-md",
|
||||
"settings/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
ec4rs.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
futures-lite.workspace = true
|
||||
fuzzy.workspace = true
|
||||
globset.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
imara-diff.workspace = true
|
||||
language_core.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
rpc.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
shellexpand.workspace = true
|
||||
smallvec.workspace = true
|
||||
streaming-iterator.workspace = true
|
||||
strsim.workspace = true
|
||||
sum_tree.workspace = true
|
||||
task.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tree-sitter-md = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter.workspace = true
|
||||
unicase = "2.6"
|
||||
util.workspace = true
|
||||
watch.workspace = true
|
||||
zlog.workspace = true
|
||||
ztracing.workspace = true
|
||||
diffy = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-elixir.workspace = true
|
||||
tree-sitter-embedded-template.workspace = true
|
||||
tree-sitter-heex.workspace = true
|
||||
tree-sitter-html.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
tree-sitter-python.workspace = true
|
||||
tree-sitter-ruby.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
tree-sitter-typescript.workspace = true
|
||||
toml.workspace = true
|
||||
unindent.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
criterion.workspace = true
|
||||
theme_settings.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "highlight_map"
|
||||
harness = false
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["tracing"]
|
||||
1
crates/language/LICENSE-GPL
Symbolic link
1
crates/language/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
144
crates/language/benches/highlight_map.rs
Normal file
144
crates/language/benches/highlight_map.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
|
||||
use gpui::rgba;
|
||||
use language::build_highlight_map;
|
||||
use theme::SyntaxTheme;
|
||||
|
||||
fn syntax_theme(highlight_names: &[&str]) -> SyntaxTheme {
|
||||
SyntaxTheme::new(highlight_names.iter().enumerate().map(|(i, name)| {
|
||||
let r = ((i * 37) % 256) as u8;
|
||||
let g = ((i * 53) % 256) as u8;
|
||||
let b = ((i * 71) % 256) as u8;
|
||||
let color = rgba(u32::from_be_bytes([r, g, b, 0xff]));
|
||||
(name.to_string(), color.into())
|
||||
}))
|
||||
}
|
||||
|
||||
static SMALL_THEME_KEYS: &[&str] = &[
|
||||
"comment", "function", "keyword", "string", "type", "variable",
|
||||
];
|
||||
|
||||
static LARGE_THEME_KEYS: &[&str] = &[
|
||||
"attribute",
|
||||
"boolean",
|
||||
"comment",
|
||||
"comment.doc",
|
||||
"constant",
|
||||
"constant.builtin",
|
||||
"constructor",
|
||||
"embedded",
|
||||
"emphasis",
|
||||
"emphasis.strong",
|
||||
"function",
|
||||
"function.builtin",
|
||||
"function.method",
|
||||
"function.method.builtin",
|
||||
"function.special.definition",
|
||||
"keyword",
|
||||
"keyword.control",
|
||||
"keyword.control.conditional",
|
||||
"keyword.control.import",
|
||||
"keyword.control.repeat",
|
||||
"keyword.control.return",
|
||||
"keyword.modifier",
|
||||
"keyword.operator",
|
||||
"label",
|
||||
"link_text",
|
||||
"link_uri",
|
||||
"number",
|
||||
"operator",
|
||||
"property",
|
||||
"punctuation",
|
||||
"punctuation.bracket",
|
||||
"punctuation.delimiter",
|
||||
"punctuation.list_marker",
|
||||
"punctuation.special",
|
||||
"string",
|
||||
"string.escape",
|
||||
"string.regex",
|
||||
"string.special",
|
||||
"string.special.symbol",
|
||||
"tag",
|
||||
"text.literal",
|
||||
"title",
|
||||
"type",
|
||||
"type.builtin",
|
||||
"type.super",
|
||||
"variable",
|
||||
"variable.builtin",
|
||||
"variable.member",
|
||||
"variable.parameter",
|
||||
"variable.special",
|
||||
];
|
||||
|
||||
static SMALL_CAPTURE_NAMES: &[&str] = &[
|
||||
"function",
|
||||
"keyword",
|
||||
"string.escape",
|
||||
"type.builtin",
|
||||
"variable.builtin",
|
||||
];
|
||||
|
||||
static LARGE_CAPTURE_NAMES: &[&str] = &[
|
||||
"attribute",
|
||||
"boolean",
|
||||
"comment",
|
||||
"comment.doc",
|
||||
"constant",
|
||||
"constant.builtin",
|
||||
"constructor",
|
||||
"function",
|
||||
"function.builtin",
|
||||
"function.method",
|
||||
"keyword",
|
||||
"keyword.control",
|
||||
"keyword.control.conditional",
|
||||
"keyword.control.import",
|
||||
"keyword.modifier",
|
||||
"keyword.operator",
|
||||
"label",
|
||||
"number",
|
||||
"operator",
|
||||
"property",
|
||||
"punctuation.bracket",
|
||||
"punctuation.delimiter",
|
||||
"punctuation.special",
|
||||
"string",
|
||||
"string.escape",
|
||||
"string.regex",
|
||||
"string.special",
|
||||
"tag",
|
||||
"type",
|
||||
"type.builtin",
|
||||
"variable",
|
||||
"variable.builtin",
|
||||
"variable.member",
|
||||
"variable.parameter",
|
||||
];
|
||||
|
||||
fn bench_build_highlight_map(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("build_highlight_map");
|
||||
|
||||
for (capture_label, capture_names) in [
|
||||
("small_captures", SMALL_CAPTURE_NAMES as &[&str]),
|
||||
("large_captures", LARGE_CAPTURE_NAMES as &[&str]),
|
||||
] {
|
||||
for (theme_label, theme_keys) in [
|
||||
("small_theme", SMALL_THEME_KEYS as &[&str]),
|
||||
("large_theme", LARGE_THEME_KEYS as &[&str]),
|
||||
] {
|
||||
let theme = syntax_theme(theme_keys);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(capture_label, theme_label),
|
||||
&(capture_names, &theme),
|
||||
|b, (capture_names, theme)| {
|
||||
b.iter(|| build_highlight_map(black_box(capture_names), black_box(theme)));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_build_highlight_map);
|
||||
criterion_main!(benches);
|
||||
5
crates/language/build.rs
Normal file
5
crates/language/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
if let Ok(bundled) = std::env::var("ZED_BUNDLE") {
|
||||
println!("cargo:rustc-env=ZED_BUNDLE={}", bundled);
|
||||
}
|
||||
}
|
||||
6107
crates/language/src/buffer.rs
Normal file
6107
crates/language/src/buffer.rs
Normal file
File diff suppressed because it is too large
Load Diff
119
crates/language/src/buffer/row_chunk.rs
Normal file
119
crates/language/src/buffer/row_chunk.rs
Normal 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
|
||||
}
|
||||
}
|
||||
4175
crates/language/src/buffer_tests.rs
Normal file
4175
crates/language/src/buffer_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
1
crates/language/src/diagnostic.rs
Normal file
1
crates/language/src/diagnostic.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub use language_core::diagnostic::{Diagnostic, DiagnosticSourceKind};
|
||||
353
crates/language/src/diagnostic_set.rs
Normal file
353
crates/language/src/diagnostic_set.rs
Normal 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
crates/language/src/file_content.rs
Normal file
158
crates/language/src/file_content.rs
Normal 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))
|
||||
}
|
||||
1991
crates/language/src/language.rs
Normal file
1991
crates/language/src/language.rs
Normal file
File diff suppressed because it is too large
Load Diff
1208
crates/language/src/language_registry.rs
Normal file
1208
crates/language/src/language_registry.rs
Normal file
File diff suppressed because it is too large
Load Diff
1153
crates/language/src/language_settings.rs
Normal file
1153
crates/language/src/language_settings.rs
Normal file
File diff suppressed because it is too large
Load Diff
28
crates/language/src/manifest.rs
Normal file
28
crates/language/src/manifest.rs
Normal 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
crates/language/src/modeline.rs
Normal file
763
crates/language/src/modeline.rs
Normal 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
crates/language/src/outline.rs
Normal file
327
crates/language/src/outline.rs
Normal 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
crates/language/src/proto.rs
Normal file
649
crates/language/src/proto.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
2220
crates/language/src/syntax_map.rs
Normal file
2220
crates/language/src/syntax_map.rs
Normal file
File diff suppressed because it is too large
Load Diff
1586
crates/language/src/syntax_map/syntax_map_tests.rs
Normal file
1586
crates/language/src/syntax_map/syntax_map_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
55
crates/language/src/task_context.rs
Normal file
55
crates/language/src/task_context.rs
Normal 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
crates/language/src/text_diff.rs
Normal file
576
crates/language/src/text_diff.rs
Normal 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
crates/language/src/toolchain.rs
Normal file
84
crates/language/src/toolchain.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user