From 1e96fc98e72108719de2355fd3a129a6e6bd105a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 19:09:29 +0100 Subject: [PATCH 01/61] Advertise `additionalTextEdits` resolution capability to language servers Co-Authored-By: Nathan Sobo --- crates/lsp/src/lsp.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d5a87f1aff..b2c658dbd6 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -232,6 +232,15 @@ impl LanguageServer { link_support: Some(true), ..Default::default() }), + completion: Some(CompletionClientCapabilities { + completion_item: Some(CompletionItemCapability { + resolve_support: Some(CompletionItemCapabilityResolveSupport { + properties: vec!["additionalTextEdits".to_string()], + }), + ..Default::default() + }), + ..Default::default() + }), ..Default::default() }), experimental: Some(json!({ From bd2527e691cb461213bc142987e3a6ea99644ea9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 19:11:13 +0100 Subject: [PATCH 02/61] Use StringMatchCandidate::new to construct candidates more conveniently Co-Authored-By: Nathan Sobo --- crates/fuzzy/src/fuzzy.rs | 10 ++++++++++ crates/language/src/buffer.rs | 12 +++--------- crates/language/src/language.rs | 10 ++++++++++ crates/language/src/outline.rs | 12 ++---------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index 92084b9dc7..dbbef435f6 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -98,6 +98,16 @@ impl<'a> MatchCandidate for PathMatchCandidate<'a> { } } +impl StringMatchCandidate { + pub fn new(id: usize, string: String) -> Self { + Self { + id, + char_bag: CharBag::from(string.as_str()), + string, + } + } +} + impl<'a> MatchCandidate for &'a StringMatchCandidate { fn has_chars(&self, bag: CharBag) -> bool { self.char_bag.is_superset(bag) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 05128776b6..014f48f462 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -7,7 +7,7 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, outline::OutlineItem, - range_from_lsp, Outline, + range_from_lsp, Outline, ToLspPosition, }; use anyhow::{anyhow, Result}; use clock::ReplicaId; @@ -589,14 +589,8 @@ impl Buffer { .collect(); lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range::new( - lsp::Position::new( - edit_start.row, - edit_start.column, - ), - lsp::Position::new( - edit_end.row, - edit_end.column, - ), + edit_start.to_lsp_position(), + edit_end.to_lsp_position(), )), range_length: None, text: new_text, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 91d774f2a4..22accb422c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -39,6 +39,10 @@ pub trait ToPointUtf16 { fn to_point_utf16(self) -> PointUtf16; } +pub trait ToLspPosition { + fn to_lsp_position(self) -> lsp::Position; +} + pub trait DiagnosticProcessor: 'static + Send + Sync { fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); } @@ -286,6 +290,12 @@ impl ToPointUtf16 for lsp::Position { } } +impl ToLspPosition for PointUtf16 { + fn to_lsp_position(self) -> lsp::Position { + lsp::Position::new(self.row, self.column) + } +} + pub fn range_from_lsp(range: lsp::Range) -> Range { let start = PointUtf16::new(range.start.line, range.start.character); let end = PointUtf16::new(range.end.line, range.end.character); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 07f0196c9f..0460d122b7 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -45,16 +45,8 @@ impl Outline { .map(|range| &item.text[range.start as usize..range.end as usize]) .collect::(); - path_candidates.push(StringMatchCandidate { - id, - char_bag: path_text.as_str().into(), - string: path_text.clone(), - }); - candidates.push(StringMatchCandidate { - id, - char_bag: candidate_text.as_str().into(), - string: candidate_text, - }); + path_candidates.push(StringMatchCandidate::new(id, path_text.clone())); + candidates.push(StringMatchCandidate::new(id, candidate_text)); } Self { From 03bcbdc33d02e3896e5f50c790d0eba39dbf3266 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 19:12:35 +0100 Subject: [PATCH 03/61] WIP --- crates/editor/src/editor.rs | 12 ++++ crates/editor/src/multi_buffer.rs | 13 +++++ crates/language/src/buffer.rs | 97 +++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index db0c099c4b..ed900bc9e2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -117,6 +117,7 @@ action!(Unfold); action!(FoldSelectedRanges); action!(Scroll, Vector2F); action!(Select, SelectPhase); +action!(ShowAutocomplete); pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { path_openers.push(Box::new(items::BufferOpener)); @@ -224,6 +225,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec) { + let position = self + .newest_selection::(&self.buffer.read(cx).read(cx)) + .head(); + self.buffer + .update(cx, |buffer, cx| buffer.completions(position, cx)) + .detach_and_log_err(cx); + } + pub fn clear(&mut self, cx: &mut ViewContext) { self.start_transaction(cx); self.select_all(&SelectAll, cx); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a8867f68d5..aa118c55cb 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -5,6 +5,7 @@ use anyhow::Result; use clock::ReplicaId; use collections::{HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +pub use language::Completion; use language::{ Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, TransactionId, @@ -847,6 +848,18 @@ impl MultiBuffer { }) } + pub fn completions( + &self, + position: T, + cx: &mut ModelContext, + ) -> Task>> + where + T: ToOffset, + { + let (buffer, text_anchor) = self.text_anchor_for_position(position, cx); + buffer.update(cx, |buffer, cx| buffer.completions(text_anchor, cx)) + } + pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { self.buffers .borrow() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 014f48f462..257d066b08 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -12,6 +12,7 @@ use crate::{ use anyhow::{anyhow, Result}; use clock::ReplicaId; use futures::FutureExt as _; +use fuzzy::StringMatchCandidate; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use lsp::LanguageServer; @@ -114,6 +115,12 @@ pub struct Diagnostic { pub is_disk_based: bool, } +pub struct Completion { + old_range: Range, + new_text: String, + lsp_completion: lsp::CompletionItem, +} + struct LanguageServerState { server: Arc, latest_snapshot: watch::Sender>, @@ -1611,6 +1618,96 @@ impl Buffer { false } } + + pub fn completions( + &self, + position: T, + cx: &mut ModelContext, + ) -> Task>> + where + T: ToOffset, + { + let file = if let Some(file) = self.file.as_ref() { + file + } else { + return Task::ready(Ok(Default::default())); + }; + + if let Some(file) = file.as_local() { + let server = if let Some(lang) = self.language_server.as_ref() { + lang.server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; + let abs_path = file.abs_path(cx); + let position = self.offset_to_point_utf16(position.to_offset(self)); + + cx.spawn(|this, mut cx| async move { + let t0 = Instant::now(); + let completions = server + .request::(lsp::CompletionParams { + text_document_position: lsp::TextDocumentPositionParams::new( + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(abs_path).unwrap(), + ), + position.to_lsp_position(), + ), + context: Default::default(), + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }) + .await?; + dbg!("completions", t0.elapsed()); + // fuzzy::match_strings(candidates, query, smart_case, max_results, cancel_flag, background) + + let mut completions = if let Some(completions) = completions { + match completions { + lsp::CompletionResponse::Array(completions) => completions, + lsp::CompletionResponse::List(list) => list.items, + } + } else { + Default::default() + }; + + this.update(&mut cx, |this, cx| { + this.edit([0..0], "use std::sync::Arc;\n", cx) + }); + + let mut futures = Vec::new(); + for completion in completions { + futures.push(server.request::(completion)); + } + + let completions = futures::future::try_join_all(futures).await?; + dbg!("resolution", t0.elapsed(), completions); + // let candidates = completions + // .iter() + // .enumerate() + // .map(|(id, completion)| { + // let text = completion + // .filter_text + // .clone() + // .unwrap_or_else(|| completion.label.clone()); + // StringMatchCandidate::new(id, text) + // }) + // .collect::>(); + // let matches = fuzzy::match_strings( + // &candidates, + // "Arc", + // false, + // 100, + // &Default::default(), + // cx.background(), + // ) + // .await; + // dbg!(matches); + + Ok(Default::default()) + }) + } else { + Task::ready(Ok(Default::default())) + } + } } #[cfg(any(test, feature = "test-support"))] From 0344c543afc30134333b2c1b244c1af214c0f57b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 11:25:00 -0700 Subject: [PATCH 04/61] Return anchored completions from Buffer::completions Co-Authored-By: Antonio Scandurra --- crates/language/src/buffer.rs | 59 ++++++++++++----------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 257d066b08..9dc7c112be 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -12,7 +12,6 @@ use crate::{ use anyhow::{anyhow, Result}; use clock::ReplicaId; use futures::FutureExt as _; -use fuzzy::StringMatchCandidate; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use lsp::LanguageServer; @@ -1642,8 +1641,7 @@ impl Buffer { let abs_path = file.abs_path(cx); let position = self.offset_to_point_utf16(position.to_offset(self)); - cx.spawn(|this, mut cx| async move { - let t0 = Instant::now(); + cx.spawn(|this, cx| async move { let completions = server .request::(lsp::CompletionParams { text_document_position: lsp::TextDocumentPositionParams::new( @@ -1657,10 +1655,8 @@ impl Buffer { partial_result_params: Default::default(), }) .await?; - dbg!("completions", t0.elapsed()); - // fuzzy::match_strings(candidates, query, smart_case, max_results, cancel_flag, background) - let mut completions = if let Some(completions) = completions { + let completions = if let Some(completions) = completions { match completions { lsp::CompletionResponse::Array(completions) => completions, lsp::CompletionResponse::List(list) => list.items, @@ -1669,40 +1665,25 @@ impl Buffer { Default::default() }; - this.update(&mut cx, |this, cx| { - this.edit([0..0], "use std::sync::Arc;\n", cx) - }); + this.read_with(&cx, |this, _| { + Ok(completions.into_iter().filter_map(|lsp_completion| { + let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? { + lsp::CompletionTextEdit::Edit(edit) => (range_from_lsp(edit.range), edit.new_text.clone()), + lsp::CompletionTextEdit::InsertAndReplace(_) => { + log::info!("received an insert and replace completion but we don't yet support that"); + return None + }, + }; - let mut futures = Vec::new(); - for completion in completions { - futures.push(server.request::(completion)); - } - - let completions = futures::future::try_join_all(futures).await?; - dbg!("resolution", t0.elapsed(), completions); - // let candidates = completions - // .iter() - // .enumerate() - // .map(|(id, completion)| { - // let text = completion - // .filter_text - // .clone() - // .unwrap_or_else(|| completion.label.clone()); - // StringMatchCandidate::new(id, text) - // }) - // .collect::>(); - // let matches = fuzzy::match_strings( - // &candidates, - // "Arc", - // false, - // 100, - // &Default::default(), - // cx.background(), - // ) - // .await; - // dbg!(matches); - - Ok(Default::default()) + let old_range = this.anchor_after(old_range.start)..this.anchor_before(old_range.end); + + Some(Completion { + old_range, + new_text, + lsp_completion, + }) + }).collect()) + }) }) } else { Task::ready(Ok(Default::default())) From 960696a504f3a723288e72a921fc7819d2ffe119 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 11:34:19 -0700 Subject: [PATCH 05/61] Bind autocomplete to ctrl-space Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 2 +- crates/gpui/src/platform/mac/event.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ed900bc9e2..5a14ab4642 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -225,7 +225,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec "space", BACKSPACE_KEY => "backspace", ENTER_KEY => "enter", ESCAPE_KEY => "escape", From ab6eb0a65558ec86dae62a59eccb2b163d72715c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 12:19:17 -0700 Subject: [PATCH 06/61] Start on completion rendering Co-Authored-By: Antonio Scandurra Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 54 +++++++++++++++++++++++++++++++---- crates/editor/src/element.rs | 5 ++++ crates/language/src/buffer.rs | 6 ++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5a14ab4642..3bfddcc48d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -26,8 +26,8 @@ use gpui::{ use items::BufferItemHandle; use itertools::Itertools as _; use language::{ - AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, - Selection, SelectionGoal, TransactionId, + AnchorRangeExt as _, BracketPair, Buffer, Completion, Diagnostic, DiagnosticSeverity, Language, + Point, Selection, SelectionGoal, TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ @@ -390,6 +390,7 @@ pub struct Editor { highlighted_rows: Option>, highlighted_ranges: BTreeMap>)>, nav_history: Option, + completion_state: Option, } pub struct EditorSnapshot { @@ -423,6 +424,11 @@ struct BracketPairState { pair: BracketPair, } +struct CompletionState { + completions: Arc<[Completion]>, + list: UniformListState, +} + #[derive(Debug)] struct ActiveDiagnosticGroup { primary_range: Range, @@ -539,6 +545,7 @@ impl Editor { highlighted_rows: None, highlighted_ranges: Default::default(), nav_history: None, + completion_state: None, }; let selection = Selection { id: post_inc(&mut this.next_selection_id), @@ -1502,9 +1509,46 @@ impl Editor { let position = self .newest_selection::(&self.buffer.read(cx).read(cx)) .head(); - self.buffer - .update(cx, |buffer, cx| buffer.completions(position, cx)) - .detach_and_log_err(cx); + + let completions = self + .buffer + .update(cx, |buffer, cx| buffer.completions(position, cx)); + + cx.spawn_weak(|this, mut cx| async move { + let completions = completions.await?; + if let Some(this) = cx.read(|cx| this.upgrade(cx)) { + this.update(&mut cx, |this, cx| { + this.completion_state = Some(CompletionState { + completions: completions.into(), + list: Default::default(), + }); + cx.notify(); + }); + } + + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn render_completions(&self) -> Option { + self.completion_state.as_ref().map(|state| { + let build_settings = self.build_settings.clone(); + let completions = state.completions.clone(); + UniformList::new( + state.list.clone(), + state.completions.len(), + move |range, items, cx| { + let settings = build_settings(cx); + for completion in &completions[range] { + items.push( + Label::new(completion.label().to_string(), settings.style.text.clone()).boxed(), + ); + } + }, + ) + .boxed() + }) } pub fn clear(&mut self, cx: &mut ViewContext) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8ef6656897..5bba59191b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -836,6 +836,7 @@ impl Element for EditorElement { max_row.saturating_sub(1) as f32, ); + let mut completions = None; self.update_view(cx.app, |view, cx| { let clamped = view.clamp_scroll_left(scroll_max.x()); let autoscrolled; @@ -855,6 +856,8 @@ impl Element for EditorElement { if clamped || autoscrolled { snapshot = view.snapshot(cx); } + + completions = view.render_completions(); }); let blocks = self.layout_blocks( @@ -891,6 +894,7 @@ impl Element for EditorElement { em_width, em_advance, selections, + completions, }), ) } @@ -1000,6 +1004,7 @@ pub struct LayoutState { highlighted_ranges: Vec<(Range, Color)>, selections: HashMap>>, text_offset: Vector2F, + completions: Option, } fn layout_line( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9dc7c112be..290cb4f1b6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2424,6 +2424,12 @@ impl Default for Diagnostic { } } +impl Completion { + pub fn label(&self) -> &str { + &self.lsp_completion.label + } +} + pub fn contiguous_ranges( values: impl Iterator, max_len: usize, From 1a6e972ed4ba1ba733e9214683e550f40949beb9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 13:17:42 -0700 Subject: [PATCH 07/61] Get basic list of completions rendering without styling Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 11 ++++++--- crates/editor/src/element.rs | 48 +++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3bfddcc48d..0f95ef07ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -289,7 +289,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec) { + fn show_completions(&mut self, _: &ShowAutocomplete, cx: &mut ViewContext) { let position = self .newest_selection::(&self.buffer.read(cx).read(cx)) .head(); @@ -1531,6 +1531,10 @@ impl Editor { .detach_and_log_err(cx); } + pub fn has_completions(&self) -> bool { + self.completion_state.is_some() + } + pub fn render_completions(&self) -> Option { self.completion_state.as_ref().map(|state| { let build_settings = self.build_settings.clone(); @@ -1542,7 +1546,8 @@ impl Editor { let settings = build_settings(cx); for completion in &completions[range] { items.push( - Label::new(completion.label().to_string(), settings.style.text.clone()).boxed(), + Label::new(completion.label().to_string(), settings.style.text.clone()) + .boxed(), ); } }, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5bba59191b..2e01a2758e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -300,7 +300,7 @@ impl EditorElement { &mut self, bounds: RectF, visible_bounds: RectF, - layout: &LayoutState, + layout: &mut LayoutState, cx: &mut PaintContext, ) { let view = self.view(cx.app); @@ -392,6 +392,28 @@ impl EditorElement { } cx.scene.pop_layer(); + if let Some((position, completions_list)) = layout.completions.as_mut() { + cx.scene.push_stacking_context(None); + + let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; + let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; + let y = (position.row() + 1) as f32 * layout.line_height - scroll_top; + let mut list_origin = content_origin + vec2f(x, y); + let list_height = completions_list.size().y(); + + if list_origin.y() + list_height > bounds.lower_left().y() { + list_origin.set_y(list_origin.y() - layout.line_height - list_height); + } + + completions_list.paint( + list_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + cx, + ); + + cx.scene.pop_stacking_context(); + } + cx.scene.pop_layer(); } @@ -857,9 +879,29 @@ impl Element for EditorElement { snapshot = view.snapshot(cx); } - completions = view.render_completions(); + if view.has_completions() { + let newest_selection_head = view + .newest_selection::(&snapshot.buffer_snapshot) + .head() + .to_display_point(&snapshot); + + if (start_row..end_row).contains(&newest_selection_head.row()) { + let list = view.render_completions().unwrap(); + completions = Some((newest_selection_head, list)); + } + } }); + if let Some((_, completions_list)) = completions.as_mut() { + completions_list.layout( + SizeConstraint { + min: Vector2F::zero(), + max: vec2f(800., (12. * line_height).min((size.y() - line_height) / 2.)), + }, + cx, + ); + } + let blocks = self.layout_blocks( start_row..end_row, &snapshot, @@ -1004,7 +1046,7 @@ pub struct LayoutState { highlighted_ranges: Vec<(Range, Color)>, selections: HashMap>>, text_offset: Vector2F, - completions: Option, + completions: Option<(DisplayPoint, ElementBox)>, } fn layout_line( From c19d639e0a107faf529df8f85c517a663641f9fb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jan 2022 13:01:20 -0800 Subject: [PATCH 08/61] Derive autocomplete menu's width from the width of its largest item Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 16 +++- crates/editor/src/element.rs | 2 +- crates/gpui/src/elements/uniform_list.rs | 103 +++++++++++++++-------- crates/theme/src/theme.rs | 9 ++ crates/zed/assets/themes/_base.toml | 7 +- 5 files changed, 98 insertions(+), 39 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0f95ef07ac..c36eccb682 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1535,9 +1535,10 @@ impl Editor { self.completion_state.is_some() } - pub fn render_completions(&self) -> Option { + pub fn render_completions(&self, cx: &AppContext) -> Option { self.completion_state.as_ref().map(|state| { let build_settings = self.build_settings.clone(); + let settings = build_settings(cx); let completions = state.completions.clone(); UniformList::new( state.list.clone(), @@ -1547,11 +1548,23 @@ impl Editor { for completion in &completions[range] { items.push( Label::new(completion.label().to_string(), settings.style.text.clone()) + .contained() + .with_style(settings.style.autocomplete.item) .boxed(), ); } }, ) + .with_width_from_item( + state + .completions + .iter() + .enumerate() + .max_by_key(|(_, completion)| completion.label().chars().count()) + .map(|(ix, _)| ix), + ) + .contained() + .with_style(settings.style.autocomplete.container) .boxed() }) } @@ -4056,6 +4069,7 @@ impl EditorSettings { invalid_information_diagnostic: default_diagnostic_style.clone(), hint_diagnostic: default_diagnostic_style.clone(), invalid_hint_diagnostic: default_diagnostic_style.clone(), + autocomplete: Default::default(), } }, } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2e01a2758e..512c6325f9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -886,7 +886,7 @@ impl Element for EditorElement { .to_display_point(&snapshot); if (start_row..end_row).contains(&newest_selection_head.row()) { - let list = view.render_completions().unwrap(); + let list = view.render_completions(cx).unwrap(); completions = Some((newest_selection_head, list)); } } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 945340e4c0..9248a8d146 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -51,6 +51,7 @@ where append_items: F, padding_top: f32, padding_bottom: f32, + get_width_from_item: Option, } impl UniformList @@ -64,9 +65,15 @@ where append_items, padding_top: 0., padding_bottom: 0., + get_width_from_item: None, } } + pub fn with_width_from_item(mut self, item_ix: Option) -> Self { + self.get_width_from_item = item_ix; + self + } + pub fn with_padding_top(mut self, padding: f32) -> Self { self.padding_top = padding; self @@ -155,46 +162,70 @@ where "UniformList does not support being rendered with an unconstrained height" ); } - let mut size = constraint.max; - let mut item_constraint = - SizeConstraint::new(vec2f(size.x(), 0.0), vec2f(size.x(), f32::INFINITY)); - let mut item_height = 0.; - let mut scroll_max = 0.; - let mut items = Vec::new(); - (self.append_items)(0..1, &mut items, cx); - if let Some(first_item) = items.first_mut() { - let mut item_size = first_item.layout(item_constraint, cx); - item_size.set_x(size.x()); - item_constraint.min = item_size; - item_constraint.max = item_size; - item_height = item_size.y(); - let scroll_height = self.item_count as f32 * item_height; - if scroll_height < size.y() { - size.set_y(size.y().min(scroll_height).max(constraint.min.y())); - } - - let scroll_height = - item_height * self.item_count as f32 + self.padding_top + self.padding_bottom; - scroll_max = (scroll_height - size.y()).max(0.); - self.autoscroll(scroll_max, size.y(), item_height); - - items.clear(); - let start = cmp::min( - ((self.scroll_top() - self.padding_top) / item_height) as usize, - self.item_count, + if self.item_count == 0 { + return ( + constraint.min, + LayoutState { + item_height: 0., + scroll_max: 0., + items, + }, ); - let end = cmp::min( - self.item_count, - start + (size.y() / item_height).ceil() as usize + 1, - ); - (self.append_items)(start..end, &mut items, cx); - for item in &mut items { - item.layout(item_constraint, cx); - } + } + + let mut size = constraint.max; + let mut item_size; + if let Some(sample_item_ix) = self.get_width_from_item { + (self.append_items)(sample_item_ix..sample_item_ix + 1, &mut items, cx); + let sample_item = items.get_mut(0).unwrap(); + item_size = sample_item.layout(constraint, cx); + size.set_x(item_size.x()); } else { - size = constraint.min; + (self.append_items)(0..1, &mut items, cx); + let first_item = items.first_mut().unwrap(); + item_size = first_item.layout( + SizeConstraint::new( + vec2f(constraint.max.x(), 0.0), + vec2f(constraint.max.x(), f32::INFINITY), + ), + cx, + ); + item_size.set_x(size.x()); + } + + let item_constraint = SizeConstraint { + min: item_size, + max: vec2f(constraint.max.x(), item_size.y()), + }; + let item_height = item_size.y(); + + let scroll_height = self.item_count as f32 * item_height; + if scroll_height < size.y() { + size.set_y(size.y().min(scroll_height).max(constraint.min.y())); + } + + let scroll_height = + item_height * self.item_count as f32 + self.padding_top + self.padding_bottom; + let scroll_max = (scroll_height - size.y()).max(0.); + self.autoscroll(scroll_max, size.y(), item_height); + + let start = cmp::min( + ((self.scroll_top() - self.padding_top) / item_height) as usize, + self.item_count, + ); + let end = cmp::min( + self.item_count, + start + (size.y() / item_height).ceil() as usize + 1, + ); + items.clear(); + (self.append_items)(start..end, &mut items, cx); + for item in &mut items { + let item_size = item.layout(item_constraint, cx); + if item_size.x() > size.x() { + size.set_x(item_size.x()); + } } ( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index fec42853dc..1c669c57f4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -292,6 +292,7 @@ pub struct EditorStyle { pub invalid_information_diagnostic: DiagnosticStyle, pub hint_diagnostic: DiagnosticStyle, pub invalid_hint_diagnostic: DiagnosticStyle, + pub autocomplete: AutocompleteStyle, } #[derive(Clone, Deserialize, Default)] @@ -321,6 +322,13 @@ pub struct DiagnosticStyle { pub text_scale_factor: f32, } +#[derive(Clone, Deserialize, Default)] +pub struct AutocompleteStyle { + #[serde(flatten)] + pub container: ContainerStyle, + pub item: ContainerStyle, +} + #[derive(Clone, Copy, Default, Deserialize)] pub struct SelectionStyle { pub cursor: Color, @@ -408,6 +416,7 @@ impl InputEditorStyle { invalid_information_diagnostic: default_diagnostic_style.clone(), hint_diagnostic: default_diagnostic_style.clone(), invalid_hint_diagnostic: default_diagnostic_style.clone(), + autocomplete: Default::default(), } } } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 93019db6e4..4281812d0e 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -188,7 +188,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -314,6 +314,11 @@ extends = "$editor.hint_diagnostic" message.text.color = "$text.3.color" message.highlight_text.color = "$text.3.color" +[editor.autocomplete] +background = "$surface.2" +border = { width = 1, color = "$border.1" } +item.padding = 2 + [project_diagnostics] background = "$surface.1" empty_message = { extends = "$text.0", size = 18 } From 6da01eac9b50a8987edbea070d498d86adedf436 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jan 2022 13:06:59 -0800 Subject: [PATCH 09/61] Make editor element's paint and layout states non-optional Co-Authored-By: Nathan Sobo --- crates/editor/src/element.rs | 57 ++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 512c6325f9..5acd46b0f8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -689,8 +689,8 @@ impl EditorElement { } impl Element for EditorElement { - type LayoutState = Option; - type PaintState = Option; + type LayoutState = LayoutState; + type PaintState = PaintState; fn layout( &mut self, @@ -918,7 +918,7 @@ impl Element for EditorElement { ( size, - Some(LayoutState { + LayoutState { size, scroll_max, gutter_size, @@ -937,7 +937,7 @@ impl Element for EditorElement { em_advance, selections, completions, - }), + }, ) } @@ -948,7 +948,6 @@ impl Element for EditorElement { layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - let layout = layout.as_mut()?; cx.scene.push_layer(Some(bounds)); let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size); @@ -971,11 +970,11 @@ impl Element for EditorElement { cx.scene.pop_layer(); - Some(PaintState { + PaintState { bounds, gutter_bounds, text_bounds, - }) + } } fn dispatch_event( @@ -986,31 +985,27 @@ impl Element for EditorElement { paint: &mut Self::PaintState, cx: &mut EventContext, ) -> bool { - if let (Some(layout), Some(paint)) = (layout, paint) { - match event { - Event::LeftMouseDown { - position, - alt, - shift, - click_count, - .. - } => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx), - Event::LeftMouseUp { position } => self.mouse_up(*position, cx), - Event::LeftMouseDragged { position } => { - self.mouse_dragged(*position, layout, paint, cx) - } - Event::ScrollWheel { - position, - delta, - precise, - } => self.scroll(*position, *delta, *precise, layout, paint, cx), - Event::KeyDown { - chars, keystroke, .. - } => self.key_down(chars, keystroke, cx), - _ => false, + match event { + Event::LeftMouseDown { + position, + alt, + shift, + click_count, + .. + } => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx), + Event::LeftMouseUp { position } => self.mouse_up(*position, cx), + Event::LeftMouseDragged { position } => { + self.mouse_dragged(*position, layout, paint, cx) } - } else { - false + Event::ScrollWheel { + position, + delta, + precise, + } => self.scroll(*position, *delta, *precise, layout, paint, cx), + Event::KeyDown { + chars, keystroke, .. + } => self.key_down(chars, keystroke, cx), + _ => false, } } From 16c0baced6f78cfce3709c2f1628cb0d8eafd8b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jan 2022 13:15:30 -0800 Subject: [PATCH 10/61] Dispatch editor events on the autocomplete list Co-Authored-By: Nathan Sobo --- crates/editor/src/element.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5acd46b0f8..5fc294bac4 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -981,10 +981,16 @@ impl Element for EditorElement { &mut self, event: &Event, _: RectF, - layout: &mut Self::LayoutState, - paint: &mut Self::PaintState, + layout: &mut LayoutState, + paint: &mut PaintState, cx: &mut EventContext, ) -> bool { + if let Some((_, completion_list)) = &mut layout.completions { + if completion_list.dispatch_event(event, cx) { + return true; + } + } + match event { Event::LeftMouseDown { position, From 071a55a7abdb1d38e961d02780645537ce9a9ca2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jan 2022 13:25:46 -0800 Subject: [PATCH 11/61] Allow selecting items from the autocomplete list Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 35 +++++++++++++++++++++++++++-- crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 4 ++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c36eccb682..813f087f54 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -426,6 +426,7 @@ struct BracketPairState { struct CompletionState { completions: Arc<[Completion]>, + selected_item: usize, list: UniformListState, } @@ -1520,6 +1521,7 @@ impl Editor { this.update(&mut cx, |this, cx| { this.completion_state = Some(CompletionState { completions: completions.into(), + selected_item: 0, list: Default::default(), }); cx.notify(); @@ -1540,16 +1542,23 @@ impl Editor { let build_settings = self.build_settings.clone(); let settings = build_settings(cx); let completions = state.completions.clone(); + let selected_item = state.selected_item; UniformList::new( state.list.clone(), state.completions.len(), move |range, items, cx| { let settings = build_settings(cx); - for completion in &completions[range] { + let start_ix = range.start; + for (ix, completion) in completions[range].iter().enumerate() { + let item_style = if start_ix + ix == selected_item { + settings.style.autocomplete.selected_item + } else { + settings.style.autocomplete.item + }; items.push( Label::new(completion.label().to_string(), settings.style.text.clone()) .contained() - .with_style(settings.style.autocomplete.item) + .with_style(item_style) .boxed(), ); } @@ -2256,6 +2265,17 @@ impl Editor { } pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if let Some(completion_state) = &mut self.completion_state { + if completion_state.selected_item > 0 { + completion_state.selected_item -= 1; + completion_state + .list + .scroll_to(ScrollTarget::Show(completion_state.selected_item)); + } + cx.notify(); + return; + } + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); return; @@ -2294,6 +2314,17 @@ impl Editor { } pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + if let Some(completion_state) = &mut self.completion_state { + if completion_state.selected_item + 1 < completion_state.completions.len() { + completion_state.selected_item += 1; + completion_state + .list + .scroll_to(ScrollTarget::Show(completion_state.selected_item)); + } + cx.notify(); + return; + } + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); return; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1c669c57f4..aca5e7d245 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -327,6 +327,7 @@ pub struct AutocompleteStyle { #[serde(flatten)] pub container: ContainerStyle, pub item: ContainerStyle, + pub selected_item: ContainerStyle, } #[derive(Clone, Copy, Default, Deserialize)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 4281812d0e..6a0c98c67f 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -319,6 +319,10 @@ background = "$surface.2" border = { width = 1, color = "$border.1" } item.padding = 2 +[editor.autocomplete.selected_item] +extends = "$editor.autocomplete.item" +background = "$state.hover" + [project_diagnostics] background = "$surface.1" empty_message = { extends = "$text.0", size = 18 } From 8d2b7ba032b78cecf78349cf7e2664d2b6b4eb29 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jan 2022 13:46:50 -0800 Subject: [PATCH 12/61] Insert completion text on enter Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 36 +++++++++++++++++++++++++++---- crates/editor/src/multi_buffer.rs | 28 +++++++++++++++++++++--- crates/language/src/buffer.rs | 12 +++++------ crates/server/src/main.rs | 2 +- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 813f087f54..89e99354d6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -117,7 +117,8 @@ action!(Unfold); action!(FoldSelectedRanges); action!(Scroll, Vector2F); action!(Select, SelectPhase); -action!(ShowAutocomplete); +action!(ShowCompletions); +action!(ConfirmCompletion); pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { path_openers.push(Box::new(items::BufferOpener)); @@ -133,6 +134,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec, + completions: Arc<[Completion]>, selected_item: usize, list: UniformListState, } @@ -1102,6 +1105,11 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.completion_state.take().is_some() { + cx.notify(); + return; + } + if self.mode != EditorMode::Full { cx.propagate_action(); return; @@ -1506,7 +1514,7 @@ impl Editor { } } - fn show_completions(&mut self, _: &ShowAutocomplete, cx: &mut ViewContext) { + fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { let position = self .newest_selection::(&self.buffer.read(cx).read(cx)) .head(); @@ -1533,6 +1541,23 @@ impl Editor { .detach_and_log_err(cx); } + fn confirm_completion(&mut self, _: &ConfirmCompletion, cx: &mut ViewContext) { + if let Some(completion_state) = self.completion_state.take() { + if let Some(completion) = completion_state + .completions + .get(completion_state.selected_item) + { + self.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent( + [completion.old_range.clone()], + completion.new_text.clone(), + cx, + ); + }) + } + } + } + pub fn has_completions(&self) -> bool { self.completion_state.is_some() } @@ -4180,6 +4205,9 @@ impl View for Editor { EditorMode::Full => "full", }; cx.map.insert("mode".into(), mode.into()); + if self.completion_state.is_some() { + cx.set.insert("completing".into()); + } cx } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index aa118c55cb..a5c2a64bea 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -852,12 +852,34 @@ impl MultiBuffer { &self, position: T, cx: &mut ModelContext, - ) -> Task>> + ) -> Task>>> where T: ToOffset, { - let (buffer, text_anchor) = self.text_anchor_for_position(position, cx); - buffer.update(cx, |buffer, cx| buffer.completions(text_anchor, cx)) + let snapshot = self.snapshot(cx); + let anchor = snapshot.anchor_before(position); + let buffer = self.buffers.borrow()[&anchor.buffer_id].buffer.clone(); + let completions = + buffer.update(cx, |buffer, cx| buffer.completions(anchor.text_anchor, cx)); + cx.foreground().spawn(async move { + completions.await.map(|completions| { + completions + .into_iter() + .map(|completion| Completion { + old_range: snapshot.anchor_in_excerpt( + anchor.excerpt_id.clone(), + completion.old_range.start, + ) + ..snapshot.anchor_in_excerpt( + anchor.excerpt_id.clone(), + completion.old_range.end, + ), + new_text: completion.new_text, + lsp_completion: completion.lsp_completion, + }) + .collect() + }) + }) } pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 290cb4f1b6..a5fc6a8b84 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -114,10 +114,10 @@ pub struct Diagnostic { pub is_disk_based: bool, } -pub struct Completion { - old_range: Range, - new_text: String, - lsp_completion: lsp::CompletionItem, +pub struct Completion { + pub old_range: Range, + pub new_text: String, + pub lsp_completion: lsp::CompletionItem, } struct LanguageServerState { @@ -1622,7 +1622,7 @@ impl Buffer { &self, position: T, cx: &mut ModelContext, - ) -> Task>> + ) -> Task>>> where T: ToOffset, { @@ -2424,7 +2424,7 @@ impl Default for Diagnostic { } } -impl Completion { +impl Completion { pub fn label(&self) -> &str { &self.lsp_completion.label } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 4b5780af68..3301fb24a9 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -2,6 +2,7 @@ mod admin; mod api; mod assets; mod auth; +mod careers; mod community; mod db; mod env; @@ -12,7 +13,6 @@ mod home; mod releases; mod rpc; mod team; -mod careers; use self::errors::TideResultExt as _; use ::rpc::Peer; From 1d1f8df180e1fbb77e122cad442c33de034597d0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jan 2022 16:50:51 -0800 Subject: [PATCH 13/61] Trigger completion when typing words or trigger characters Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 108 +++++++++++++++++++++++++++++- crates/editor/src/multi_buffer.rs | 39 +++++++++++ crates/language/src/buffer.rs | 83 +++++++++++++++++++++++ crates/lsp/src/lsp.rs | 27 +++++++- 6 files changed, 254 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0c2c1a2c7..d7c811bc53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1552,6 +1552,7 @@ dependencies = [ "language", "lazy_static", "log", + "lsp", "parking_lot", "postage", "project", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index b953d160ad..6450db7d39 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -41,6 +41,7 @@ smol = "1.2" [dev-dependencies] text = { path = "../text", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 89e99354d6..4e383d5518 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1253,6 +1253,7 @@ impl Editor { self.insert(text, cx); self.autoclose_pairs(cx); self.end_transaction(cx); + self.trigger_completion_on_input(text, cx); } } @@ -1388,6 +1389,20 @@ impl Editor { self.end_transaction(cx); } + fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { + if self.completion_state.is_none() { + if let Some(selection) = self.newest_anchor_selection() { + if self + .buffer + .read(cx) + .is_completion_trigger(selection.head(), text, cx) + { + self.show_completions(&ShowCompletions, cx); + } + } + } + } + fn autoclose_pairs(&mut self, cx: &mut ViewContext) { let selections = self.local_selections::(cx); let mut bracket_pair_state = None; @@ -4398,8 +4413,8 @@ pub fn char_kind(c: char) -> CharKind { #[cfg(test)] mod tests { use super::*; - use language::LanguageConfig; - use std::{cell::RefCell, rc::Rc, time::Instant}; + use language::{FakeFile, LanguageConfig}; + use std::{cell::RefCell, path::Path, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; use util::test::sample_text; @@ -6456,6 +6471,95 @@ mod tests { }); } + #[gpui::test] + async fn test_completion(mut cx: gpui::TestAppContext) { + let settings = cx.read(EditorSettings::test); + let (language_server, mut fake) = lsp::LanguageServer::fake_with_capabilities( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx.background(), + ) + .await; + + let text = " + one + two + three + " + .unindent(); + let buffer = cx.add_model(|cx| { + Buffer::from_file( + 0, + text, + Box::new(FakeFile { + path: Arc::from(Path::new("/the/file")), + }), + cx, + ) + .with_language_server(language_server, cx) + }); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + editor.update(&mut cx, |editor, cx| { + editor.select_ranges([3..3], None, cx); + editor.handle_input(&Input(".".to_string()), cx); + }); + + let (id, params) = fake.receive_request::().await; + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/the/file").unwrap() + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 4) + ); + + fake.respond( + id, + Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), + new_text: "first_completion".to_string(), + })), + ..Default::default() + }, + lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), + new_text: "second_completion".to_string(), + })), + ..Default::default() + }, + ])), + ) + .await; + + editor.next_notification(&cx).await; + + editor.update(&mut cx, |editor, cx| { + editor.move_down(&MoveDown, cx); + editor.confirm_completion(&ConfirmCompletion, cx); + assert_eq!( + editor.text(cx), + " + one.second_completion + two + three + " + .unindent() + ); + }); + } + #[gpui::test] async fn test_toggle_comment(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a5c2a64bea..11aec10d5c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -882,6 +882,45 @@ impl MultiBuffer { }) } + pub fn is_completion_trigger(&self, position: T, text: &str, cx: &AppContext) -> bool + where + T: ToOffset, + { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + if chars.next().is_some() { + return false; + } + + if char.is_alphanumeric() || char == '_' { + return true; + } + + let snapshot = self.snapshot(cx); + let anchor = snapshot.anchor_before(position); + let buffer = self.buffers.borrow()[&anchor.buffer_id].buffer.clone(); + if let Some(language_server) = buffer.read(cx).language_server() { + language_server + .capabilities() + .completion_provider + .as_ref() + .map_or(false, |provider| { + provider + .trigger_characters + .as_ref() + .map_or(false, |characters| { + characters.iter().any(|string| string == text) + }) + }) + } else { + false + } + } + pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { self.buffers .borrow() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a5fc6a8b84..a8a642920c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -214,6 +214,85 @@ pub trait LocalFile: File { ); } +#[cfg(feature = "test-support")] +pub struct FakeFile { + pub path: Arc, +} + +#[cfg(feature = "test-support")] +impl File for FakeFile { + fn as_local(&self) -> Option<&dyn LocalFile> { + Some(self) + } + + fn mtime(&self) -> SystemTime { + SystemTime::UNIX_EPOCH + } + + fn path(&self) -> &Arc { + &self.path + } + + fn full_path(&self, _: &AppContext) -> PathBuf { + self.path.to_path_buf() + } + + fn file_name(&self, _: &AppContext) -> OsString { + self.path.file_name().unwrap().to_os_string() + } + + fn is_deleted(&self) -> bool { + false + } + + fn save( + &self, + _: u64, + _: Rope, + _: clock::Global, + cx: &mut MutableAppContext, + ) -> Task> { + cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) + } + + fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) + -> Option>> { + None + } + + fn buffer_updated(&self, _: u64, operation: Operation, cx: &mut MutableAppContext) {} + + fn buffer_removed(&self, _: u64, cx: &mut MutableAppContext) {} + + fn as_any(&self) -> &dyn Any { + self + } + + fn to_proto(&self) -> rpc::proto::File { + unimplemented!() + } +} + +#[cfg(feature = "test-support")] +impl LocalFile for FakeFile { + fn abs_path(&self, _: &AppContext) -> PathBuf { + self.path.to_path_buf() + } + + fn load(&self, cx: &AppContext) -> Task> { + cx.background().spawn(async move { Ok(Default::default()) }) + } + + fn buffer_reloaded( + &self, + buffer_id: u64, + version: &clock::Global, + mtime: SystemTime, + cx: &mut MutableAppContext, + ) { + } +} + pub(crate) struct QueryCursorHandle(Option); #[derive(Clone)] @@ -759,6 +838,10 @@ impl Buffer { self.language.as_ref() } + pub fn language_server(&self) -> Option<&Arc> { + self.language_server.as_ref().map(|state| &state.server) + } + pub fn parse_count(&self) -> usize { self.parse_count } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index b2c658dbd6..803148427f 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context, Result}; use futures::{io::BufWriter, AsyncRead, AsyncWrite}; use gpui::{executor, Task}; -use parking_lot::{Mutex, RwLock}; +use parking_lot::{Mutex, RwLock, RwLockReadGuard}; use postage::{barrier, oneshot, prelude::Stream, sink::Sink}; use serde::{Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; @@ -34,6 +34,7 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, outbound_tx: RwLock>>>, + capabilities: RwLock, notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, @@ -197,6 +198,7 @@ impl LanguageServer { let this = Arc::new(Self { notification_handlers, response_handlers, + capabilities: Default::default(), next_id: Default::default(), outbound_tx: RwLock::new(Some(outbound_tx)), executor: executor.clone(), @@ -265,7 +267,8 @@ impl LanguageServer { this.outbound_tx.read().as_ref(), params, ); - request.await?; + let response = request.await?; + *this.capabilities.write() = response.capabilities; Self::notify_internal::( this.outbound_tx.read().as_ref(), InitializedParams {}, @@ -324,6 +327,10 @@ impl LanguageServer { } } + pub fn capabilities(&self) -> RwLockReadGuard { + self.capabilities.read() + } + pub fn request( self: &Arc, params: T::Params, @@ -458,6 +465,13 @@ pub struct RequestId { #[cfg(any(test, feature = "test-support"))] impl LanguageServer { pub async fn fake(executor: Arc) -> (Arc, FakeLanguageServer) { + Self::fake_with_capabilities(Default::default(), executor).await + } + + pub async fn fake_with_capabilities( + capabilities: ServerCapabilities, + executor: Arc, + ) -> (Arc, FakeLanguageServer) { let stdin = async_pipe::pipe(); let stdout = async_pipe::pipe(); let mut fake = FakeLanguageServer { @@ -470,7 +484,14 @@ impl LanguageServer { let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap(); let (init_id, _) = fake.receive_request::().await; - fake.respond(init_id, InitializeResult::default()).await; + fake.respond( + init_id, + InitializeResult { + capabilities, + ..Default::default() + }, + ) + .await; fake.receive_notification::() .await; From fde03b1b37a9a8232ee5a4c71a29c7e5adf849f3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 18:26:26 -0700 Subject: [PATCH 14/61] Make the anchor range inclusive on completions This will help us to correctly interpolate the replacement range when we confirm before receiving new completions after typing with a completion open. --- crates/language/src/buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a8a642920c..446bd60b87 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1758,7 +1758,7 @@ impl Buffer { }, }; - let old_range = this.anchor_after(old_range.start)..this.anchor_before(old_range.end); + let old_range = this.anchor_before(old_range.start)..this.anchor_after(old_range.end); Some(Completion { old_range, From ae0237c21b02457c1c92e8213b356020c9952d1b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 18:33:25 -0700 Subject: [PATCH 15/61] Create anchors with latest snapshot when completions are ready Using the previous snapshot was panicking. --- crates/editor/src/multi_buffer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 11aec10d5c..ccae933517 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -856,13 +856,13 @@ impl MultiBuffer { where T: ToOffset, { - let snapshot = self.snapshot(cx); - let anchor = snapshot.anchor_before(position); + let anchor = self.read(cx).anchor_before(position); let buffer = self.buffers.borrow()[&anchor.buffer_id].buffer.clone(); let completions = buffer.update(cx, |buffer, cx| buffer.completions(anchor.text_anchor, cx)); - cx.foreground().spawn(async move { + cx.spawn(|this, cx| async move { completions.await.map(|completions| { + let snapshot = this.read_with(&cx, |buffer, cx| buffer.snapshot(cx)); completions .into_iter() .map(|completion| Completion { From 327ddbe2b49b80c72d43590a633ca135b0654e06 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 18:34:20 -0700 Subject: [PATCH 16/61] Always issue a new completions request when typing a trigger character We'll interpolate the anchor range of original request, but it's still a good idea to be up-to-date in case the language server is influenced by the content preceding the location. This doesn't *seem* to be the case with rust-analyzer so far, but it's how VS Code works so let's do it this way. --- crates/editor/src/editor.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4e383d5518..1f892369ee 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1390,15 +1390,13 @@ impl Editor { } fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { - if self.completion_state.is_none() { - if let Some(selection) = self.newest_anchor_selection() { - if self - .buffer - .read(cx) - .is_completion_trigger(selection.head(), text, cx) - { - self.show_completions(&ShowCompletions, cx); - } + if let Some(selection) = self.newest_anchor_selection() { + if self + .buffer + .read(cx) + .is_completion_trigger(selection.head(), text, cx) + { + self.show_completions(&ShowCompletions, cx); } } } From 92f0491c0ee625801f874bce21d7effacd825cf9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 31 Jan 2022 18:38:49 -0700 Subject: [PATCH 17/61] Don't assign completion_state when completions are empty --- crates/editor/src/editor.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f892369ee..c01383bb8a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1538,17 +1538,18 @@ impl Editor { cx.spawn_weak(|this, mut cx| async move { let completions = completions.await?; - if let Some(this) = cx.read(|cx| this.upgrade(cx)) { - this.update(&mut cx, |this, cx| { - this.completion_state = Some(CompletionState { - completions: completions.into(), - selected_item: 0, - list: Default::default(), + if !completions.is_empty() { + if let Some(this) = cx.read(|cx| this.upgrade(cx)) { + this.update(&mut cx, |this, cx| { + this.completion_state = Some(CompletionState { + completions: completions.into(), + selected_item: 0, + list: Default::default(), + }); + cx.notify(); }); - cx.notify(); - }); + } } - Ok::<_, anyhow::Error>(()) }) .detach_and_log_err(cx); From bcbd265de9d2fbde46e4ba8633faaa0296ed6394 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 1 Feb 2022 09:43:13 +0100 Subject: [PATCH 18/61] Dismiss autocomplete when moving outside of a word --- crates/editor/src/editor.rs | 65 +++++++++++++++++++-------- crates/editor/src/movement.rs | 82 +++++++++++++++++++++++++++-------- crates/find/src/find.rs | 2 +- 3 files changed, 112 insertions(+), 37 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c01383bb8a..cfc00fa7e6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -428,6 +428,7 @@ struct BracketPairState { } struct CompletionState { + initial_position: Anchor, completions: Arc<[Completion]>, selected_item: usize, list: UniformListState, @@ -452,7 +453,7 @@ pub struct NavigationData { offset: usize, } -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] pub enum CharKind { Newline, Punctuation, @@ -887,7 +888,7 @@ impl Editor { mode = SelectMode::Character; } 2 => { - let range = movement::surrounding_word(&display_map, position); + let (range, _) = movement::surrounding_word(&display_map, position); start = buffer.anchor_before(range.start.to_point(&display_map)); end = buffer.anchor_before(range.end.to_point(&display_map)); mode = SelectMode::Word(start.clone()..end.clone()); @@ -990,7 +991,7 @@ impl Editor { if movement::is_inside_word(&display_map, position) || original_display_range.contains(&position) { - let word_range = movement::surrounding_word(&display_map, position); + let (word_range, _) = movement::surrounding_word(&display_map, position); if word_range.start < original_display_range.start { head = word_range.start.to_point(&display_map); } else { @@ -1397,6 +1398,9 @@ impl Editor { .is_completion_trigger(selection.head(), text, cx) { self.show_completions(&ShowCompletions, cx); + } else { + self.completion_state.take(); + cx.notify(); } } } @@ -1528,25 +1532,30 @@ impl Editor { } fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { - let position = self - .newest_selection::(&self.buffer.read(cx).read(cx)) - .head(); + let position = if let Some(selection) = self.newest_anchor_selection() { + selection.head() + } else { + return; + }; let completions = self .buffer - .update(cx, |buffer, cx| buffer.completions(position, cx)); + .update(cx, |buffer, cx| buffer.completions(position.clone(), cx)); cx.spawn_weak(|this, mut cx| async move { let completions = completions.await?; if !completions.is_empty() { if let Some(this) = cx.read(|cx| this.upgrade(cx)) { this.update(&mut cx, |this, cx| { - this.completion_state = Some(CompletionState { - completions: completions.into(), - selected_item: 0, - list: Default::default(), - }); - cx.notify(); + if this.focused { + this.completion_state = Some(CompletionState { + initial_position: position, + completions: completions.into(), + selected_item: 0, + list: Default::default(), + }); + cx.notify(); + } }); } } @@ -2905,7 +2914,7 @@ impl Editor { } else if selections.len() == 1 { let selection = selections.last_mut().unwrap(); if selection.start == selection.end { - let word_range = movement::surrounding_word( + let (word_range, _) = movement::surrounding_word( &display_map, selection.start.to_display_point(&display_map), ); @@ -3525,7 +3534,8 @@ impl Editor { ) where T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { - let buffer = self.buffer.read(cx).snapshot(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; let old_cursor_position = self.newest_anchor_selection().map(|s| s.head()); selections.sort_unstable_by_key(|s| s.start); @@ -3570,16 +3580,32 @@ impl Editor { } } + let new_cursor_position = selections + .iter() + .max_by_key(|s| s.id) + .map(|s| s.head().to_point(&buffer)); if let Some(old_cursor_position) = old_cursor_position { - let new_cursor_position = selections - .iter() - .max_by_key(|s| s.id) - .map(|s| s.head().to_point(&buffer)); if new_cursor_position.is_some() { self.push_to_nav_history(old_cursor_position, new_cursor_position, cx); } } + if let Some((completion_state, cursor_position)) = + self.completion_state.as_ref().zip(new_cursor_position) + { + let cursor_position = cursor_position.to_display_point(&display_map); + let initial_position = completion_state + .initial_position + .to_display_point(&display_map); + + let (word_range, kind) = movement::surrounding_word(&display_map, initial_position); + if kind != Some(CharKind::Word) || !word_range.to_inclusive().contains(&cursor_position) + { + self.completion_state.take(); + cx.notify(); + } + } + if let Some(autoscroll) = autoscroll { self.request_autoscroll(autoscroll, cx); } @@ -4207,6 +4233,7 @@ impl View for Editor { self.show_local_cursors = false; self.buffer .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + self.completion_state.take(); cx.emit(Event::Blurred); cx.notify(); } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 9a800f9abb..36b384fadb 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -183,7 +183,10 @@ pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } -pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range { +pub fn surrounding_word( + map: &DisplaySnapshot, + point: DisplayPoint, +) -> (Range, Option) { let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let mut end = start; @@ -211,8 +214,11 @@ pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range Date: Tue, 1 Feb 2022 15:11:20 +0100 Subject: [PATCH 19/61] Filter and sort suggestions in autocomplete Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 + crates/editor/Cargo.toml | 2 + crates/editor/src/editor.rs | 197 ++++++++++++++++++---------- crates/editor/src/movement.rs | 122 ++++------------- crates/editor/src/multi_buffer.rs | 49 +++++++ crates/find/src/find.rs | 2 +- crates/fuzzy/src/fuzzy.rs | 4 + crates/gpui/src/fonts.rs | 15 ++- crates/language/src/buffer.rs | 32 ++++- crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 3 +- 11 files changed, 258 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7c811bc53..5de5b7ce1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1547,12 +1547,14 @@ dependencies = [ "collections", "ctor", "env_logger", + "fuzzy", "gpui", "itertools", "language", "lazy_static", "log", "lsp", + "ordered-float", "parking_lot", "postage", "project", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 6450db7d39..2ce10b4d81 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ text = { path = "../text" } clock = { path = "../clock" } collections = { path = "../collections" } +fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } project = { path = "../project" } @@ -31,6 +32,7 @@ anyhow = "1.0" itertools = "0.10" lazy_static = "1.4" log = "0.4" +ordered-float = "2.1.1" parking_lot = "0.11" postage = { version = "0.4", features = ["futures-traits"] } rand = { version = "0.8.3", optional = true } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cfc00fa7e6..9cdb670067 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13,6 +13,7 @@ use collections::{BTreeMap, HashMap, HashSet}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ action, color::Color, @@ -31,16 +32,17 @@ use language::{ }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot, - ToOffset, ToPoint, + char_kind, Anchor, AnchorRangeExt, CharKind, ExcerptId, ExcerptProperties, MultiBuffer, + MultiBufferSnapshot, ToOffset, ToPoint, }; +use ordered_float::OrderedFloat; use postage::watch; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; use std::{ any::TypeId, - cmp::{self, Ordering}, + cmp::{self, Ordering, Reverse}, iter::{self, FromIterator}, mem, ops::{Deref, Range, RangeInclusive, Sub}, @@ -50,7 +52,7 @@ use std::{ pub use sum_tree::Bias; use text::rope::TextDimension; use theme::{DiagnosticStyle, EditorStyle}; -use util::post_inc; +use util::{post_inc, ResultExt}; use workspace::{ItemNavHistory, PathOpener, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -430,6 +432,7 @@ struct BracketPairState { struct CompletionState { initial_position: Anchor, completions: Arc<[Completion]>, + matches: Arc<[StringMatch]>, selected_item: usize, list: UniformListState, } @@ -453,14 +456,6 @@ pub struct NavigationData { offset: usize, } -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] -pub enum CharKind { - Newline, - Punctuation, - Whitespace, - Word, -} - impl Editor { pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext) -> Self { let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); @@ -888,7 +883,7 @@ impl Editor { mode = SelectMode::Character; } 2 => { - let (range, _) = movement::surrounding_word(&display_map, position); + let range = movement::surrounding_word(&display_map, position); start = buffer.anchor_before(range.start.to_point(&display_map)); end = buffer.anchor_before(range.end.to_point(&display_map)); mode = SelectMode::Word(start.clone()..end.clone()); @@ -991,7 +986,7 @@ impl Editor { if movement::is_inside_word(&display_map, position) || original_display_range.contains(&position) { - let (word_range, _) = movement::surrounding_word(&display_map, position); + let word_range = movement::surrounding_word(&display_map, position); if word_range.start < original_display_range.start { head = word_range.start.to_point(&display_map); } else { @@ -1538,27 +1533,90 @@ impl Editor { return; }; + let query = { + let buffer = self.buffer.read(cx).read(cx); + let offset = position.to_offset(&buffer); + let (word_range, kind) = buffer.surrounding_word(offset); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + }; + let completions = self .buffer .update(cx, |buffer, cx| buffer.completions(position.clone(), cx)); cx.spawn_weak(|this, mut cx| async move { let completions = completions.await?; - if !completions.is_empty() { - if let Some(this) = cx.read(|cx| this.upgrade(cx)) { - this.update(&mut cx, |this, cx| { - if this.focused { - this.completion_state = Some(CompletionState { - initial_position: position, - completions: completions.into(), - selected_item: 0, - list: Default::default(), - }); - cx.notify(); - } - }); + let candidates = completions + .iter() + .enumerate() + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label()[completion.filter_range()].into(), + ) + }) + .collect::>(); + let mut matches = if let Some(query) = query.as_ref() { + fuzzy::match_strings( + &candidates, + query, + false, + 100, + &Default::default(), + cx.background(), + ) + .await + } else { + candidates + .into_iter() + .enumerate() + .map(|(candidate_id, candidate)| StringMatch { + candidate_id, + score: Default::default(), + positions: Default::default(), + string: candidate.string, + }) + .collect() + }; + matches.sort_unstable_by_key(|mat| { + ( + Reverse(OrderedFloat(mat.score)), + completions[mat.candidate_id].sort_key(), + ) + }); + + for mat in &mut matches { + let filter_start = completions[mat.candidate_id].filter_range().start; + for position in &mut mat.positions { + *position += filter_start; } } + + if let Some(this) = cx.read(|cx| this.upgrade(cx)) { + this.update(&mut cx, |this, cx| { + if matches.is_empty() { + this.completion_state.take(); + } else if this.focused { + this.completion_state = Some(CompletionState { + initial_position: position, + completions: completions.into(), + matches: matches.into(), + selected_item: 0, + list: Default::default(), + }); + } + + cx.notify(); + }); + } Ok::<_, anyhow::Error>(()) }) .detach_and_log_err(cx); @@ -1590,21 +1648,33 @@ impl Editor { let build_settings = self.build_settings.clone(); let settings = build_settings(cx); let completions = state.completions.clone(); + let matches = state.matches.clone(); let selected_item = state.selected_item; UniformList::new( state.list.clone(), - state.completions.len(), + matches.len(), move |range, items, cx| { let settings = build_settings(cx); let start_ix = range.start; - for (ix, completion) in completions[range].iter().enumerate() { + let label_style = LabelStyle { + text: settings.style.text.clone(), + highlight_text: settings + .style + .text + .clone() + .highlight(settings.style.autocomplete.match_highlight, cx.font_cache()) + .log_err(), + }; + for (ix, mat) in matches[range].iter().enumerate() { let item_style = if start_ix + ix == selected_item { settings.style.autocomplete.selected_item } else { settings.style.autocomplete.item }; + let completion = &completions[mat.candidate_id]; items.push( - Label::new(completion.label().to_string(), settings.style.text.clone()) + Label::new(completion.label().to_string(), label_style.clone()) + .with_highlights(mat.positions.clone()) .contained() .with_style(item_style) .boxed(), @@ -1614,10 +1684,12 @@ impl Editor { ) .with_width_from_item( state - .completions + .matches .iter() .enumerate() - .max_by_key(|(_, completion)| completion.label().chars().count()) + .max_by_key(|(_, mat)| { + state.completions[mat.candidate_id].label().chars().count() + }) .map(|(ix, _)| ix), ) .contained() @@ -2914,7 +2986,7 @@ impl Editor { } else if selections.len() == 1 { let selection = selections.last_mut().unwrap(); if selection.start == selection.end { - let (word_range, _) = movement::surrounding_word( + let word_range = movement::surrounding_word( &display_map, selection.start.to_display_point(&display_map), ); @@ -3534,8 +3606,7 @@ impl Editor { ) where T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; + let buffer = self.buffer.read(cx).snapshot(cx); let old_cursor_position = self.newest_anchor_selection().map(|s| s.head()); selections.sort_unstable_by_key(|s| s.start); @@ -3580,29 +3651,14 @@ impl Editor { } } - let new_cursor_position = selections - .iter() - .max_by_key(|s| s.id) - .map(|s| s.head().to_point(&buffer)); + let new_cursor_position = selections.iter().max_by_key(|s| s.id).map(|s| s.head()); if let Some(old_cursor_position) = old_cursor_position { - if new_cursor_position.is_some() { - self.push_to_nav_history(old_cursor_position, new_cursor_position, cx); - } - } - - if let Some((completion_state, cursor_position)) = - self.completion_state.as_ref().zip(new_cursor_position) - { - let cursor_position = cursor_position.to_display_point(&display_map); - let initial_position = completion_state - .initial_position - .to_display_point(&display_map); - - let (word_range, kind) = movement::surrounding_word(&display_map, initial_position); - if kind != Some(CharKind::Word) || !word_range.to_inclusive().contains(&cursor_position) - { - self.completion_state.take(); - cx.notify(); + if let Some(new_cursor_position) = new_cursor_position { + self.push_to_nav_history( + old_cursor_position, + Some(new_cursor_position.to_point(&buffer)), + cx, + ); } } @@ -3628,6 +3684,21 @@ impl Editor { })), cx, ); + + if let Some((completion_state, cursor_position)) = + self.completion_state.as_ref().zip(new_cursor_position) + { + let cursor_position = cursor_position.to_offset(&buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_state.initial_position.clone()); + if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) + { + self.show_completions(&ShowCompletions, cx); + } else { + self.completion_state.take(); + cx.notify(); + } + } } /// Compute new ranges for any selections that were located in excerpts that have @@ -4424,18 +4495,6 @@ pub fn settings_builder( }) } -pub fn char_kind(c: char) -> CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { - CharKind::Whitespace - } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 36b384fadb..7eee1e627c 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,7 +1,7 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, ToPoint}; use anyhow::Result; -use std::{cmp, ops::Range}; +use std::ops::Range; pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result { if point.column() > 0 { @@ -183,42 +183,20 @@ pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } -pub fn surrounding_word( - map: &DisplaySnapshot, - point: DisplayPoint, -) -> (Range, Option) { - let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); - let mut end = start; - - let text = &map.buffer_snapshot; - let mut next_chars = text.chars_at(start).peekable(); - let mut prev_chars = text.reversed_chars_at(start).peekable(); - let word_kind = cmp::max( - prev_chars.peek().copied().map(char_kind), - next_chars.peek().copied().map(char_kind), - ); - - for ch in prev_chars { - if Some(char_kind(ch)) == word_kind { - start -= ch.len_utf8(); - } else { - break; - } - } - - for ch in next_chars { - if Some(char_kind(ch)) == word_kind { - end += ch.len_utf8(); - } else { - break; - } - } - - ( - start.to_point(&map.buffer_snapshot).to_display_point(map) - ..end.to_point(&map.buffer_snapshot).to_display_point(map), - word_kind, - ) +pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range { + let position = map + .clip_point(position, Bias::Left) + .to_offset(map, Bias::Left); + let (range, _) = map.buffer_snapshot.surrounding_word(position); + let start = range + .start + .to_point(&map.buffer_snapshot) + .to_display_point(map); + let end = range + .end + .to_point(&map.buffer_snapshot) + .to_display_point(map); + start..end } #[cfg(test)] @@ -412,101 +390,59 @@ mod tests { assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 0)), - ( - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 2)), - ( - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 5)), - ( - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 6)), - ( - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 7)), - ( - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 11)), - ( - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 13)), - ( - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14), - Some(CharKind::Whitespace) - ) + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 14)), - ( - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 17)), - ( - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(0, 19)), - ( - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - Some(CharKind::Word) - ) + DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(1, 0)), - ( - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), - Some(CharKind::Whitespace) - ) + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(1, 1)), - ( - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), - Some(CharKind::Whitespace) - ) + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(1, 6)), - ( - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), - Some(CharKind::Word) - ) + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), ); assert_eq!( surrounding_word(&snapshot, DisplayPoint::new(1, 7)), - ( - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), - Some(CharKind::Word) - ) + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), ); } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index ccae933517..eafb87d9c1 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -50,6 +50,14 @@ struct History { group_interval: Duration, } +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] +pub enum CharKind { + Newline, + Punctuation, + Whitespace, + Word, +} + struct Transaction { id: usize, buffer_transactions: HashSet<(usize, text::TransactionId)>, @@ -1155,6 +1163,35 @@ impl MultiBufferSnapshot { .eq(needle.bytes()) } + pub fn surrounding_word(&self, start: T) -> (Range, Option) { + let mut start = start.to_offset(self); + let mut end = start; + let mut next_chars = self.chars_at(start).peekable(); + let mut prev_chars = self.reversed_chars_at(start).peekable(); + let word_kind = cmp::max( + prev_chars.peek().copied().map(char_kind), + next_chars.peek().copied().map(char_kind), + ); + + for ch in prev_chars { + if Some(char_kind(ch)) == word_kind { + start -= ch.len_utf8(); + } else { + break; + } + } + + for ch in next_chars { + if Some(char_kind(ch)) == word_kind { + end += ch.len_utf8(); + } else { + break; + } + } + + (start..end, word_kind) + } + fn as_singleton(&self) -> Option<&Excerpt> { if self.singleton { self.excerpts.iter().next() @@ -2418,6 +2455,18 @@ impl ToPoint for Point { } } +pub fn char_kind(c: char) -> CharKind { + if c == '\n' { + CharKind::Newline + } else if c.is_whitespace() { + CharKind::Whitespace + } else if c.is_alphanumeric() || c == '_' { + CharKind::Word + } else { + CharKind::Punctuation + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 571d9f4c4b..859e9f4923 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -296,7 +296,7 @@ impl FindBar { let mut text: String; if selection.start == selection.end { let point = selection.start.to_display_point(&display_map); - let (range, _) = editor::movement::surrounding_word(&display_map, point); + let range = editor::movement::surrounding_word(&display_map, point); let range = range.start.to_offset(&display_map, Bias::Left) ..range.end.to_offset(&display_map, Bias::Right); text = display_map.buffer_snapshot.text_for_range(range).collect(); diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index dbbef435f6..7458f27c91 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -181,6 +181,10 @@ pub async fn match_strings( cancel_flag: &AtomicBool, background: Arc, ) -> Vec { + if candidates.is_empty() { + return Default::default(); + } + let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 2768b9f986..7dc1173048 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -5,7 +5,7 @@ use crate::{ text_layout::RunStyle, FontCache, }; -use anyhow::anyhow; +use anyhow::{anyhow, Result}; pub use font_kit::{ metrics::Metrics, properties::{Properties, Stretch, Style, Weight}, @@ -107,7 +107,7 @@ impl TextStyle { underline: Option, color: Color, font_cache: &FontCache, - ) -> anyhow::Result { + ) -> Result { let font_family_name = font_family_name.into(); let font_family_id = font_cache.load_family(&[&font_family_name])?; let font_id = font_cache.select_font(font_family_id, &font_properties)?; @@ -127,6 +127,15 @@ impl TextStyle { self } + pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result { + if self.font_properties != style.font_properties { + self.font_id = font_cache.select_font(self.font_family_id, &style.font_properties)?; + } + self.color = style.color; + self.underline = style.underline; + Ok(self) + } + pub fn to_run(&self) -> RunStyle { RunStyle { font_id: self.font_id, @@ -135,7 +144,7 @@ impl TextStyle { } } - fn from_json(json: TextStyleJson) -> anyhow::Result { + fn from_json(json: TextStyleJson) -> Result { FONT_CACHE.with(|font_cache| { if let Some(font_cache) = font_cache.borrow().as_ref() { let font_properties = properties_from_json(json.weight, json.italic); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 446bd60b87..5b66d4c8e8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -114,6 +114,7 @@ pub struct Diagnostic { pub is_disk_based: bool, } +#[derive(Debug)] pub struct Completion { pub old_range: Range, pub new_text: String, @@ -230,7 +231,7 @@ impl File for FakeFile { } fn path(&self) -> &Arc { - &self.path + &self.path } fn full_path(&self, _: &AppContext) -> PathBuf { @@ -255,8 +256,11 @@ impl File for FakeFile { cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) } - fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) - -> Option>> { + fn format_remote( + &self, + buffer_id: u64, + cx: &mut MutableAppContext, + ) -> Option>> { None } @@ -1759,7 +1763,7 @@ impl Buffer { }; let old_range = this.anchor_before(old_range.start)..this.anchor_after(old_range.end); - + Some(Completion { old_range, new_text, @@ -2511,6 +2515,26 @@ impl Completion { pub fn label(&self) -> &str { &self.lsp_completion.label } + + pub fn filter_range(&self) -> Range { + if let Some(filter_text) = self.lsp_completion.filter_text.as_deref() { + if let Some(start) = self.label().find(filter_text) { + start..start + filter_text.len() + } else { + 0..self.label().len() + } + } else { + 0..self.label().len() + } + } + + pub fn sort_key(&self) -> (usize, &str) { + let kind_key = match self.lsp_completion.kind { + Some(lsp::CompletionItemKind::VARIABLE) => 0, + _ => 1, + }; + (kind_key, &self.label()[self.filter_range()]) + } } pub fn contiguous_ranges( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index aca5e7d245..d097e1b003 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -328,6 +328,7 @@ pub struct AutocompleteStyle { pub container: ContainerStyle, pub item: ContainerStyle, pub selected_item: ContainerStyle, + pub match_highlight: HighlightStyle, } #[derive(Clone, Copy, Default, Deserialize)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 6a0c98c67f..2475dd9531 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -188,7 +188,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -318,6 +318,7 @@ message.highlight_text.color = "$text.3.color" background = "$surface.2" border = { width = 1, color = "$border.1" } item.padding = 2 +match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" } [editor.autocomplete.selected_item] extends = "$editor.autocomplete.item" From 497626ef2b719062fd1e039bc09c27902821adf1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 1 Feb 2022 07:59:37 -0700 Subject: [PATCH 20/61] Re-filter existing completions on selection update We still request new completions, but this ensures results are up-to-date in the meantime. Also: Cancel any pending completions task when we dismiss the completions dialog or start a new completions request. Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 204 +++++++++++++---------- crates/editor/src/multi_buffer/anchor.rs | 2 +- 2 files changed, 115 insertions(+), 91 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9cdb670067..97bdfd1df1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18,11 +18,12 @@ use gpui::{ action, color::Color, elements::*, + executor, fonts::TextStyle, geometry::vector::{vec2f, Vector2F}, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, - MutableAppContext, RenderContext, View, ViewContext, WeakModelHandle, WeakViewHandle, + MutableAppContext, RenderContext, Task, View, ViewContext, WeakModelHandle, WeakViewHandle, }; use items::BufferItemHandle; use itertools::Itertools as _; @@ -52,7 +53,7 @@ use std::{ pub use sum_tree::Bias; use text::rope::TextDimension; use theme::{DiagnosticStyle, EditorStyle}; -use util::{post_inc, ResultExt}; +use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, PathOpener, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -396,6 +397,7 @@ pub struct Editor { highlighted_ranges: BTreeMap>)>, nav_history: Option, completion_state: Option, + completions_task: Option>>, } pub struct EditorSnapshot { @@ -432,11 +434,54 @@ struct BracketPairState { struct CompletionState { initial_position: Anchor, completions: Arc<[Completion]>, + match_candidates: Vec, matches: Arc<[StringMatch]>, selected_item: usize, list: UniformListState, } +impl CompletionState { + pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { + let mut matches = if let Some(query) = query { + fuzzy::match_strings( + &self.match_candidates, + query, + false, + 100, + &Default::default(), + executor, + ) + .await + } else { + self.match_candidates + .iter() + .enumerate() + .map(|(candidate_id, candidate)| StringMatch { + candidate_id, + score: Default::default(), + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect() + }; + matches.sort_unstable_by_key(|mat| { + ( + Reverse(OrderedFloat(mat.score)), + self.completions[mat.candidate_id].sort_key(), + ) + }); + + for mat in &mut matches { + let filter_start = self.completions[mat.candidate_id].filter_range().start; + for position in &mut mat.positions { + *position += filter_start; + } + } + + self.matches = matches.into(); + } +} + #[derive(Debug)] struct ActiveDiagnosticGroup { primary_range: Range, @@ -546,6 +591,7 @@ impl Editor { highlighted_ranges: Default::default(), nav_history: None, completion_state: None, + completions_task: None, }; let selection = Selection { id: post_inc(&mut this.next_selection_id), @@ -1101,8 +1147,7 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if self.completion_state.take().is_some() { - cx.notify(); + if self.hide_completions(cx).is_some() { return; } @@ -1394,8 +1439,7 @@ impl Editor { { self.show_completions(&ShowCompletions, cx); } else { - self.completion_state.take(); - cx.notify(); + self.hide_completions(cx); } } } @@ -1526,6 +1570,20 @@ impl Editor { } } + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + let offset = position.to_offset(buffer); + let (word_range, kind) = buffer.surrounding_word(offset); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + } + fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { let position = if let Some(selection) = self.newest_anchor_selection() { selection.head() @@ -1533,97 +1591,62 @@ impl Editor { return; }; - let query = { - let buffer = self.buffer.read(cx).read(cx); - let offset = position.to_offset(&buffer); - let (word_range, kind) = buffer.surrounding_word(offset); - if offset > word_range.start && kind == Some(CharKind::Word) { - Some( - buffer - .text_for_range(word_range.start..offset) - .collect::(), - ) - } else { - None - } - }; - + let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); let completions = self .buffer .update(cx, |buffer, cx| buffer.completions(position.clone(), cx)); - cx.spawn_weak(|this, mut cx| async move { - let completions = completions.await?; - let candidates = completions - .iter() - .enumerate() - .map(|(id, completion)| { - StringMatchCandidate::new( - id, - completion.label()[completion.filter_range()].into(), - ) - }) - .collect::>(); - let mut matches = if let Some(query) = query.as_ref() { - fuzzy::match_strings( - &candidates, - query, - false, - 100, - &Default::default(), - cx.background(), - ) - .await - } else { - candidates - .into_iter() - .enumerate() - .map(|(candidate_id, candidate)| StringMatch { - candidate_id, - score: Default::default(), - positions: Default::default(), - string: candidate.string, - }) - .collect() - }; - matches.sort_unstable_by_key(|mat| { - ( - Reverse(OrderedFloat(mat.score)), - completions[mat.candidate_id].sort_key(), - ) - }); + self.completions_task = Some(cx.spawn_weak(|this, mut cx| { + async move { + let completions = completions.await?; - for mat in &mut matches { - let filter_start = completions[mat.candidate_id].filter_range().start; - for position in &mut mat.positions { - *position += filter_start; + let mut completion_state = CompletionState { + initial_position: position, + match_candidates: completions + .iter() + .enumerate() + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label()[completion.filter_range()].into(), + ) + }) + .collect(), + completions: completions.into(), + matches: Vec::new().into(), + selected_item: 0, + list: Default::default(), + }; + + completion_state + .filter(query.as_deref(), cx.background()) + .await; + + if let Some(this) = cx.read(|cx| this.upgrade(cx)) { + this.update(&mut cx, |this, cx| { + if completion_state.matches.is_empty() { + this.hide_completions(cx); + } else if this.focused { + this.completion_state = Some(completion_state); + } + + cx.notify(); + }); } + Ok::<_, anyhow::Error>(()) } + .log_err() + })); + } - if let Some(this) = cx.read(|cx| this.upgrade(cx)) { - this.update(&mut cx, |this, cx| { - if matches.is_empty() { - this.completion_state.take(); - } else if this.focused { - this.completion_state = Some(CompletionState { - initial_position: position, - completions: completions.into(), - matches: matches.into(), - selected_item: 0, - list: Default::default(), - }); - } - - cx.notify(); - }); - } - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); + fn hide_completions(&mut self, cx: &mut ViewContext) -> Option { + cx.notify(); + self.completions_task.take(); + self.completion_state.take() } fn confirm_completion(&mut self, _: &ConfirmCompletion, cx: &mut ViewContext) { - if let Some(completion_state) = self.completion_state.take() { + if let Some(completion_state) = self.hide_completions(cx) { if let Some(completion) = completion_state .completions .get(completion_state.selected_item) @@ -3686,17 +3709,18 @@ impl Editor { ); if let Some((completion_state, cursor_position)) = - self.completion_state.as_ref().zip(new_cursor_position) + self.completion_state.as_mut().zip(new_cursor_position) { let cursor_position = cursor_position.to_offset(&buffer); let (word_range, kind) = buffer.surrounding_word(completion_state.initial_position.clone()); if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) { + let query = Self::completion_query(&buffer, cursor_position); + smol::block_on(completion_state.filter(query.as_deref(), cx.background().clone())); self.show_completions(&ShowCompletions, cx); } else { - self.completion_state.take(); - cx.notify(); + self.hide_completions(cx); } } } @@ -4304,7 +4328,7 @@ impl View for Editor { self.show_local_cursors = false; self.buffer .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - self.completion_state.take(); + self.hide_completions(cx); cx.emit(Event::Blurred); cx.notify(); } diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index e2b6d2f2ad..03d16db1ef 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -106,7 +106,7 @@ impl Anchor { } impl ToOffset for Anchor { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize { self.summary(snapshot) } } From bcc57036a599b522c1283d23979cae060b87c822 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 1 Feb 2022 16:59:03 +0100 Subject: [PATCH 21/61] Fix warnings in `language::FakeFile` Co-Authored-By: Nathan Sobo --- crates/language/src/buffer.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5b66d4c8e8..f431da1476 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -256,17 +256,13 @@ impl File for FakeFile { cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) } - fn format_remote( - &self, - buffer_id: u64, - cx: &mut MutableAppContext, - ) -> Option>> { + fn format_remote(&self, _: u64, _: &mut MutableAppContext) -> Option>> { None } - fn buffer_updated(&self, _: u64, operation: Operation, cx: &mut MutableAppContext) {} + fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} - fn buffer_removed(&self, _: u64, cx: &mut MutableAppContext) {} + fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} fn as_any(&self) -> &dyn Any { self @@ -287,13 +283,7 @@ impl LocalFile for FakeFile { cx.background().spawn(async move { Ok(Default::default()) }) } - fn buffer_reloaded( - &self, - buffer_id: u64, - version: &clock::Global, - mtime: SystemTime, - cx: &mut MutableAppContext, - ) { + fn buffer_reloaded(&self, _: u64, _: &clock::Global, _: SystemTime, _: &mut MutableAppContext) { } } From 6c7d2cf6b506204ee96b8624c645d6aeaa62236e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 1 Feb 2022 17:38:11 +0100 Subject: [PATCH 22/61] Apply additional edits when confirming a completion Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 53 +++++++++++---- crates/editor/src/multi_buffer.rs | 30 ++++++++- crates/language/src/buffer.rs | 105 +++++++++++++++++++++--------- 3 files changed, 145 insertions(+), 43 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 97bdfd1df1..cd6ef78417 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8,6 +8,7 @@ mod multi_buffer; mod test; use aho_corasick::AhoCorasick; +use anyhow::Result; use clock::ReplicaId; use collections::{BTreeMap, HashMap, HashSet}; pub use display_map::DisplayPoint; @@ -295,7 +296,9 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec) { + fn confirm_completion(&mut self, cx: &mut ViewContext) -> Task> { if let Some(completion_state) = self.hide_completions(cx) { if let Some(completion) = completion_state - .completions + .matches .get(completion_state.selected_item) + .and_then(|mat| completion_state.completions.get(mat.candidate_id)) { - self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent( - [completion.old_range.clone()], - completion.new_text.clone(), - cx, - ); - }) + return self.buffer.update(cx, |buffer, cx| { + buffer.apply_completion(completion.clone(), cx) + }); } } + + Task::ready(Ok(())) } pub fn has_completions(&self) -> bool { @@ -6654,9 +6656,9 @@ mod tests { editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { + let apply_additional_edits = editor.update(&mut cx, |editor, cx| { editor.move_down(&MoveDown, cx); - editor.confirm_completion(&ConfirmCompletion, cx); + let apply_additional_edits = editor.confirm_completion(cx); assert_eq!( editor.text(cx), " @@ -6666,7 +6668,34 @@ mod tests { " .unindent() ); + apply_additional_edits }); + let (id, _) = fake + .receive_request::() + .await; + fake.respond( + id, + lsp::CompletionItem { + additional_text_edits: Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 5)), + "\nadditional edit".to_string(), + )]), + ..Default::default() + }, + ) + .await; + + apply_additional_edits.await.unwrap(); + assert_eq!( + editor.read_with(&cx, |editor, cx| editor.text(cx)), + " + one.second_completion + two + three + additional edit + " + .unindent() + ); } #[gpui::test] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index eafb87d9c1..c70ea5d395 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1,7 +1,7 @@ mod anchor; pub use anchor::{Anchor, AnchorRangeExt}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::{HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; @@ -929,6 +929,34 @@ impl MultiBuffer { } } + pub fn apply_completion( + &self, + completion: Completion, + cx: &mut ModelContext, + ) -> Task> { + let buffer = if let Some(buffer) = self + .buffers + .borrow() + .get(&completion.old_range.start.buffer_id) + { + buffer.buffer.clone() + } else { + return Task::ready(Err(anyhow!("completion cannot be applied to any buffer"))); + }; + + buffer.update(cx, |buffer, cx| { + buffer.apply_completion( + Completion { + old_range: completion.old_range.start.text_anchor + ..completion.old_range.end.text_anchor, + new_text: completion.new_text, + lsp_completion: completion.lsp_completion, + }, + cx, + ) + }) + } + pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { self.buffers .borrow() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f431da1476..79a076c6fb 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -114,7 +114,7 @@ pub struct Diagnostic { pub is_disk_based: bool, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Completion { pub old_range: Range, pub new_text: String, @@ -165,6 +165,10 @@ pub enum Event { pub trait File { fn as_local(&self) -> Option<&dyn LocalFile>; + fn is_local(&self) -> bool { + self.as_local().is_some() + } + fn mtime(&self) -> SystemTime; /// Returns the path of this file relative to the worktree's root directory. @@ -567,21 +571,7 @@ impl Buffer { if let Some(edits) = edits { this.update(&mut cx, |this, cx| { if this.version == version { - for edit in &edits { - let range = range_from_lsp(edit.range); - if this.clip_point_utf16(range.start, Bias::Left) != range.start - || this.clip_point_utf16(range.end, Bias::Left) != range.end - { - return Err(anyhow!( - "invalid formatting edits received from language server" - )); - } - } - - for edit in edits.into_iter().rev() { - this.edit([range_from_lsp(edit.range)], edit.new_text, cx); - } - Ok(()) + this.apply_lsp_edits(edits, cx) } else { Err(anyhow!("buffer edited since starting to format")) } @@ -1390,13 +1380,6 @@ impl Buffer { self.edit_internal(ranges_iter, new_text, true, cx) } - /* - impl Buffer - pub fn edit - pub fn edit_internal - pub fn edit_with_autoindent - */ - pub fn edit_internal( &mut self, ranges_iter: I, @@ -1485,6 +1468,29 @@ impl Buffer { self.send_operation(Operation::Buffer(text::Operation::Edit(edit)), cx); } + fn apply_lsp_edits( + &mut self, + edits: Vec, + cx: &mut ModelContext, + ) -> Result<()> { + for edit in &edits { + let range = range_from_lsp(edit.range); + if self.clip_point_utf16(range.start, Bias::Left) != range.start + || self.clip_point_utf16(range.end, Bias::Left) != range.end + { + return Err(anyhow!( + "invalid formatting edits received from language server" + )); + } + } + + for edit in edits.into_iter().rev() { + self.edit([range_from_lsp(edit.range)], edit.new_text, cx); + } + + Ok(()) + } + fn did_edit( &mut self, old_version: &clock::Global, @@ -1752,13 +1758,17 @@ impl Buffer { }, }; - let old_range = this.anchor_before(old_range.start)..this.anchor_after(old_range.end); - - Some(Completion { - old_range, - new_text, - lsp_completion, - }) + let clipped_start = this.clip_point_utf16(old_range.start, Bias::Left); + let clipped_end = this.clip_point_utf16(old_range.end, Bias::Left) ; + if clipped_start == old_range.start && clipped_end == old_range.end { + Some(Completion { + old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end), + new_text, + lsp_completion, + }) + } else { + None + } }).collect()) }) }) @@ -1766,6 +1776,41 @@ impl Buffer { Task::ready(Ok(Default::default())) } } + + pub fn apply_completion( + &mut self, + completion: Completion, + cx: &mut ModelContext, + ) -> Task> { + self.edit_with_autoindent([completion.old_range], completion.new_text.clone(), cx); + + let file = if let Some(file) = self.file.as_ref() { + file + } else { + return Task::ready(Ok(Default::default())); + }; + if file.is_local() { + let server = if let Some(lang) = self.language_server.as_ref() { + lang.server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; + + cx.spawn(|this, mut cx| async move { + let resolved_completion = server + .request::(completion.lsp_completion) + .await?; + if let Some(additional_edits) = resolved_completion.additional_text_edits { + this.update(&mut cx, |this, cx| { + this.apply_lsp_edits(additional_edits, cx) + })?; + } + Ok::<_, anyhow::Error>(()) + }) + } else { + return Task::ready(Ok(Default::default())); + } + } } #[cfg(any(test, feature = "test-support"))] From d8737867107f5bf8f83b18bc62cab3a6013ef0aa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 1 Feb 2022 18:20:47 +0100 Subject: [PATCH 23/61] Advertise `snippet_support` capability to LSP Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/lsp/src/lsp.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 803148427f..cd235e8151 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -236,6 +236,7 @@ impl LanguageServer { }), completion: Some(CompletionClientCapabilities { completion_item: Some(CompletionItemCapability { + snippet_support: Some(true), resolve_support: Some(CompletionItemCapabilityResolveSupport { properties: vec!["additionalTextEdits".to_string()], }), From 956748e10cec8711cc5e6e02f6e83bbc994f02da Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Feb 2022 10:42:14 -0800 Subject: [PATCH 24/61] Add snippet crate Co-Authored-By: Nathan Sobo Co-Authored-By: Antonio Scandurra --- Cargo.lock | 18 + crates/snippet/Cargo.toml | 13 + crates/snippet/grammar/Cargo.toml | 26 + crates/snippet/grammar/binding.gyp | 19 + .../snippet/grammar/bindings/node/binding.cc | 28 + crates/snippet/grammar/bindings/node/index.js | 19 + crates/snippet/grammar/bindings/rust/build.rs | 40 ++ crates/snippet/grammar/bindings/rust/lib.rs | 52 ++ crates/snippet/grammar/grammar.js | 26 + crates/snippet/grammar/package.json | 19 + crates/snippet/grammar/src/grammar.json | 133 +++++ crates/snippet/grammar/src/node-types.json | 84 +++ crates/snippet/grammar/src/parser.c | 545 ++++++++++++++++++ .../snippet/grammar/src/tree_sitter/parser.h | 224 +++++++ crates/snippet/src/snippet.rs | 139 +++++ 15 files changed, 1385 insertions(+) create mode 100644 crates/snippet/Cargo.toml create mode 100644 crates/snippet/grammar/Cargo.toml create mode 100644 crates/snippet/grammar/binding.gyp create mode 100644 crates/snippet/grammar/bindings/node/binding.cc create mode 100644 crates/snippet/grammar/bindings/node/index.js create mode 100644 crates/snippet/grammar/bindings/rust/build.rs create mode 100644 crates/snippet/grammar/bindings/rust/lib.rs create mode 100644 crates/snippet/grammar/grammar.js create mode 100644 crates/snippet/grammar/package.json create mode 100644 crates/snippet/grammar/src/grammar.json create mode 100644 crates/snippet/grammar/src/node-types.json create mode 100644 crates/snippet/grammar/src/parser.c create mode 100644 crates/snippet/grammar/src/tree_sitter/parser.h create mode 100644 crates/snippet/src/snippet.rs diff --git a/Cargo.lock b/Cargo.lock index 5de5b7ce1c..53d7651e32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4418,6 +4418,16 @@ dependencies = [ "pin-project-lite 0.1.12", ] +[[package]] +name = "snippet" +version = "0.1.0" +dependencies = [ + "anyhow", + "smallvec", + "tree-sitter", + "tree-sitter-snippet", +] + [[package]] name = "socket2" version = "0.3.19" @@ -5203,6 +5213,14 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-snippet" +version = "0.0.1" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "ttf-parser" version = "0.9.0" diff --git a/crates/snippet/Cargo.toml b/crates/snippet/Cargo.toml new file mode 100644 index 0000000000..daba92f848 --- /dev/null +++ b/crates/snippet/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "snippet" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/snippet.rs" + +[dependencies] +anyhow = "1.0" +smallvec = { version = "1.6", features = ["union"] } +tree-sitter = "0.20" +tree-sitter-snippet = { path = "./grammar" } diff --git a/crates/snippet/grammar/Cargo.toml b/crates/snippet/grammar/Cargo.toml new file mode 100644 index 0000000000..83defad879 --- /dev/null +++ b/crates/snippet/grammar/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tree-sitter-snippet" +description = "snippet grammar for the tree-sitter parsing library" +version = "0.0.1" +keywords = ["incremental", "parsing", "snippet"] +categories = ["parsing", "text-editors"] +repository = "https://github.com/tree-sitter/tree-sitter-snippet" +edition = "2018" +license = "MIT" + +build = "bindings/rust/build.rs" +include = [ + "bindings/rust/*", + "grammar.js", + "queries/*", + "src/*", +] + +[lib] +path = "bindings/rust/lib.rs" + +[dependencies] +tree-sitter = "~0.20" + +[build-dependencies] +cc = "1.0" diff --git a/crates/snippet/grammar/binding.gyp b/crates/snippet/grammar/binding.gyp new file mode 100644 index 0000000000..a99fa70f98 --- /dev/null +++ b/crates/snippet/grammar/binding.gyp @@ -0,0 +1,19 @@ +{ + "targets": [ + { + "target_name": "tree_sitter_snippet_binding", + "include_dirs": [ + " +#include "nan.h" + +using namespace v8; + +extern "C" TSLanguage * tree_sitter_snippet(); + +namespace { + +NAN_METHOD(New) {} + +void Init(Local exports, Local module) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("Language").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + Local constructor = Nan::GetFunction(tpl).ToLocalChecked(); + Local instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked(); + Nan::SetInternalFieldPointer(instance, 0, tree_sitter_snippet()); + + Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("snippet").ToLocalChecked()); + Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance); +} + +NODE_MODULE(tree_sitter_snippet_binding, Init) + +} // namespace diff --git a/crates/snippet/grammar/bindings/node/index.js b/crates/snippet/grammar/bindings/node/index.js new file mode 100644 index 0000000000..c5ea00c7af --- /dev/null +++ b/crates/snippet/grammar/bindings/node/index.js @@ -0,0 +1,19 @@ +try { + module.exports = require("../../build/Release/tree_sitter_snippet_binding"); +} catch (error1) { + if (error1.code !== 'MODULE_NOT_FOUND') { + throw error1; + } + try { + module.exports = require("../../build/Debug/tree_sitter_snippet_binding"); + } catch (error2) { + if (error2.code !== 'MODULE_NOT_FOUND') { + throw error2; + } + throw error1 + } +} + +try { + module.exports.nodeTypeInfo = require("../../src/node-types.json"); +} catch (_) {} diff --git a/crates/snippet/grammar/bindings/rust/build.rs b/crates/snippet/grammar/bindings/rust/build.rs new file mode 100644 index 0000000000..c6061f0995 --- /dev/null +++ b/crates/snippet/grammar/bindings/rust/build.rs @@ -0,0 +1,40 @@ +fn main() { + let src_dir = std::path::Path::new("src"); + + let mut c_config = cc::Build::new(); + c_config.include(&src_dir); + c_config + .flag_if_supported("-Wno-unused-parameter") + .flag_if_supported("-Wno-unused-but-set-variable") + .flag_if_supported("-Wno-trigraphs"); + let parser_path = src_dir.join("parser.c"); + c_config.file(&parser_path); + + // If your language uses an external scanner written in C, + // then include this block of code: + + /* + let scanner_path = src_dir.join("scanner.c"); + c_config.file(&scanner_path); + println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); + */ + + c_config.compile("parser"); + println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap()); + + // If your language uses an external scanner written in C++, + // then include this block of code: + + /* + let mut cpp_config = cc::Build::new(); + cpp_config.cpp(true); + cpp_config.include(&src_dir); + cpp_config + .flag_if_supported("-Wno-unused-parameter") + .flag_if_supported("-Wno-unused-but-set-variable"); + let scanner_path = src_dir.join("scanner.cc"); + cpp_config.file(&scanner_path); + cpp_config.compile("scanner"); + println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); + */ +} diff --git a/crates/snippet/grammar/bindings/rust/lib.rs b/crates/snippet/grammar/bindings/rust/lib.rs new file mode 100644 index 0000000000..6809635413 --- /dev/null +++ b/crates/snippet/grammar/bindings/rust/lib.rs @@ -0,0 +1,52 @@ +//! This crate provides snippet language support for the [tree-sitter][] parsing library. +//! +//! Typically, you will use the [language][language func] function to add this language to a +//! tree-sitter [Parser][], and then use the parser to parse some code: +//! +//! ``` +//! let code = ""; +//! let mut parser = tree_sitter::Parser::new(); +//! parser.set_language(tree_sitter_snippet::language()).expect("Error loading snippet grammar"); +//! let tree = parser.parse(code, None).unwrap(); +//! ``` +//! +//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html +//! [language func]: fn.language.html +//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html +//! [tree-sitter]: https://tree-sitter.github.io/ + +use tree_sitter::Language; + +extern "C" { + fn tree_sitter_snippet() -> Language; +} + +/// Get the tree-sitter [Language][] for this grammar. +/// +/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html +pub fn language() -> Language { + unsafe { tree_sitter_snippet() } +} + +/// The content of the [`node-types.json`][] file for this grammar. +/// +/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types +pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json"); + +// Uncomment these to include any queries that this grammar contains + +// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm"); +// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm"); +// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm"); +// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm"); + +#[cfg(test)] +mod tests { + #[test] + fn test_can_load_grammar() { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(super::language()) + .expect("Error loading snippet language"); + } +} diff --git a/crates/snippet/grammar/grammar.js b/crates/snippet/grammar/grammar.js new file mode 100644 index 0000000000..25dedf4aa0 --- /dev/null +++ b/crates/snippet/grammar/grammar.js @@ -0,0 +1,26 @@ +module.exports = grammar({ + name: 'snippet', + + rules: { + snippet: $ => repeat1($._any), + + _any: $ => choice( + $.tabstop, + $.placeholder, + $.text + ), + + tabstop: $ => choice( + seq('$', $.int), + seq('${', $.int, '}'), + ), + + placeholder: $ => seq('${', $.int, ':', $.snippet, '}'), + + int: $ => /[0-9]+/, + + text: $ => choice($._raw_curly, $._plain_text), + _raw_curly: $ => token(prec(-1, /}+/)), + _plain_text: $ => /([^$}]|\\[$\\}])+/, + } +}) \ No newline at end of file diff --git a/crates/snippet/grammar/package.json b/crates/snippet/grammar/package.json new file mode 100644 index 0000000000..817cb1cae1 --- /dev/null +++ b/crates/snippet/grammar/package.json @@ -0,0 +1,19 @@ +{ + "name": "tree-sitter-snippet", + "version": "0.0.1", + "description": "snippet grammar for tree-sitter", + "main": "bindings/node", + "keywords": [ + "parsing", + "incremental" + ], + "dependencies": { + "nan": "^2.12.1" + }, + "devDependencies": { + "tree-sitter-cli": "^0.20.4" + }, + "scripts": { + "test": "tree-sitter test" + } +} diff --git a/crates/snippet/grammar/src/grammar.json b/crates/snippet/grammar/src/grammar.json new file mode 100644 index 0000000000..0d6aa3acf3 --- /dev/null +++ b/crates/snippet/grammar/src/grammar.json @@ -0,0 +1,133 @@ +{ + "name": "snippet", + "rules": { + "snippet": { + "type": "REPEAT1", + "content": { + "type": "SYMBOL", + "name": "_any" + } + }, + "_any": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "tabstop" + }, + { + "type": "SYMBOL", + "name": "placeholder" + }, + { + "type": "SYMBOL", + "name": "text" + } + ] + }, + "tabstop": { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "$" + }, + { + "type": "SYMBOL", + "name": "int" + } + ] + }, + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "${" + }, + { + "type": "SYMBOL", + "name": "int" + }, + { + "type": "STRING", + "value": "}" + } + ] + } + ] + }, + "placeholder": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "${" + }, + { + "type": "SYMBOL", + "name": "int" + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "SYMBOL", + "name": "snippet" + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "int": { + "type": "PATTERN", + "value": "[0-9]+" + }, + "text": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "_raw_curly" + }, + { + "type": "SYMBOL", + "name": "_plain_text" + } + ] + }, + "_raw_curly": { + "type": "TOKEN", + "content": { + "type": "PREC", + "value": -1, + "content": { + "type": "PATTERN", + "value": "}+" + } + } + }, + "_plain_text": { + "type": "PATTERN", + "value": "([^$}]|\\\\[$\\\\}])+" + } + }, + "extras": [ + { + "type": "PATTERN", + "value": "\\s" + } + ], + "conflicts": [], + "precedences": [], + "externals": [], + "inline": [], + "supertypes": [] +} + diff --git a/crates/snippet/grammar/src/node-types.json b/crates/snippet/grammar/src/node-types.json new file mode 100644 index 0000000000..ea5dde3575 --- /dev/null +++ b/crates/snippet/grammar/src/node-types.json @@ -0,0 +1,84 @@ +[ + { + "type": "placeholder", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "int", + "named": true + }, + { + "type": "snippet", + "named": true + } + ] + } + }, + { + "type": "snippet", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "placeholder", + "named": true + }, + { + "type": "tabstop", + "named": true + }, + { + "type": "text", + "named": true + } + ] + } + }, + { + "type": "tabstop", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "int", + "named": true + } + ] + } + }, + { + "type": "text", + "named": true, + "fields": {} + }, + { + "type": "$", + "named": false + }, + { + "type": "${", + "named": false + }, + { + "type": ":", + "named": false + }, + { + "type": "int", + "named": true + }, + { + "type": "}", + "named": false + } +] \ No newline at end of file diff --git a/crates/snippet/grammar/src/parser.c b/crates/snippet/grammar/src/parser.c new file mode 100644 index 0000000000..00c34b67dc --- /dev/null +++ b/crates/snippet/grammar/src/parser.c @@ -0,0 +1,545 @@ +#include + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + +#define LANGUAGE_VERSION 13 +#define STATE_COUNT 25 +#define LARGE_STATE_COUNT 8 +#define SYMBOL_COUNT 14 +#define ALIAS_COUNT 0 +#define TOKEN_COUNT 8 +#define EXTERNAL_TOKEN_COUNT 0 +#define FIELD_COUNT 0 +#define MAX_ALIAS_SEQUENCE_LENGTH 5 +#define PRODUCTION_ID_COUNT 1 + +enum { + anon_sym_DOLLAR = 1, + anon_sym_DOLLAR_LBRACE = 2, + anon_sym_RBRACE = 3, + anon_sym_COLON = 4, + sym_int = 5, + sym__raw_curly = 6, + sym__plain_text = 7, + sym_snippet = 8, + sym__any = 9, + sym_tabstop = 10, + sym_placeholder = 11, + sym_text = 12, + aux_sym_snippet_repeat1 = 13, +}; + +static const char * const ts_symbol_names[] = { + [ts_builtin_sym_end] = "end", + [anon_sym_DOLLAR] = "$", + [anon_sym_DOLLAR_LBRACE] = "${", + [anon_sym_RBRACE] = "}", + [anon_sym_COLON] = ":", + [sym_int] = "int", + [sym__raw_curly] = "_raw_curly", + [sym__plain_text] = "_plain_text", + [sym_snippet] = "snippet", + [sym__any] = "_any", + [sym_tabstop] = "tabstop", + [sym_placeholder] = "placeholder", + [sym_text] = "text", + [aux_sym_snippet_repeat1] = "snippet_repeat1", +}; + +static const TSSymbol ts_symbol_map[] = { + [ts_builtin_sym_end] = ts_builtin_sym_end, + [anon_sym_DOLLAR] = anon_sym_DOLLAR, + [anon_sym_DOLLAR_LBRACE] = anon_sym_DOLLAR_LBRACE, + [anon_sym_RBRACE] = anon_sym_RBRACE, + [anon_sym_COLON] = anon_sym_COLON, + [sym_int] = sym_int, + [sym__raw_curly] = sym__raw_curly, + [sym__plain_text] = sym__plain_text, + [sym_snippet] = sym_snippet, + [sym__any] = sym__any, + [sym_tabstop] = sym_tabstop, + [sym_placeholder] = sym_placeholder, + [sym_text] = sym_text, + [aux_sym_snippet_repeat1] = aux_sym_snippet_repeat1, +}; + +static const TSSymbolMetadata ts_symbol_metadata[] = { + [ts_builtin_sym_end] = { + .visible = false, + .named = true, + }, + [anon_sym_DOLLAR] = { + .visible = true, + .named = false, + }, + [anon_sym_DOLLAR_LBRACE] = { + .visible = true, + .named = false, + }, + [anon_sym_RBRACE] = { + .visible = true, + .named = false, + }, + [anon_sym_COLON] = { + .visible = true, + .named = false, + }, + [sym_int] = { + .visible = true, + .named = true, + }, + [sym__raw_curly] = { + .visible = false, + .named = true, + }, + [sym__plain_text] = { + .visible = false, + .named = true, + }, + [sym_snippet] = { + .visible = true, + .named = true, + }, + [sym__any] = { + .visible = false, + .named = true, + }, + [sym_tabstop] = { + .visible = true, + .named = true, + }, + [sym_placeholder] = { + .visible = true, + .named = true, + }, + [sym_text] = { + .visible = true, + .named = true, + }, + [aux_sym_snippet_repeat1] = { + .visible = false, + .named = false, + }, +}; + +static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = { + [0] = {0}, +}; + +static const uint16_t ts_non_terminal_alias_map[] = { + 0, +}; + +static bool ts_lex(TSLexer *lexer, TSStateId state) { + START_LEXER(); + eof = lexer->eof(lexer); + switch (state) { + case 0: + if (eof) ADVANCE(3); + if (lookahead == '$') ADVANCE(4); + if (lookahead == ':') ADVANCE(7); + if (lookahead == '}') ADVANCE(6); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') SKIP(0) + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(8); + END_STATE(); + case 1: + if (lookahead == '$') ADVANCE(4); + if (lookahead == '\\') ADVANCE(12); + if (lookahead == '}') ADVANCE(6); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') ADVANCE(10); + if (lookahead != 0) ADVANCE(11); + END_STATE(); + case 2: + if (eof) ADVANCE(3); + if (lookahead == '$') ADVANCE(4); + if (lookahead == '\\') ADVANCE(12); + if (lookahead == '}') ADVANCE(9); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') ADVANCE(10); + if (lookahead != 0) ADVANCE(11); + END_STATE(); + case 3: + ACCEPT_TOKEN(ts_builtin_sym_end); + END_STATE(); + case 4: + ACCEPT_TOKEN(anon_sym_DOLLAR); + if (lookahead == '{') ADVANCE(5); + END_STATE(); + case 5: + ACCEPT_TOKEN(anon_sym_DOLLAR_LBRACE); + END_STATE(); + case 6: + ACCEPT_TOKEN(anon_sym_RBRACE); + END_STATE(); + case 7: + ACCEPT_TOKEN(anon_sym_COLON); + END_STATE(); + case 8: + ACCEPT_TOKEN(sym_int); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(8); + END_STATE(); + case 9: + ACCEPT_TOKEN(sym__raw_curly); + if (lookahead == '}') ADVANCE(9); + END_STATE(); + case 10: + ACCEPT_TOKEN(sym__plain_text); + if (lookahead == '\\') ADVANCE(12); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') ADVANCE(10); + if (lookahead != 0 && + lookahead != '$' && + lookahead != '}') ADVANCE(11); + END_STATE(); + case 11: + ACCEPT_TOKEN(sym__plain_text); + if (lookahead == '\\') ADVANCE(12); + if (lookahead != 0 && + lookahead != '$' && + lookahead != '}') ADVANCE(11); + END_STATE(); + case 12: + ACCEPT_TOKEN(sym__plain_text); + if (lookahead == '\\') ADVANCE(12); + if (lookahead != 0) ADVANCE(11); + END_STATE(); + default: + return false; + } +} + +static const TSLexMode ts_lex_modes[STATE_COUNT] = { + [0] = {.lex_state = 0}, + [1] = {.lex_state = 2}, + [2] = {.lex_state = 2}, + [3] = {.lex_state = 2}, + [4] = {.lex_state = 1}, + [5] = {.lex_state = 1}, + [6] = {.lex_state = 2}, + [7] = {.lex_state = 2}, + [8] = {.lex_state = 1}, + [9] = {.lex_state = 2}, + [10] = {.lex_state = 2}, + [11] = {.lex_state = 2}, + [12] = {.lex_state = 1}, + [13] = {.lex_state = 1}, + [14] = {.lex_state = 2}, + [15] = {.lex_state = 1}, + [16] = {.lex_state = 0}, + [17] = {.lex_state = 0}, + [18] = {.lex_state = 0}, + [19] = {.lex_state = 0}, + [20] = {.lex_state = 0}, + [21] = {.lex_state = 0}, + [22] = {.lex_state = 0}, + [23] = {.lex_state = 0}, + [24] = {.lex_state = 0}, +}; + +static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { + [0] = { + [ts_builtin_sym_end] = ACTIONS(1), + [anon_sym_DOLLAR] = ACTIONS(1), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(1), + [anon_sym_RBRACE] = ACTIONS(1), + [anon_sym_COLON] = ACTIONS(1), + [sym_int] = ACTIONS(1), + [sym__raw_curly] = ACTIONS(1), + }, + [1] = { + [sym_snippet] = STATE(20), + [sym__any] = STATE(6), + [sym_tabstop] = STATE(6), + [sym_placeholder] = STATE(6), + [sym_text] = STATE(6), + [aux_sym_snippet_repeat1] = STATE(6), + [anon_sym_DOLLAR] = ACTIONS(3), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(5), + [sym__raw_curly] = ACTIONS(7), + [sym__plain_text] = ACTIONS(9), + }, + [2] = { + [sym_snippet] = STATE(18), + [sym__any] = STATE(5), + [sym_tabstop] = STATE(5), + [sym_placeholder] = STATE(5), + [sym_text] = STATE(5), + [aux_sym_snippet_repeat1] = STATE(5), + [anon_sym_DOLLAR] = ACTIONS(11), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(13), + [sym__raw_curly] = ACTIONS(15), + [sym__plain_text] = ACTIONS(17), + }, + [3] = { + [sym_snippet] = STATE(22), + [sym__any] = STATE(5), + [sym_tabstop] = STATE(5), + [sym_placeholder] = STATE(5), + [sym_text] = STATE(5), + [aux_sym_snippet_repeat1] = STATE(5), + [anon_sym_DOLLAR] = ACTIONS(11), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(13), + [sym__raw_curly] = ACTIONS(15), + [sym__plain_text] = ACTIONS(17), + }, + [4] = { + [sym__any] = STATE(4), + [sym_tabstop] = STATE(4), + [sym_placeholder] = STATE(4), + [sym_text] = STATE(4), + [aux_sym_snippet_repeat1] = STATE(4), + [anon_sym_DOLLAR] = ACTIONS(19), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(22), + [anon_sym_RBRACE] = ACTIONS(25), + [sym__raw_curly] = ACTIONS(27), + [sym__plain_text] = ACTIONS(30), + }, + [5] = { + [sym__any] = STATE(4), + [sym_tabstop] = STATE(4), + [sym_placeholder] = STATE(4), + [sym_text] = STATE(4), + [aux_sym_snippet_repeat1] = STATE(4), + [anon_sym_DOLLAR] = ACTIONS(11), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(13), + [anon_sym_RBRACE] = ACTIONS(33), + [sym__raw_curly] = ACTIONS(15), + [sym__plain_text] = ACTIONS(17), + }, + [6] = { + [sym__any] = STATE(7), + [sym_tabstop] = STATE(7), + [sym_placeholder] = STATE(7), + [sym_text] = STATE(7), + [aux_sym_snippet_repeat1] = STATE(7), + [ts_builtin_sym_end] = ACTIONS(35), + [anon_sym_DOLLAR] = ACTIONS(3), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(5), + [sym__raw_curly] = ACTIONS(7), + [sym__plain_text] = ACTIONS(9), + }, + [7] = { + [sym__any] = STATE(7), + [sym_tabstop] = STATE(7), + [sym_placeholder] = STATE(7), + [sym_text] = STATE(7), + [aux_sym_snippet_repeat1] = STATE(7), + [ts_builtin_sym_end] = ACTIONS(37), + [anon_sym_DOLLAR] = ACTIONS(39), + [anon_sym_DOLLAR_LBRACE] = ACTIONS(42), + [sym__raw_curly] = ACTIONS(45), + [sym__plain_text] = ACTIONS(48), + }, +}; + +static const uint16_t ts_small_parse_table[] = { + [0] = 2, + ACTIONS(53), 1, + sym__plain_text, + ACTIONS(51), 4, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + anon_sym_RBRACE, + sym__raw_curly, + [10] = 2, + ACTIONS(55), 2, + ts_builtin_sym_end, + sym__plain_text, + ACTIONS(57), 3, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + sym__raw_curly, + [20] = 2, + ACTIONS(53), 2, + ts_builtin_sym_end, + sym__plain_text, + ACTIONS(51), 3, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + sym__raw_curly, + [30] = 2, + ACTIONS(59), 2, + ts_builtin_sym_end, + sym__plain_text, + ACTIONS(61), 3, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + sym__raw_curly, + [40] = 2, + ACTIONS(65), 1, + sym__plain_text, + ACTIONS(63), 4, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + anon_sym_RBRACE, + sym__raw_curly, + [50] = 2, + ACTIONS(55), 1, + sym__plain_text, + ACTIONS(57), 4, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + anon_sym_RBRACE, + sym__raw_curly, + [60] = 2, + ACTIONS(65), 2, + ts_builtin_sym_end, + sym__plain_text, + ACTIONS(63), 3, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + sym__raw_curly, + [70] = 2, + ACTIONS(59), 1, + sym__plain_text, + ACTIONS(61), 4, + anon_sym_DOLLAR, + anon_sym_DOLLAR_LBRACE, + anon_sym_RBRACE, + sym__raw_curly, + [80] = 2, + ACTIONS(67), 1, + anon_sym_RBRACE, + ACTIONS(69), 1, + anon_sym_COLON, + [87] = 2, + ACTIONS(71), 1, + anon_sym_RBRACE, + ACTIONS(73), 1, + anon_sym_COLON, + [94] = 1, + ACTIONS(75), 1, + anon_sym_RBRACE, + [98] = 1, + ACTIONS(77), 1, + sym_int, + [102] = 1, + ACTIONS(79), 1, + ts_builtin_sym_end, + [106] = 1, + ACTIONS(81), 1, + sym_int, + [110] = 1, + ACTIONS(83), 1, + anon_sym_RBRACE, + [114] = 1, + ACTIONS(85), 1, + sym_int, + [118] = 1, + ACTIONS(87), 1, + sym_int, +}; + +static const uint32_t ts_small_parse_table_map[] = { + [SMALL_STATE(8)] = 0, + [SMALL_STATE(9)] = 10, + [SMALL_STATE(10)] = 20, + [SMALL_STATE(11)] = 30, + [SMALL_STATE(12)] = 40, + [SMALL_STATE(13)] = 50, + [SMALL_STATE(14)] = 60, + [SMALL_STATE(15)] = 70, + [SMALL_STATE(16)] = 80, + [SMALL_STATE(17)] = 87, + [SMALL_STATE(18)] = 94, + [SMALL_STATE(19)] = 98, + [SMALL_STATE(20)] = 102, + [SMALL_STATE(21)] = 106, + [SMALL_STATE(22)] = 110, + [SMALL_STATE(23)] = 114, + [SMALL_STATE(24)] = 118, +}; + +static const TSParseActionEntry ts_parse_actions[] = { + [0] = {.entry = {.count = 0, .reusable = false}}, + [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(), + [3] = {.entry = {.count = 1, .reusable = false}}, SHIFT(19), + [5] = {.entry = {.count = 1, .reusable = false}}, SHIFT(24), + [7] = {.entry = {.count = 1, .reusable = false}}, SHIFT(14), + [9] = {.entry = {.count = 1, .reusable = true}}, SHIFT(14), + [11] = {.entry = {.count = 1, .reusable = false}}, SHIFT(21), + [13] = {.entry = {.count = 1, .reusable = false}}, SHIFT(23), + [15] = {.entry = {.count = 1, .reusable = false}}, SHIFT(12), + [17] = {.entry = {.count = 1, .reusable = true}}, SHIFT(12), + [19] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(21), + [22] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(23), + [25] = {.entry = {.count = 1, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), + [27] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(12), + [30] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(12), + [33] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_snippet, 1), + [35] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_snippet, 1), + [37] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_snippet_repeat1, 2), + [39] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(19), + [42] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(24), + [45] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(14), + [48] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(14), + [51] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_tabstop, 3), + [53] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_tabstop, 3), + [55] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_tabstop, 2), + [57] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_tabstop, 2), + [59] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_placeholder, 5), + [61] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_placeholder, 5), + [63] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_text, 1), + [65] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_text, 1), + [67] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10), + [69] = {.entry = {.count = 1, .reusable = true}}, SHIFT(2), + [71] = {.entry = {.count = 1, .reusable = true}}, SHIFT(8), + [73] = {.entry = {.count = 1, .reusable = true}}, SHIFT(3), + [75] = {.entry = {.count = 1, .reusable = true}}, SHIFT(11), + [77] = {.entry = {.count = 1, .reusable = true}}, SHIFT(9), + [79] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(), + [81] = {.entry = {.count = 1, .reusable = true}}, SHIFT(13), + [83] = {.entry = {.count = 1, .reusable = true}}, SHIFT(15), + [85] = {.entry = {.count = 1, .reusable = true}}, SHIFT(17), + [87] = {.entry = {.count = 1, .reusable = true}}, SHIFT(16), +}; + +#ifdef __cplusplus +extern "C" { +#endif +#ifdef _WIN32 +#define extern __declspec(dllexport) +#endif + +extern const TSLanguage *tree_sitter_snippet(void) { + static const TSLanguage language = { + .version = LANGUAGE_VERSION, + .symbol_count = SYMBOL_COUNT, + .alias_count = ALIAS_COUNT, + .token_count = TOKEN_COUNT, + .external_token_count = EXTERNAL_TOKEN_COUNT, + .state_count = STATE_COUNT, + .large_state_count = LARGE_STATE_COUNT, + .production_id_count = PRODUCTION_ID_COUNT, + .field_count = FIELD_COUNT, + .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH, + .parse_table = &ts_parse_table[0][0], + .small_parse_table = ts_small_parse_table, + .small_parse_table_map = ts_small_parse_table_map, + .parse_actions = ts_parse_actions, + .symbol_names = ts_symbol_names, + .symbol_metadata = ts_symbol_metadata, + .public_symbol_map = ts_symbol_map, + .alias_map = ts_non_terminal_alias_map, + .alias_sequences = &ts_alias_sequences[0][0], + .lex_modes = ts_lex_modes, + .lex_fn = ts_lex, + }; + return &language; +} +#ifdef __cplusplus +} +#endif diff --git a/crates/snippet/grammar/src/tree_sitter/parser.h b/crates/snippet/grammar/src/tree_sitter/parser.h new file mode 100644 index 0000000000..2b14ac1046 --- /dev/null +++ b/crates/snippet/grammar/src/tree_sitter/parser.h @@ -0,0 +1,224 @@ +#ifndef TREE_SITTER_PARSER_H_ +#define TREE_SITTER_PARSER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define ts_builtin_sym_error ((TSSymbol)-1) +#define ts_builtin_sym_end 0 +#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 + +typedef uint16_t TSStateId; + +#ifndef TREE_SITTER_API_H_ +typedef uint16_t TSSymbol; +typedef uint16_t TSFieldId; +typedef struct TSLanguage TSLanguage; +#endif + +typedef struct { + TSFieldId field_id; + uint8_t child_index; + bool inherited; +} TSFieldMapEntry; + +typedef struct { + uint16_t index; + uint16_t length; +} TSFieldMapSlice; + +typedef struct { + bool visible; + bool named; + bool supertype; +} TSSymbolMetadata; + +typedef struct TSLexer TSLexer; + +struct TSLexer { + int32_t lookahead; + TSSymbol result_symbol; + void (*advance)(TSLexer *, bool); + void (*mark_end)(TSLexer *); + uint32_t (*get_column)(TSLexer *); + bool (*is_at_included_range_start)(const TSLexer *); + bool (*eof)(const TSLexer *); +}; + +typedef enum { + TSParseActionTypeShift, + TSParseActionTypeReduce, + TSParseActionTypeAccept, + TSParseActionTypeRecover, +} TSParseActionType; + +typedef union { + struct { + uint8_t type; + TSStateId state; + bool extra; + bool repetition; + } shift; + struct { + uint8_t type; + uint8_t child_count; + TSSymbol symbol; + int16_t dynamic_precedence; + uint16_t production_id; + } reduce; + uint8_t type; +} TSParseAction; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; +} TSLexMode; + +typedef union { + TSParseAction action; + struct { + uint8_t count; + bool reusable; + } entry; +} TSParseActionEntry; + +struct TSLanguage { + uint32_t version; + uint32_t symbol_count; + uint32_t alias_count; + uint32_t token_count; + uint32_t external_token_count; + uint32_t state_count; + uint32_t large_state_count; + uint32_t production_id_count; + uint32_t field_count; + uint16_t max_alias_sequence_length; + const uint16_t *parse_table; + const uint16_t *small_parse_table; + const uint32_t *small_parse_table_map; + const TSParseActionEntry *parse_actions; + const char * const *symbol_names; + const char * const *field_names; + const TSFieldMapSlice *field_map_slices; + const TSFieldMapEntry *field_map_entries; + const TSSymbolMetadata *symbol_metadata; + const TSSymbol *public_symbol_map; + const uint16_t *alias_map; + const TSSymbol *alias_sequences; + const TSLexMode *lex_modes; + bool (*lex_fn)(TSLexer *, TSStateId); + bool (*keyword_lex_fn)(TSLexer *, TSStateId); + TSSymbol keyword_capture_token; + struct { + const bool *states; + const TSSymbol *symbol_map; + void *(*create)(void); + void (*destroy)(void *); + bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); + unsigned (*serialize)(void *, char *); + void (*deserialize)(void *, const char *, unsigned); + } external_scanner; + const TSStateId *primary_state_ids; +}; + +/* + * Lexer Macros + */ + +#define START_LEXER() \ + bool result = false; \ + bool skip = false; \ + bool eof = false; \ + int32_t lookahead; \ + goto start; \ + next_state: \ + lexer->advance(lexer, skip); \ + start: \ + skip = false; \ + lookahead = lexer->lookahead; + +#define ADVANCE(state_value) \ + { \ + state = state_value; \ + goto next_state; \ + } + +#define SKIP(state_value) \ + { \ + skip = true; \ + state = state_value; \ + goto next_state; \ + } + +#define ACCEPT_TOKEN(symbol_value) \ + result = true; \ + lexer->result_symbol = symbol_value; \ + lexer->mark_end(lexer); + +#define END_STATE() return result; + +/* + * Parse Table Macros + */ + +#define SMALL_STATE(id) id - LARGE_STATE_COUNT + +#define STATE(id) id + +#define ACTIONS(id) id + +#define SHIFT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = state_value \ + } \ + }} + +#define SHIFT_REPEAT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = state_value, \ + .repetition = true \ + } \ + }} + +#define SHIFT_EXTRA() \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .extra = true \ + } \ + }} + +#define REDUCE(symbol_val, child_count_val, ...) \ + {{ \ + .reduce = { \ + .type = TSParseActionTypeReduce, \ + .symbol = symbol_val, \ + .child_count = child_count_val, \ + __VA_ARGS__ \ + }, \ + }} + +#define RECOVER() \ + {{ \ + .type = TSParseActionTypeRecover \ + }} + +#define ACCEPT_INPUT() \ + {{ \ + .type = TSParseActionTypeAccept \ + }} + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_PARSER_H_ diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs new file mode 100644 index 0000000000..51d58aadeb --- /dev/null +++ b/crates/snippet/src/snippet.rs @@ -0,0 +1,139 @@ +use anyhow::{anyhow, Result}; +use smallvec::SmallVec; +use std::{collections::BTreeMap, ops::Range}; +use tree_sitter::{Parser, TreeCursor}; + +#[derive(Default)] +pub struct Snippet { + pub text: String, + pub tabstops: Vec; 2]>>, +} + +impl Snippet { + pub fn parse(source: &str) -> Result { + let mut parser = Parser::new(); + parser + .set_language(tree_sitter_snippet::language()) + .unwrap(); + + let tree = parser.parse(source, None).unwrap(); + if tree.root_node().has_error() { + return Err(anyhow!("invalid snippet")); + } + + let mut text = String::new(); + let mut tabstops = BTreeMap::new(); + let mut cursor = tree.root_node().walk(); + parse_snippet_node(&mut cursor, &mut text, &mut tabstops, source)?; + + Ok(Snippet { + text, + tabstops: tabstops.into_values().collect(), + }) + } +} + +fn parse_snippet_node( + cursor: &mut TreeCursor, + text: &mut String, + tabstops: &mut BTreeMap; 2]>>, + source: &str, +) -> Result<()> { + cursor.goto_first_child(); + loop { + let node = cursor.node(); + match node.kind() { + "text" => text.push_str(&source[node.byte_range()]), + "tabstop" => { + if let Some(int_node) = node.named_child(0) { + let index = source[int_node.byte_range()].parse::()?; + tabstops + .entry(index) + .or_insert(SmallVec::new()) + .push(text.len()..text.len()); + } + } + "placeholder" => { + cursor.goto_first_child(); + cursor.goto_next_sibling(); + let int_node = cursor.node(); + let index = source[int_node.byte_range()].parse::()?; + + cursor.goto_next_sibling(); + cursor.goto_next_sibling(); + let range_start = text.len(); + + parse_snippet_node(cursor, text, tabstops, source)?; + tabstops + .entry(index) + .or_insert(SmallVec::new()) + .push(range_start..text.len()); + + cursor.goto_parent(); + } + _ => {} + } + + if !cursor.goto_next_sibling() { + break; + } + } + cursor.goto_parent(); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_snippet_with_tabstops() { + let snippet = Snippet::parse("one$1two").unwrap(); + assert_eq!(snippet.text, "onetwo"); + assert_eq!( + snippet + .tabstops + .iter() + .map(SmallVec::as_slice) + .collect::>(), + &[vec![3..3]] + ); + } + + #[test] + fn test_parse_snippet_with_placeholders() { + let snippet = Snippet::parse("one${1:two}three").unwrap(); + assert_eq!(snippet.text, "onetwothree"); + assert_eq!( + snippet + .tabstops + .iter() + .map(SmallVec::as_slice) + .collect::>(), + &[vec![3..6]] + ); + } + + #[test] + fn test_parse_snippet_with_nested_placeholders() { + let snippet = Snippet::parse( + "for (${1:var ${2:i} = 0; ${2:i} < ${3:${4:array}.length}; ${2:i}++}) {$5}", + ) + .unwrap(); + assert_eq!(snippet.text, "for (var i = 0; i < array.length; i++) {}"); + assert_eq!( + snippet + .tabstops + .iter() + .map(SmallVec::as_slice) + .collect::>(), + &[ + vec![5..37], + vec![9..10, 16..17, 34..35], + vec![20..32], + vec![20..25], + vec![40..40], + ] + ); + } +} From 55cc7bb8681145fe4feb47cd73119c1d52f5c6f4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Feb 2022 13:41:41 -0800 Subject: [PATCH 25/61] Reimplement snippet parsing using recursive descent --- Cargo.lock | 10 - crates/snippet/Cargo.toml | 2 - crates/snippet/grammar/Cargo.toml | 26 - crates/snippet/grammar/binding.gyp | 19 - .../snippet/grammar/bindings/node/binding.cc | 28 - crates/snippet/grammar/bindings/node/index.js | 19 - crates/snippet/grammar/bindings/rust/build.rs | 40 -- crates/snippet/grammar/bindings/rust/lib.rs | 52 -- crates/snippet/grammar/grammar.js | 26 - crates/snippet/grammar/package.json | 19 - crates/snippet/grammar/src/grammar.json | 133 ----- crates/snippet/grammar/src/node-types.json | 84 --- crates/snippet/grammar/src/parser.c | 545 ------------------ .../snippet/grammar/src/tree_sitter/parser.h | 224 ------- crates/snippet/src/snippet.rs | 152 +++-- 15 files changed, 90 insertions(+), 1289 deletions(-) delete mode 100644 crates/snippet/grammar/Cargo.toml delete mode 100644 crates/snippet/grammar/binding.gyp delete mode 100644 crates/snippet/grammar/bindings/node/binding.cc delete mode 100644 crates/snippet/grammar/bindings/node/index.js delete mode 100644 crates/snippet/grammar/bindings/rust/build.rs delete mode 100644 crates/snippet/grammar/bindings/rust/lib.rs delete mode 100644 crates/snippet/grammar/grammar.js delete mode 100644 crates/snippet/grammar/package.json delete mode 100644 crates/snippet/grammar/src/grammar.json delete mode 100644 crates/snippet/grammar/src/node-types.json delete mode 100644 crates/snippet/grammar/src/parser.c delete mode 100644 crates/snippet/grammar/src/tree_sitter/parser.h diff --git a/Cargo.lock b/Cargo.lock index 53d7651e32..8f4ebb8b89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4424,8 +4424,6 @@ version = "0.1.0" dependencies = [ "anyhow", "smallvec", - "tree-sitter", - "tree-sitter-snippet", ] [[package]] @@ -5213,14 +5211,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "tree-sitter-snippet" -version = "0.0.1" -dependencies = [ - "cc", - "tree-sitter", -] - [[package]] name = "ttf-parser" version = "0.9.0" diff --git a/crates/snippet/Cargo.toml b/crates/snippet/Cargo.toml index daba92f848..b3712cd6a0 100644 --- a/crates/snippet/Cargo.toml +++ b/crates/snippet/Cargo.toml @@ -9,5 +9,3 @@ path = "src/snippet.rs" [dependencies] anyhow = "1.0" smallvec = { version = "1.6", features = ["union"] } -tree-sitter = "0.20" -tree-sitter-snippet = { path = "./grammar" } diff --git a/crates/snippet/grammar/Cargo.toml b/crates/snippet/grammar/Cargo.toml deleted file mode 100644 index 83defad879..0000000000 --- a/crates/snippet/grammar/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "tree-sitter-snippet" -description = "snippet grammar for the tree-sitter parsing library" -version = "0.0.1" -keywords = ["incremental", "parsing", "snippet"] -categories = ["parsing", "text-editors"] -repository = "https://github.com/tree-sitter/tree-sitter-snippet" -edition = "2018" -license = "MIT" - -build = "bindings/rust/build.rs" -include = [ - "bindings/rust/*", - "grammar.js", - "queries/*", - "src/*", -] - -[lib] -path = "bindings/rust/lib.rs" - -[dependencies] -tree-sitter = "~0.20" - -[build-dependencies] -cc = "1.0" diff --git a/crates/snippet/grammar/binding.gyp b/crates/snippet/grammar/binding.gyp deleted file mode 100644 index a99fa70f98..0000000000 --- a/crates/snippet/grammar/binding.gyp +++ /dev/null @@ -1,19 +0,0 @@ -{ - "targets": [ - { - "target_name": "tree_sitter_snippet_binding", - "include_dirs": [ - " -#include "nan.h" - -using namespace v8; - -extern "C" TSLanguage * tree_sitter_snippet(); - -namespace { - -NAN_METHOD(New) {} - -void Init(Local exports, Local module) { - Local tpl = Nan::New(New); - tpl->SetClassName(Nan::New("Language").ToLocalChecked()); - tpl->InstanceTemplate()->SetInternalFieldCount(1); - - Local constructor = Nan::GetFunction(tpl).ToLocalChecked(); - Local instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked(); - Nan::SetInternalFieldPointer(instance, 0, tree_sitter_snippet()); - - Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("snippet").ToLocalChecked()); - Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance); -} - -NODE_MODULE(tree_sitter_snippet_binding, Init) - -} // namespace diff --git a/crates/snippet/grammar/bindings/node/index.js b/crates/snippet/grammar/bindings/node/index.js deleted file mode 100644 index c5ea00c7af..0000000000 --- a/crates/snippet/grammar/bindings/node/index.js +++ /dev/null @@ -1,19 +0,0 @@ -try { - module.exports = require("../../build/Release/tree_sitter_snippet_binding"); -} catch (error1) { - if (error1.code !== 'MODULE_NOT_FOUND') { - throw error1; - } - try { - module.exports = require("../../build/Debug/tree_sitter_snippet_binding"); - } catch (error2) { - if (error2.code !== 'MODULE_NOT_FOUND') { - throw error2; - } - throw error1 - } -} - -try { - module.exports.nodeTypeInfo = require("../../src/node-types.json"); -} catch (_) {} diff --git a/crates/snippet/grammar/bindings/rust/build.rs b/crates/snippet/grammar/bindings/rust/build.rs deleted file mode 100644 index c6061f0995..0000000000 --- a/crates/snippet/grammar/bindings/rust/build.rs +++ /dev/null @@ -1,40 +0,0 @@ -fn main() { - let src_dir = std::path::Path::new("src"); - - let mut c_config = cc::Build::new(); - c_config.include(&src_dir); - c_config - .flag_if_supported("-Wno-unused-parameter") - .flag_if_supported("-Wno-unused-but-set-variable") - .flag_if_supported("-Wno-trigraphs"); - let parser_path = src_dir.join("parser.c"); - c_config.file(&parser_path); - - // If your language uses an external scanner written in C, - // then include this block of code: - - /* - let scanner_path = src_dir.join("scanner.c"); - c_config.file(&scanner_path); - println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); - */ - - c_config.compile("parser"); - println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap()); - - // If your language uses an external scanner written in C++, - // then include this block of code: - - /* - let mut cpp_config = cc::Build::new(); - cpp_config.cpp(true); - cpp_config.include(&src_dir); - cpp_config - .flag_if_supported("-Wno-unused-parameter") - .flag_if_supported("-Wno-unused-but-set-variable"); - let scanner_path = src_dir.join("scanner.cc"); - cpp_config.file(&scanner_path); - cpp_config.compile("scanner"); - println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); - */ -} diff --git a/crates/snippet/grammar/bindings/rust/lib.rs b/crates/snippet/grammar/bindings/rust/lib.rs deleted file mode 100644 index 6809635413..0000000000 --- a/crates/snippet/grammar/bindings/rust/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! This crate provides snippet language support for the [tree-sitter][] parsing library. -//! -//! Typically, you will use the [language][language func] function to add this language to a -//! tree-sitter [Parser][], and then use the parser to parse some code: -//! -//! ``` -//! let code = ""; -//! let mut parser = tree_sitter::Parser::new(); -//! parser.set_language(tree_sitter_snippet::language()).expect("Error loading snippet grammar"); -//! let tree = parser.parse(code, None).unwrap(); -//! ``` -//! -//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html -//! [language func]: fn.language.html -//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html -//! [tree-sitter]: https://tree-sitter.github.io/ - -use tree_sitter::Language; - -extern "C" { - fn tree_sitter_snippet() -> Language; -} - -/// Get the tree-sitter [Language][] for this grammar. -/// -/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html -pub fn language() -> Language { - unsafe { tree_sitter_snippet() } -} - -/// The content of the [`node-types.json`][] file for this grammar. -/// -/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types -pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json"); - -// Uncomment these to include any queries that this grammar contains - -// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm"); -// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm"); -// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm"); -// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm"); - -#[cfg(test)] -mod tests { - #[test] - fn test_can_load_grammar() { - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(super::language()) - .expect("Error loading snippet language"); - } -} diff --git a/crates/snippet/grammar/grammar.js b/crates/snippet/grammar/grammar.js deleted file mode 100644 index 25dedf4aa0..0000000000 --- a/crates/snippet/grammar/grammar.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = grammar({ - name: 'snippet', - - rules: { - snippet: $ => repeat1($._any), - - _any: $ => choice( - $.tabstop, - $.placeholder, - $.text - ), - - tabstop: $ => choice( - seq('$', $.int), - seq('${', $.int, '}'), - ), - - placeholder: $ => seq('${', $.int, ':', $.snippet, '}'), - - int: $ => /[0-9]+/, - - text: $ => choice($._raw_curly, $._plain_text), - _raw_curly: $ => token(prec(-1, /}+/)), - _plain_text: $ => /([^$}]|\\[$\\}])+/, - } -}) \ No newline at end of file diff --git a/crates/snippet/grammar/package.json b/crates/snippet/grammar/package.json deleted file mode 100644 index 817cb1cae1..0000000000 --- a/crates/snippet/grammar/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "tree-sitter-snippet", - "version": "0.0.1", - "description": "snippet grammar for tree-sitter", - "main": "bindings/node", - "keywords": [ - "parsing", - "incremental" - ], - "dependencies": { - "nan": "^2.12.1" - }, - "devDependencies": { - "tree-sitter-cli": "^0.20.4" - }, - "scripts": { - "test": "tree-sitter test" - } -} diff --git a/crates/snippet/grammar/src/grammar.json b/crates/snippet/grammar/src/grammar.json deleted file mode 100644 index 0d6aa3acf3..0000000000 --- a/crates/snippet/grammar/src/grammar.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "name": "snippet", - "rules": { - "snippet": { - "type": "REPEAT1", - "content": { - "type": "SYMBOL", - "name": "_any" - } - }, - "_any": { - "type": "CHOICE", - "members": [ - { - "type": "SYMBOL", - "name": "tabstop" - }, - { - "type": "SYMBOL", - "name": "placeholder" - }, - { - "type": "SYMBOL", - "name": "text" - } - ] - }, - "tabstop": { - "type": "CHOICE", - "members": [ - { - "type": "SEQ", - "members": [ - { - "type": "STRING", - "value": "$" - }, - { - "type": "SYMBOL", - "name": "int" - } - ] - }, - { - "type": "SEQ", - "members": [ - { - "type": "STRING", - "value": "${" - }, - { - "type": "SYMBOL", - "name": "int" - }, - { - "type": "STRING", - "value": "}" - } - ] - } - ] - }, - "placeholder": { - "type": "SEQ", - "members": [ - { - "type": "STRING", - "value": "${" - }, - { - "type": "SYMBOL", - "name": "int" - }, - { - "type": "STRING", - "value": ":" - }, - { - "type": "SYMBOL", - "name": "snippet" - }, - { - "type": "STRING", - "value": "}" - } - ] - }, - "int": { - "type": "PATTERN", - "value": "[0-9]+" - }, - "text": { - "type": "CHOICE", - "members": [ - { - "type": "SYMBOL", - "name": "_raw_curly" - }, - { - "type": "SYMBOL", - "name": "_plain_text" - } - ] - }, - "_raw_curly": { - "type": "TOKEN", - "content": { - "type": "PREC", - "value": -1, - "content": { - "type": "PATTERN", - "value": "}+" - } - } - }, - "_plain_text": { - "type": "PATTERN", - "value": "([^$}]|\\\\[$\\\\}])+" - } - }, - "extras": [ - { - "type": "PATTERN", - "value": "\\s" - } - ], - "conflicts": [], - "precedences": [], - "externals": [], - "inline": [], - "supertypes": [] -} - diff --git a/crates/snippet/grammar/src/node-types.json b/crates/snippet/grammar/src/node-types.json deleted file mode 100644 index ea5dde3575..0000000000 --- a/crates/snippet/grammar/src/node-types.json +++ /dev/null @@ -1,84 +0,0 @@ -[ - { - "type": "placeholder", - "named": true, - "fields": {}, - "children": { - "multiple": true, - "required": true, - "types": [ - { - "type": "int", - "named": true - }, - { - "type": "snippet", - "named": true - } - ] - } - }, - { - "type": "snippet", - "named": true, - "fields": {}, - "children": { - "multiple": true, - "required": true, - "types": [ - { - "type": "placeholder", - "named": true - }, - { - "type": "tabstop", - "named": true - }, - { - "type": "text", - "named": true - } - ] - } - }, - { - "type": "tabstop", - "named": true, - "fields": {}, - "children": { - "multiple": false, - "required": true, - "types": [ - { - "type": "int", - "named": true - } - ] - } - }, - { - "type": "text", - "named": true, - "fields": {} - }, - { - "type": "$", - "named": false - }, - { - "type": "${", - "named": false - }, - { - "type": ":", - "named": false - }, - { - "type": "int", - "named": true - }, - { - "type": "}", - "named": false - } -] \ No newline at end of file diff --git a/crates/snippet/grammar/src/parser.c b/crates/snippet/grammar/src/parser.c deleted file mode 100644 index 00c34b67dc..0000000000 --- a/crates/snippet/grammar/src/parser.c +++ /dev/null @@ -1,545 +0,0 @@ -#include - -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#endif - -#define LANGUAGE_VERSION 13 -#define STATE_COUNT 25 -#define LARGE_STATE_COUNT 8 -#define SYMBOL_COUNT 14 -#define ALIAS_COUNT 0 -#define TOKEN_COUNT 8 -#define EXTERNAL_TOKEN_COUNT 0 -#define FIELD_COUNT 0 -#define MAX_ALIAS_SEQUENCE_LENGTH 5 -#define PRODUCTION_ID_COUNT 1 - -enum { - anon_sym_DOLLAR = 1, - anon_sym_DOLLAR_LBRACE = 2, - anon_sym_RBRACE = 3, - anon_sym_COLON = 4, - sym_int = 5, - sym__raw_curly = 6, - sym__plain_text = 7, - sym_snippet = 8, - sym__any = 9, - sym_tabstop = 10, - sym_placeholder = 11, - sym_text = 12, - aux_sym_snippet_repeat1 = 13, -}; - -static const char * const ts_symbol_names[] = { - [ts_builtin_sym_end] = "end", - [anon_sym_DOLLAR] = "$", - [anon_sym_DOLLAR_LBRACE] = "${", - [anon_sym_RBRACE] = "}", - [anon_sym_COLON] = ":", - [sym_int] = "int", - [sym__raw_curly] = "_raw_curly", - [sym__plain_text] = "_plain_text", - [sym_snippet] = "snippet", - [sym__any] = "_any", - [sym_tabstop] = "tabstop", - [sym_placeholder] = "placeholder", - [sym_text] = "text", - [aux_sym_snippet_repeat1] = "snippet_repeat1", -}; - -static const TSSymbol ts_symbol_map[] = { - [ts_builtin_sym_end] = ts_builtin_sym_end, - [anon_sym_DOLLAR] = anon_sym_DOLLAR, - [anon_sym_DOLLAR_LBRACE] = anon_sym_DOLLAR_LBRACE, - [anon_sym_RBRACE] = anon_sym_RBRACE, - [anon_sym_COLON] = anon_sym_COLON, - [sym_int] = sym_int, - [sym__raw_curly] = sym__raw_curly, - [sym__plain_text] = sym__plain_text, - [sym_snippet] = sym_snippet, - [sym__any] = sym__any, - [sym_tabstop] = sym_tabstop, - [sym_placeholder] = sym_placeholder, - [sym_text] = sym_text, - [aux_sym_snippet_repeat1] = aux_sym_snippet_repeat1, -}; - -static const TSSymbolMetadata ts_symbol_metadata[] = { - [ts_builtin_sym_end] = { - .visible = false, - .named = true, - }, - [anon_sym_DOLLAR] = { - .visible = true, - .named = false, - }, - [anon_sym_DOLLAR_LBRACE] = { - .visible = true, - .named = false, - }, - [anon_sym_RBRACE] = { - .visible = true, - .named = false, - }, - [anon_sym_COLON] = { - .visible = true, - .named = false, - }, - [sym_int] = { - .visible = true, - .named = true, - }, - [sym__raw_curly] = { - .visible = false, - .named = true, - }, - [sym__plain_text] = { - .visible = false, - .named = true, - }, - [sym_snippet] = { - .visible = true, - .named = true, - }, - [sym__any] = { - .visible = false, - .named = true, - }, - [sym_tabstop] = { - .visible = true, - .named = true, - }, - [sym_placeholder] = { - .visible = true, - .named = true, - }, - [sym_text] = { - .visible = true, - .named = true, - }, - [aux_sym_snippet_repeat1] = { - .visible = false, - .named = false, - }, -}; - -static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = { - [0] = {0}, -}; - -static const uint16_t ts_non_terminal_alias_map[] = { - 0, -}; - -static bool ts_lex(TSLexer *lexer, TSStateId state) { - START_LEXER(); - eof = lexer->eof(lexer); - switch (state) { - case 0: - if (eof) ADVANCE(3); - if (lookahead == '$') ADVANCE(4); - if (lookahead == ':') ADVANCE(7); - if (lookahead == '}') ADVANCE(6); - if (lookahead == '\t' || - lookahead == '\n' || - lookahead == '\r' || - lookahead == ' ') SKIP(0) - if (('0' <= lookahead && lookahead <= '9')) ADVANCE(8); - END_STATE(); - case 1: - if (lookahead == '$') ADVANCE(4); - if (lookahead == '\\') ADVANCE(12); - if (lookahead == '}') ADVANCE(6); - if (lookahead == '\t' || - lookahead == '\n' || - lookahead == '\r' || - lookahead == ' ') ADVANCE(10); - if (lookahead != 0) ADVANCE(11); - END_STATE(); - case 2: - if (eof) ADVANCE(3); - if (lookahead == '$') ADVANCE(4); - if (lookahead == '\\') ADVANCE(12); - if (lookahead == '}') ADVANCE(9); - if (lookahead == '\t' || - lookahead == '\n' || - lookahead == '\r' || - lookahead == ' ') ADVANCE(10); - if (lookahead != 0) ADVANCE(11); - END_STATE(); - case 3: - ACCEPT_TOKEN(ts_builtin_sym_end); - END_STATE(); - case 4: - ACCEPT_TOKEN(anon_sym_DOLLAR); - if (lookahead == '{') ADVANCE(5); - END_STATE(); - case 5: - ACCEPT_TOKEN(anon_sym_DOLLAR_LBRACE); - END_STATE(); - case 6: - ACCEPT_TOKEN(anon_sym_RBRACE); - END_STATE(); - case 7: - ACCEPT_TOKEN(anon_sym_COLON); - END_STATE(); - case 8: - ACCEPT_TOKEN(sym_int); - if (('0' <= lookahead && lookahead <= '9')) ADVANCE(8); - END_STATE(); - case 9: - ACCEPT_TOKEN(sym__raw_curly); - if (lookahead == '}') ADVANCE(9); - END_STATE(); - case 10: - ACCEPT_TOKEN(sym__plain_text); - if (lookahead == '\\') ADVANCE(12); - if (lookahead == '\t' || - lookahead == '\n' || - lookahead == '\r' || - lookahead == ' ') ADVANCE(10); - if (lookahead != 0 && - lookahead != '$' && - lookahead != '}') ADVANCE(11); - END_STATE(); - case 11: - ACCEPT_TOKEN(sym__plain_text); - if (lookahead == '\\') ADVANCE(12); - if (lookahead != 0 && - lookahead != '$' && - lookahead != '}') ADVANCE(11); - END_STATE(); - case 12: - ACCEPT_TOKEN(sym__plain_text); - if (lookahead == '\\') ADVANCE(12); - if (lookahead != 0) ADVANCE(11); - END_STATE(); - default: - return false; - } -} - -static const TSLexMode ts_lex_modes[STATE_COUNT] = { - [0] = {.lex_state = 0}, - [1] = {.lex_state = 2}, - [2] = {.lex_state = 2}, - [3] = {.lex_state = 2}, - [4] = {.lex_state = 1}, - [5] = {.lex_state = 1}, - [6] = {.lex_state = 2}, - [7] = {.lex_state = 2}, - [8] = {.lex_state = 1}, - [9] = {.lex_state = 2}, - [10] = {.lex_state = 2}, - [11] = {.lex_state = 2}, - [12] = {.lex_state = 1}, - [13] = {.lex_state = 1}, - [14] = {.lex_state = 2}, - [15] = {.lex_state = 1}, - [16] = {.lex_state = 0}, - [17] = {.lex_state = 0}, - [18] = {.lex_state = 0}, - [19] = {.lex_state = 0}, - [20] = {.lex_state = 0}, - [21] = {.lex_state = 0}, - [22] = {.lex_state = 0}, - [23] = {.lex_state = 0}, - [24] = {.lex_state = 0}, -}; - -static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { - [0] = { - [ts_builtin_sym_end] = ACTIONS(1), - [anon_sym_DOLLAR] = ACTIONS(1), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(1), - [anon_sym_RBRACE] = ACTIONS(1), - [anon_sym_COLON] = ACTIONS(1), - [sym_int] = ACTIONS(1), - [sym__raw_curly] = ACTIONS(1), - }, - [1] = { - [sym_snippet] = STATE(20), - [sym__any] = STATE(6), - [sym_tabstop] = STATE(6), - [sym_placeholder] = STATE(6), - [sym_text] = STATE(6), - [aux_sym_snippet_repeat1] = STATE(6), - [anon_sym_DOLLAR] = ACTIONS(3), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(5), - [sym__raw_curly] = ACTIONS(7), - [sym__plain_text] = ACTIONS(9), - }, - [2] = { - [sym_snippet] = STATE(18), - [sym__any] = STATE(5), - [sym_tabstop] = STATE(5), - [sym_placeholder] = STATE(5), - [sym_text] = STATE(5), - [aux_sym_snippet_repeat1] = STATE(5), - [anon_sym_DOLLAR] = ACTIONS(11), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(13), - [sym__raw_curly] = ACTIONS(15), - [sym__plain_text] = ACTIONS(17), - }, - [3] = { - [sym_snippet] = STATE(22), - [sym__any] = STATE(5), - [sym_tabstop] = STATE(5), - [sym_placeholder] = STATE(5), - [sym_text] = STATE(5), - [aux_sym_snippet_repeat1] = STATE(5), - [anon_sym_DOLLAR] = ACTIONS(11), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(13), - [sym__raw_curly] = ACTIONS(15), - [sym__plain_text] = ACTIONS(17), - }, - [4] = { - [sym__any] = STATE(4), - [sym_tabstop] = STATE(4), - [sym_placeholder] = STATE(4), - [sym_text] = STATE(4), - [aux_sym_snippet_repeat1] = STATE(4), - [anon_sym_DOLLAR] = ACTIONS(19), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(22), - [anon_sym_RBRACE] = ACTIONS(25), - [sym__raw_curly] = ACTIONS(27), - [sym__plain_text] = ACTIONS(30), - }, - [5] = { - [sym__any] = STATE(4), - [sym_tabstop] = STATE(4), - [sym_placeholder] = STATE(4), - [sym_text] = STATE(4), - [aux_sym_snippet_repeat1] = STATE(4), - [anon_sym_DOLLAR] = ACTIONS(11), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(13), - [anon_sym_RBRACE] = ACTIONS(33), - [sym__raw_curly] = ACTIONS(15), - [sym__plain_text] = ACTIONS(17), - }, - [6] = { - [sym__any] = STATE(7), - [sym_tabstop] = STATE(7), - [sym_placeholder] = STATE(7), - [sym_text] = STATE(7), - [aux_sym_snippet_repeat1] = STATE(7), - [ts_builtin_sym_end] = ACTIONS(35), - [anon_sym_DOLLAR] = ACTIONS(3), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(5), - [sym__raw_curly] = ACTIONS(7), - [sym__plain_text] = ACTIONS(9), - }, - [7] = { - [sym__any] = STATE(7), - [sym_tabstop] = STATE(7), - [sym_placeholder] = STATE(7), - [sym_text] = STATE(7), - [aux_sym_snippet_repeat1] = STATE(7), - [ts_builtin_sym_end] = ACTIONS(37), - [anon_sym_DOLLAR] = ACTIONS(39), - [anon_sym_DOLLAR_LBRACE] = ACTIONS(42), - [sym__raw_curly] = ACTIONS(45), - [sym__plain_text] = ACTIONS(48), - }, -}; - -static const uint16_t ts_small_parse_table[] = { - [0] = 2, - ACTIONS(53), 1, - sym__plain_text, - ACTIONS(51), 4, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - anon_sym_RBRACE, - sym__raw_curly, - [10] = 2, - ACTIONS(55), 2, - ts_builtin_sym_end, - sym__plain_text, - ACTIONS(57), 3, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - sym__raw_curly, - [20] = 2, - ACTIONS(53), 2, - ts_builtin_sym_end, - sym__plain_text, - ACTIONS(51), 3, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - sym__raw_curly, - [30] = 2, - ACTIONS(59), 2, - ts_builtin_sym_end, - sym__plain_text, - ACTIONS(61), 3, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - sym__raw_curly, - [40] = 2, - ACTIONS(65), 1, - sym__plain_text, - ACTIONS(63), 4, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - anon_sym_RBRACE, - sym__raw_curly, - [50] = 2, - ACTIONS(55), 1, - sym__plain_text, - ACTIONS(57), 4, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - anon_sym_RBRACE, - sym__raw_curly, - [60] = 2, - ACTIONS(65), 2, - ts_builtin_sym_end, - sym__plain_text, - ACTIONS(63), 3, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - sym__raw_curly, - [70] = 2, - ACTIONS(59), 1, - sym__plain_text, - ACTIONS(61), 4, - anon_sym_DOLLAR, - anon_sym_DOLLAR_LBRACE, - anon_sym_RBRACE, - sym__raw_curly, - [80] = 2, - ACTIONS(67), 1, - anon_sym_RBRACE, - ACTIONS(69), 1, - anon_sym_COLON, - [87] = 2, - ACTIONS(71), 1, - anon_sym_RBRACE, - ACTIONS(73), 1, - anon_sym_COLON, - [94] = 1, - ACTIONS(75), 1, - anon_sym_RBRACE, - [98] = 1, - ACTIONS(77), 1, - sym_int, - [102] = 1, - ACTIONS(79), 1, - ts_builtin_sym_end, - [106] = 1, - ACTIONS(81), 1, - sym_int, - [110] = 1, - ACTIONS(83), 1, - anon_sym_RBRACE, - [114] = 1, - ACTIONS(85), 1, - sym_int, - [118] = 1, - ACTIONS(87), 1, - sym_int, -}; - -static const uint32_t ts_small_parse_table_map[] = { - [SMALL_STATE(8)] = 0, - [SMALL_STATE(9)] = 10, - [SMALL_STATE(10)] = 20, - [SMALL_STATE(11)] = 30, - [SMALL_STATE(12)] = 40, - [SMALL_STATE(13)] = 50, - [SMALL_STATE(14)] = 60, - [SMALL_STATE(15)] = 70, - [SMALL_STATE(16)] = 80, - [SMALL_STATE(17)] = 87, - [SMALL_STATE(18)] = 94, - [SMALL_STATE(19)] = 98, - [SMALL_STATE(20)] = 102, - [SMALL_STATE(21)] = 106, - [SMALL_STATE(22)] = 110, - [SMALL_STATE(23)] = 114, - [SMALL_STATE(24)] = 118, -}; - -static const TSParseActionEntry ts_parse_actions[] = { - [0] = {.entry = {.count = 0, .reusable = false}}, - [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(), - [3] = {.entry = {.count = 1, .reusable = false}}, SHIFT(19), - [5] = {.entry = {.count = 1, .reusable = false}}, SHIFT(24), - [7] = {.entry = {.count = 1, .reusable = false}}, SHIFT(14), - [9] = {.entry = {.count = 1, .reusable = true}}, SHIFT(14), - [11] = {.entry = {.count = 1, .reusable = false}}, SHIFT(21), - [13] = {.entry = {.count = 1, .reusable = false}}, SHIFT(23), - [15] = {.entry = {.count = 1, .reusable = false}}, SHIFT(12), - [17] = {.entry = {.count = 1, .reusable = true}}, SHIFT(12), - [19] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(21), - [22] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(23), - [25] = {.entry = {.count = 1, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), - [27] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(12), - [30] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(12), - [33] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_snippet, 1), - [35] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_snippet, 1), - [37] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_snippet_repeat1, 2), - [39] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(19), - [42] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(24), - [45] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(14), - [48] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_snippet_repeat1, 2), SHIFT_REPEAT(14), - [51] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_tabstop, 3), - [53] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_tabstop, 3), - [55] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_tabstop, 2), - [57] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_tabstop, 2), - [59] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_placeholder, 5), - [61] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_placeholder, 5), - [63] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_text, 1), - [65] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_text, 1), - [67] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10), - [69] = {.entry = {.count = 1, .reusable = true}}, SHIFT(2), - [71] = {.entry = {.count = 1, .reusable = true}}, SHIFT(8), - [73] = {.entry = {.count = 1, .reusable = true}}, SHIFT(3), - [75] = {.entry = {.count = 1, .reusable = true}}, SHIFT(11), - [77] = {.entry = {.count = 1, .reusable = true}}, SHIFT(9), - [79] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(), - [81] = {.entry = {.count = 1, .reusable = true}}, SHIFT(13), - [83] = {.entry = {.count = 1, .reusable = true}}, SHIFT(15), - [85] = {.entry = {.count = 1, .reusable = true}}, SHIFT(17), - [87] = {.entry = {.count = 1, .reusable = true}}, SHIFT(16), -}; - -#ifdef __cplusplus -extern "C" { -#endif -#ifdef _WIN32 -#define extern __declspec(dllexport) -#endif - -extern const TSLanguage *tree_sitter_snippet(void) { - static const TSLanguage language = { - .version = LANGUAGE_VERSION, - .symbol_count = SYMBOL_COUNT, - .alias_count = ALIAS_COUNT, - .token_count = TOKEN_COUNT, - .external_token_count = EXTERNAL_TOKEN_COUNT, - .state_count = STATE_COUNT, - .large_state_count = LARGE_STATE_COUNT, - .production_id_count = PRODUCTION_ID_COUNT, - .field_count = FIELD_COUNT, - .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH, - .parse_table = &ts_parse_table[0][0], - .small_parse_table = ts_small_parse_table, - .small_parse_table_map = ts_small_parse_table_map, - .parse_actions = ts_parse_actions, - .symbol_names = ts_symbol_names, - .symbol_metadata = ts_symbol_metadata, - .public_symbol_map = ts_symbol_map, - .alias_map = ts_non_terminal_alias_map, - .alias_sequences = &ts_alias_sequences[0][0], - .lex_modes = ts_lex_modes, - .lex_fn = ts_lex, - }; - return &language; -} -#ifdef __cplusplus -} -#endif diff --git a/crates/snippet/grammar/src/tree_sitter/parser.h b/crates/snippet/grammar/src/tree_sitter/parser.h deleted file mode 100644 index 2b14ac1046..0000000000 --- a/crates/snippet/grammar/src/tree_sitter/parser.h +++ /dev/null @@ -1,224 +0,0 @@ -#ifndef TREE_SITTER_PARSER_H_ -#define TREE_SITTER_PARSER_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -#define ts_builtin_sym_error ((TSSymbol)-1) -#define ts_builtin_sym_end 0 -#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 - -typedef uint16_t TSStateId; - -#ifndef TREE_SITTER_API_H_ -typedef uint16_t TSSymbol; -typedef uint16_t TSFieldId; -typedef struct TSLanguage TSLanguage; -#endif - -typedef struct { - TSFieldId field_id; - uint8_t child_index; - bool inherited; -} TSFieldMapEntry; - -typedef struct { - uint16_t index; - uint16_t length; -} TSFieldMapSlice; - -typedef struct { - bool visible; - bool named; - bool supertype; -} TSSymbolMetadata; - -typedef struct TSLexer TSLexer; - -struct TSLexer { - int32_t lookahead; - TSSymbol result_symbol; - void (*advance)(TSLexer *, bool); - void (*mark_end)(TSLexer *); - uint32_t (*get_column)(TSLexer *); - bool (*is_at_included_range_start)(const TSLexer *); - bool (*eof)(const TSLexer *); -}; - -typedef enum { - TSParseActionTypeShift, - TSParseActionTypeReduce, - TSParseActionTypeAccept, - TSParseActionTypeRecover, -} TSParseActionType; - -typedef union { - struct { - uint8_t type; - TSStateId state; - bool extra; - bool repetition; - } shift; - struct { - uint8_t type; - uint8_t child_count; - TSSymbol symbol; - int16_t dynamic_precedence; - uint16_t production_id; - } reduce; - uint8_t type; -} TSParseAction; - -typedef struct { - uint16_t lex_state; - uint16_t external_lex_state; -} TSLexMode; - -typedef union { - TSParseAction action; - struct { - uint8_t count; - bool reusable; - } entry; -} TSParseActionEntry; - -struct TSLanguage { - uint32_t version; - uint32_t symbol_count; - uint32_t alias_count; - uint32_t token_count; - uint32_t external_token_count; - uint32_t state_count; - uint32_t large_state_count; - uint32_t production_id_count; - uint32_t field_count; - uint16_t max_alias_sequence_length; - const uint16_t *parse_table; - const uint16_t *small_parse_table; - const uint32_t *small_parse_table_map; - const TSParseActionEntry *parse_actions; - const char * const *symbol_names; - const char * const *field_names; - const TSFieldMapSlice *field_map_slices; - const TSFieldMapEntry *field_map_entries; - const TSSymbolMetadata *symbol_metadata; - const TSSymbol *public_symbol_map; - const uint16_t *alias_map; - const TSSymbol *alias_sequences; - const TSLexMode *lex_modes; - bool (*lex_fn)(TSLexer *, TSStateId); - bool (*keyword_lex_fn)(TSLexer *, TSStateId); - TSSymbol keyword_capture_token; - struct { - const bool *states; - const TSSymbol *symbol_map; - void *(*create)(void); - void (*destroy)(void *); - bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); - unsigned (*serialize)(void *, char *); - void (*deserialize)(void *, const char *, unsigned); - } external_scanner; - const TSStateId *primary_state_ids; -}; - -/* - * Lexer Macros - */ - -#define START_LEXER() \ - bool result = false; \ - bool skip = false; \ - bool eof = false; \ - int32_t lookahead; \ - goto start; \ - next_state: \ - lexer->advance(lexer, skip); \ - start: \ - skip = false; \ - lookahead = lexer->lookahead; - -#define ADVANCE(state_value) \ - { \ - state = state_value; \ - goto next_state; \ - } - -#define SKIP(state_value) \ - { \ - skip = true; \ - state = state_value; \ - goto next_state; \ - } - -#define ACCEPT_TOKEN(symbol_value) \ - result = true; \ - lexer->result_symbol = symbol_value; \ - lexer->mark_end(lexer); - -#define END_STATE() return result; - -/* - * Parse Table Macros - */ - -#define SMALL_STATE(id) id - LARGE_STATE_COUNT - -#define STATE(id) id - -#define ACTIONS(id) id - -#define SHIFT(state_value) \ - {{ \ - .shift = { \ - .type = TSParseActionTypeShift, \ - .state = state_value \ - } \ - }} - -#define SHIFT_REPEAT(state_value) \ - {{ \ - .shift = { \ - .type = TSParseActionTypeShift, \ - .state = state_value, \ - .repetition = true \ - } \ - }} - -#define SHIFT_EXTRA() \ - {{ \ - .shift = { \ - .type = TSParseActionTypeShift, \ - .extra = true \ - } \ - }} - -#define REDUCE(symbol_val, child_count_val, ...) \ - {{ \ - .reduce = { \ - .type = TSParseActionTypeReduce, \ - .symbol = symbol_val, \ - .child_count = child_count_val, \ - __VA_ARGS__ \ - }, \ - }} - -#define RECOVER() \ - {{ \ - .type = TSParseActionTypeRecover \ - }} - -#define ACCEPT_INPUT() \ - {{ \ - .type = TSParseActionTypeAccept \ - }} - -#ifdef __cplusplus -} -#endif - -#endif // TREE_SITTER_PARSER_H_ diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 51d58aadeb..f838721ad5 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -1,31 +1,21 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use smallvec::SmallVec; use std::{collections::BTreeMap, ops::Range}; -use tree_sitter::{Parser, TreeCursor}; #[derive(Default)] pub struct Snippet { pub text: String, - pub tabstops: Vec; 2]>>, + pub tabstops: Vec, } +type TabStop = SmallVec<[Range; 2]>; + impl Snippet { pub fn parse(source: &str) -> Result { - let mut parser = Parser::new(); - parser - .set_language(tree_sitter_snippet::language()) - .unwrap(); - - let tree = parser.parse(source, None).unwrap(); - if tree.root_node().has_error() { - return Err(anyhow!("invalid snippet")); - } - let mut text = String::new(); let mut tabstops = BTreeMap::new(); - let mut cursor = tree.root_node().walk(); - parse_snippet_node(&mut cursor, &mut text, &mut tabstops, source)?; - + parse_snippet(source, false, &mut text, &mut tabstops) + .context("failed to parse snippet")?; Ok(Snippet { text, tabstops: tabstops.into_values().collect(), @@ -33,53 +23,79 @@ impl Snippet { } } -fn parse_snippet_node( - cursor: &mut TreeCursor, +fn parse_snippet<'a>( + mut source: &'a str, + nested: bool, text: &mut String, - tabstops: &mut BTreeMap; 2]>>, - source: &str, -) -> Result<()> { - cursor.goto_first_child(); + tabstops: &mut BTreeMap, +) -> Result<&'a str> { loop { - let node = cursor.node(); - match node.kind() { - "text" => text.push_str(&source[node.byte_range()]), - "tabstop" => { - if let Some(int_node) = node.named_child(0) { - let index = source[int_node.byte_range()].parse::()?; - tabstops - .entry(index) - .or_insert(SmallVec::new()) - .push(text.len()..text.len()); + match source.chars().next() { + None => return Ok(""), + Some('$') => { + source = parse_tabstop(&source[1..], text, tabstops)?; + } + Some('}') => { + if nested { + return Ok(source); + } else { + text.push('}'); + source = &source[1..]; } } - "placeholder" => { - cursor.goto_first_child(); - cursor.goto_next_sibling(); - let int_node = cursor.node(); - let index = source[int_node.byte_range()].parse::()?; - - cursor.goto_next_sibling(); - cursor.goto_next_sibling(); - let range_start = text.len(); - - parse_snippet_node(cursor, text, tabstops, source)?; - tabstops - .entry(index) - .or_insert(SmallVec::new()) - .push(range_start..text.len()); - - cursor.goto_parent(); + Some(_) => { + let chunk_end = source.find(&['}', '$']).unwrap_or(source.len()); + let (chunk, rest) = source.split_at(chunk_end); + text.push_str(chunk); + source = rest; } - _ => {} - } - - if !cursor.goto_next_sibling() { - break; } } - cursor.goto_parent(); - Ok(()) +} + +fn parse_tabstop<'a>( + mut source: &'a str, + text: &mut String, + tabstops: &mut BTreeMap, +) -> Result<&'a str> { + let tabstop_start = text.len(); + let tabstop_index; + if source.chars().next() == Some('{') { + let (index, rest) = parse_int(&source[1..])?; + tabstop_index = index; + source = rest; + + if source.chars().next() == Some(':') { + source = parse_snippet(&source[1..], true, text, tabstops)?; + } + + if source.chars().next() == Some('}') { + source = &source[1..]; + } else { + return Err(anyhow!("expected a closing brace")); + } + } else { + let (index, rest) = parse_int(&source)?; + tabstop_index = index; + source = rest; + } + + tabstops + .entry(tabstop_index) + .or_default() + .push(tabstop_start..text.len()); + Ok(source) +} + +fn parse_int(source: &str) -> Result<(usize, &str)> { + let len = source + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(source.len()); + if len == 0 { + return Err(anyhow!("expected an integer")); + } + let (prefix, suffix) = source.split_at(len); + Ok((prefix.parse()?, suffix)) } #[cfg(test)] @@ -98,19 +114,31 @@ mod tests { .collect::>(), &[vec![3..3]] ); - } - #[test] - fn test_parse_snippet_with_placeholders() { - let snippet = Snippet::parse("one${1:two}three").unwrap(); - assert_eq!(snippet.text, "onetwothree"); + // Multi-digit numbers + let snippet = Snippet::parse("one$123 $99").unwrap(); + assert_eq!(snippet.text, "one "); assert_eq!( snippet .tabstops .iter() .map(SmallVec::as_slice) .collect::>(), - &[vec![3..6]] + &[vec![4..4], vec![3..3]] + ); + } + + #[test] + fn test_parse_snippet_with_placeholders() { + let snippet = Snippet::parse("one${1:two}three${2:four}").unwrap(); + assert_eq!(snippet.text, "onetwothreefour"); + assert_eq!( + snippet + .tabstops + .iter() + .map(SmallVec::as_slice) + .collect::>(), + &[vec![3..6], vec![11..15]] ); } From 1371a20e5863b1ccc7e8b3185b26e88c4ba4dedc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Feb 2022 14:27:01 -0800 Subject: [PATCH 26/61] :art: Return an option task from confirm_completion --- crates/editor/src/editor.rs | 31 +++++++++++------------ crates/editor/src/multi_buffer.rs | 16 +++++------- crates/language/src/buffer.rs | 41 +++++++++++-------------------- crates/snippet/src/snippet.rs | 2 +- 4 files changed, 36 insertions(+), 54 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cd6ef78417..120ed69af1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -297,7 +297,9 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec) -> Task> { - if let Some(completion_state) = self.hide_completions(cx) { - if let Some(completion) = completion_state - .matches - .get(completion_state.selected_item) - .and_then(|mat| completion_state.completions.get(mat.candidate_id)) - { - return self.buffer.update(cx, |buffer, cx| { - buffer.apply_completion(completion.clone(), cx) - }); - } - } - - Task::ready(Ok(())) + fn confirm_completion(&mut self, cx: &mut ViewContext) -> Option>> { + let completion_state = self.hide_completions(cx)?; + let mat = completion_state + .matches + .get(completion_state.selected_item)?; + let completion = completion_state.completions.get(mat.candidate_id)?; + self.buffer.update(cx, |buffer, cx| { + let mut completion = completion.clone(); + // completion. + buffer.apply_completion(completion, cx) + }) } pub fn has_completions(&self) -> bool { @@ -6658,7 +6657,7 @@ mod tests { let apply_additional_edits = editor.update(&mut cx, |editor, cx| { editor.move_down(&MoveDown, cx); - let apply_additional_edits = editor.confirm_completion(cx); + let apply_additional_edits = editor.confirm_completion(cx).unwrap(); assert_eq!( editor.text(cx), " diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c70ea5d395..e9e839726d 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1,7 +1,7 @@ mod anchor; pub use anchor::{Anchor, AnchorRangeExt}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use clock::ReplicaId; use collections::{HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; @@ -933,17 +933,13 @@ impl MultiBuffer { &self, completion: Completion, cx: &mut ModelContext, - ) -> Task> { - let buffer = if let Some(buffer) = self + ) -> Option>> { + let buffer = self .buffers .borrow() - .get(&completion.old_range.start.buffer_id) - { - buffer.buffer.clone() - } else { - return Task::ready(Err(anyhow!("completion cannot be applied to any buffer"))); - }; - + .get(&completion.old_range.start.buffer_id)? + .buffer + .clone(); buffer.update(cx, |buffer, cx| { buffer.apply_completion( Completion { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 79a076c6fb..eb9f82ea29 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1781,35 +1781,22 @@ impl Buffer { &mut self, completion: Completion, cx: &mut ModelContext, - ) -> Task> { + ) -> Option>> { self.edit_with_autoindent([completion.old_range], completion.new_text.clone(), cx); - let file = if let Some(file) = self.file.as_ref() { - file - } else { - return Task::ready(Ok(Default::default())); - }; - if file.is_local() { - let server = if let Some(lang) = self.language_server.as_ref() { - lang.server.clone() - } else { - return Task::ready(Ok(Default::default())); - }; - - cx.spawn(|this, mut cx| async move { - let resolved_completion = server - .request::(completion.lsp_completion) - .await?; - if let Some(additional_edits) = resolved_completion.additional_text_edits { - this.update(&mut cx, |this, cx| { - this.apply_lsp_edits(additional_edits, cx) - })?; - } - Ok::<_, anyhow::Error>(()) - }) - } else { - return Task::ready(Ok(Default::default())); - } + self.file.as_ref()?.as_local()?; + let server = self.language_server.as_ref()?.server.clone(); + Some(cx.spawn(|this, mut cx| async move { + let resolved_completion = server + .request::(completion.lsp_completion) + .await?; + if let Some(additional_edits) = resolved_completion.additional_text_edits { + this.update(&mut cx, |this, cx| { + this.apply_lsp_edits(additional_edits, cx) + })?; + } + Ok::<_, anyhow::Error>(()) + })) } } diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index f838721ad5..47330c74af 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -12,7 +12,7 @@ type TabStop = SmallVec<[Range; 2]>; impl Snippet { pub fn parse(source: &str) -> Result { - let mut text = String::new(); + let mut text = String::with_capacity(source.len()); let mut tabstops = BTreeMap::new(); parse_snippet(source, false, &mut text, &mut tabstops) .context("failed to parse snippet")?; From 680fde9608e07bd7ab765a60a7023ca7fe9f7112 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Feb 2022 15:33:48 -0800 Subject: [PATCH 27/61] Handle special "final tabstop" in snippets --- crates/snippet/src/snippet.rs | 76 +++++++++++++++++------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 47330c74af..88e8aa2f5f 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -16,9 +16,13 @@ impl Snippet { let mut tabstops = BTreeMap::new(); parse_snippet(source, false, &mut text, &mut tabstops) .context("failed to parse snippet")?; + + let last_tabstop = tabstops + .remove(&0) + .unwrap_or_else(|| SmallVec::from_iter([text.len()..text.len()])); Ok(Snippet { text, - tabstops: tabstops.into_values().collect(), + tabstops: tabstops.into_values().chain(Some(last_tabstop)).collect(), }) } } @@ -103,58 +107,50 @@ mod tests { use super::*; #[test] - fn test_parse_snippet_with_tabstops() { - let snippet = Snippet::parse("one$1two").unwrap(); - assert_eq!(snippet.text, "onetwo"); - assert_eq!( - snippet - .tabstops - .iter() - .map(SmallVec::as_slice) - .collect::>(), - &[vec![3..3]] - ); - - // Multi-digit numbers - let snippet = Snippet::parse("one$123 $99").unwrap(); - assert_eq!(snippet.text, "one "); - assert_eq!( - snippet - .tabstops - .iter() - .map(SmallVec::as_slice) - .collect::>(), - &[vec![4..4], vec![3..3]] - ); + fn test_snippet_without_tabstops() { + let snippet = Snippet::parse("one-two-three").unwrap(); + assert_eq!(snippet.text, "one-two-three"); + assert_eq!(tabstops(&snippet), &[vec![13..13]]); } #[test] - fn test_parse_snippet_with_placeholders() { + fn test_snippet_with_tabstops() { + let snippet = Snippet::parse("one$1two").unwrap(); + assert_eq!(snippet.text, "onetwo"); + assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]); + + // Multi-digit numbers + let snippet = Snippet::parse("one$123-$99-two").unwrap(); + assert_eq!(snippet.text, "one--two"); + assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]); + } + + #[test] + fn test_snippet_with_explicit_final_tabstop() { + let snippet = Snippet::parse(r#"
$0
"#).unwrap(); + assert_eq!(snippet.text, r#"
"#); + assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]); + } + + #[test] + fn test_snippet_with_placeholders() { let snippet = Snippet::parse("one${1:two}three${2:four}").unwrap(); assert_eq!(snippet.text, "onetwothreefour"); assert_eq!( - snippet - .tabstops - .iter() - .map(SmallVec::as_slice) - .collect::>(), - &[vec![3..6], vec![11..15]] + tabstops(&snippet), + &[vec![3..6], vec![11..15], vec![15..15]] ); } #[test] - fn test_parse_snippet_with_nested_placeholders() { + fn test_snippet_with_nested_placeholders() { let snippet = Snippet::parse( - "for (${1:var ${2:i} = 0; ${2:i} < ${3:${4:array}.length}; ${2:i}++}) {$5}", + "for (${1:var ${2:i} = 0; ${2:i} < ${3:${4:array}.length}; ${2:i}++}) {$0}", ) .unwrap(); assert_eq!(snippet.text, "for (var i = 0; i < array.length; i++) {}"); assert_eq!( - snippet - .tabstops - .iter() - .map(SmallVec::as_slice) - .collect::>(), + tabstops(&snippet), &[ vec![5..37], vec![9..10, 16..17, 34..35], @@ -164,4 +160,8 @@ mod tests { ] ); } + + fn tabstops(snippet: &Snippet) -> Vec>> { + snippet.tabstops.iter().map(|t| t.to_vec()).collect() + } } From 7270fd00bad72e4dd00c6cbb2bbf3c4faa4894bc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Feb 2022 15:35:02 -0800 Subject: [PATCH 28/61] Start work on handling snippet completions --- Cargo.lock | 1 + crates/editor/Cargo.toml | 2 + crates/editor/src/editor.rs | 72 +++++++++++++++++++++++++++++-- crates/editor/src/multi_buffer.rs | 4 +- crates/language/src/buffer.rs | 4 +- 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f4ebb8b89..986d46d545 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1562,6 +1562,7 @@ dependencies = [ "serde", "smallvec", "smol", + "snippet", "sum_tree", "text", "theme", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 2ce10b4d81..77dcbab3df 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -22,7 +22,9 @@ collections = { path = "../collections" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } +lsp = { path = "../lsp" } project = { path = "../project" } +snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 120ed69af1..ada7d1f9af 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -42,6 +42,7 @@ use postage::watch; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; +use snippet::Snippet; use std::{ any::TypeId, cmp::{self, Ordering, Reverse}, @@ -1656,10 +1657,22 @@ impl Editor { .matches .get(completion_state.selected_item)?; let completion = completion_state.completions.get(mat.candidate_id)?; + + if completion.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) { + self.insert_snippet(completion.old_range.clone(), &completion.new_text, cx) + .log_err(); + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent( + [completion.old_range.clone()], + &completion.new_text, + cx, + ); + }); + } + self.buffer.update(cx, |buffer, cx| { - let mut completion = completion.clone(); - // completion. - buffer.apply_completion(completion, cx) + buffer.apply_additional_edits_for_completion(completion.clone(), cx) }) } @@ -1722,6 +1735,42 @@ impl Editor { }) } + pub fn insert_snippet( + &mut self, + range: Range, + text: &str, + cx: &mut ViewContext, + ) -> Result<()> + where + S: Clone + ToOffset, + { + let snippet = Snippet::parse(text)?; + let tabstops = self.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent([range.clone()], snippet.text, cx); + let snapshot = buffer.read(cx); + let start = range.start.to_offset(&snapshot); + snippet + .tabstops + .iter() + .map(|ranges| { + ranges + .into_iter() + .map(|range| { + snapshot.anchor_before(start + range.start) + ..snapshot.anchor_after(start + range.end) + }) + .collect::>() + }) + .collect::>() + }); + + if let Some(tabstop) = tabstops.first() { + self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx); + } + + Ok(()) + } + pub fn clear(&mut self, cx: &mut ViewContext) { self.start_transaction(cx); self.select_all(&SelectAll, cx); @@ -6581,6 +6630,23 @@ mod tests { }); } + #[gpui::test] + async fn test_snippets(mut cx: gpui::TestAppContext) { + let settings = cx.read(EditorSettings::test); + + let text = "a. b"; + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + editor.update(&mut cx, |editor, cx| { + editor + .insert_snippet(2..2, "f(${1:one}, ${2:two})$0", cx) + .unwrap(); + assert_eq!(editor.text(cx), "a.f(one, two) b"); + assert_eq!(editor.selected_ranges::(cx), &[4..7]); + }); + } + #[gpui::test] async fn test_completion(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e9e839726d..024dd4e67a 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -929,7 +929,7 @@ impl MultiBuffer { } } - pub fn apply_completion( + pub fn apply_additional_edits_for_completion( &self, completion: Completion, cx: &mut ModelContext, @@ -941,7 +941,7 @@ impl MultiBuffer { .buffer .clone(); buffer.update(cx, |buffer, cx| { - buffer.apply_completion( + buffer.apply_additional_edits_for_completion( Completion { old_range: completion.old_range.start.text_anchor ..completion.old_range.end.text_anchor, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index eb9f82ea29..187f1b00a9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1777,13 +1777,11 @@ impl Buffer { } } - pub fn apply_completion( + pub fn apply_additional_edits_for_completion( &mut self, completion: Completion, cx: &mut ModelContext, ) -> Option>> { - self.edit_with_autoindent([completion.old_range], completion.new_text.clone(), cx); - self.file.as_ref()?.as_local()?; let server = self.language_server.as_ref()?.server.clone(); Some(cx.spawn(|this, mut cx| async move { From 6ff45f2ade96788d624bf4a17b1caa6e7e5622e7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Feb 2022 17:40:39 -0800 Subject: [PATCH 29/61] Generalize autoclose stack to include snippet ranges as well --- crates/editor/src/editor.rs | 176 ++++++++++++++++++++++++++++++------ 1 file changed, 146 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ada7d1f9af..1d40fe4281 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -385,7 +385,7 @@ pub struct Editor { select_next_state: Option, selection_history: HashMap]>, Option]>>)>, - autoclose_stack: Vec, + action_region_stack: Vec, select_larger_syntax_node_stack: Vec]>>, active_diagnostics: Option, scroll_position: Vector2F, @@ -432,9 +432,27 @@ struct SelectNextState { } #[derive(Debug)] -struct BracketPairState { - ranges: Vec>, - pair: BracketPair, +enum ActionRegionState { + BracketPair { + ranges: Vec>, + pair: BracketPair, + }, + Snippet { + ranges: Vec>>, + active_index: usize, + }, +} + +impl ActionRegionState { + fn ranges(&self) -> &[Range] { + match self { + ActionRegionState::BracketPair { ranges, .. } => ranges.as_slice(), + ActionRegionState::Snippet { + ranges, + active_index, + } => ranges[*active_index].as_slice(), + } + } } struct CompletionState { @@ -579,7 +597,7 @@ impl Editor { add_selections_state: None, select_next_state: None, selection_history: Default::default(), - autoclose_stack: Default::default(), + action_region_stack: Vec::new(), select_larger_syntax_node_stack: Vec::new(), active_diagnostics: None, build_settings, @@ -1510,7 +1528,7 @@ impl Editor { if pair.end.len() == 1 { let mut delta = 0; - bracket_pair_state = Some(BracketPairState { + bracket_pair_state = Some(ActionRegionState::BracketPair { ranges: selections .iter() .map(move |selection| { @@ -1529,27 +1547,33 @@ impl Editor { self.update_selections(new_selections, None, cx); } if let Some(bracket_pair_state) = bracket_pair_state { - self.autoclose_stack.push(bracket_pair_state); + self.action_region_stack.push(bracket_pair_state); } } fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { let old_selections = self.local_selections::(cx); - let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() { - autoclose_pair + let mut autoclose_ranges = None; + if let Some(region_state) = self.action_region_stack.last() { + if let ActionRegionState::BracketPair { ranges, pair } = region_state { + if pair.end == text { + autoclose_ranges = Some(ranges.as_slice()); + } + } + } + + let autoclose_ranges = if let Some(ranges) = autoclose_ranges { + ranges } else { return false; }; - if text != autoclose_pair.pair.end { - return false; - } - debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); + debug_assert_eq!(old_selections.len(), autoclose_ranges.len()); let buffer = self.buffer.read(cx).snapshot(cx); if old_selections .iter() - .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer))) + .zip(autoclose_ranges.iter().map(|r| r.to_offset(&buffer))) .all(|(selection, autoclose_range)| { let autoclose_range_end = autoclose_range.end.to_offset(&buffer); selection.is_empty() && selection.start == autoclose_range_end @@ -1568,7 +1592,7 @@ impl Editor { } }) .collect(); - self.autoclose_stack.pop(); + self.action_region_stack.pop(); self.update_selections(new_selections, Some(Autoscroll::Fit), cx); true } else { @@ -1766,11 +1790,79 @@ impl Editor { if let Some(tabstop) = tabstops.first() { self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx); + self.action_region_stack.push(ActionRegionState::Snippet { + active_index: 0, + ranges: tabstops, + }); } Ok(()) } + pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { + self.move_to_snippet_tabstop(Bias::Right, cx) + } + + pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { + self.move_to_snippet_tabstop(Bias::Left, cx) + } + + pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { + let buffer = self.buffer.read(cx).snapshot(cx); + let old_selections = self.local_selections::(cx); + if let Some(region_state) = self.action_region_stack.last_mut() { + if let ActionRegionState::Snippet { + ranges, + active_index, + } = region_state + { + match bias { + Bias::Left => { + if *active_index > 0 { + *active_index -= 1; + } else { + return false; + } + } + Bias::Right => { + if *active_index + 1 < ranges.len() { + *active_index += 1; + } else { + return false; + } + } + } + if let Some(current_ranges) = ranges.get(*active_index) { + let new_selections = old_selections + .into_iter() + .zip(current_ranges.iter()) + .map(|(selection, new_range)| { + let new_range = new_range.to_offset(&buffer); + Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + + // Remove the snippet state when moving to the last tabstop. + if *active_index + 1 == ranges.len() { + self.action_region_stack.pop(); + } + + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + return true; + } + self.action_region_stack.pop(); + } + } + + false + } + pub fn clear(&mut self, cx: &mut ViewContext) { self.start_transaction(cx); self.select_all(&SelectAll, cx); @@ -1817,6 +1909,10 @@ impl Editor { } pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + if self.move_to_next_snippet_tabstop(cx) { + return; + } + self.start_transaction(cx); let tab_size = (self.build_settings)(cx).tab_size; let mut selections = self.local_selections::(cx); @@ -1889,6 +1985,10 @@ impl Editor { } pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { + if self.move_to_prev_snippet_tabstop(cx) { + return; + } + self.start_transaction(cx); let tab_size = (self.build_settings)(cx).tab_size; let selections = self.local_selections::(cx); @@ -3703,24 +3803,24 @@ impl Editor { self.add_selections_state = None; self.select_next_state = None; self.select_larger_syntax_node_stack.clear(); - while let Some(autoclose_pair) = self.autoclose_stack.last() { - let all_selections_inside_autoclose_ranges = - if selections.len() == autoclose_pair.ranges.len() { - selections - .iter() - .zip(autoclose_pair.ranges.iter().map(|r| r.to_point(&buffer))) - .all(|(selection, autoclose_range)| { - let head = selection.head().to_point(&buffer); - autoclose_range.start <= head && autoclose_range.end >= head - }) - } else { - false - }; + while let Some(region_state) = self.action_region_stack.last() { + let region_ranges = region_state.ranges(); + let all_selections_inside_action_ranges = if selections.len() == region_ranges.len() { + selections + .iter() + .zip(region_ranges.iter().map(|r| r.to_point(&buffer))) + .all(|(selection, action_range)| { + let head = selection.head().to_point(&buffer); + action_range.start <= head && action_range.end >= head + }) + } else { + false + }; - if all_selections_inside_autoclose_ranges { + if all_selections_inside_action_ranges { break; } else { - self.autoclose_stack.pop(); + self.action_region_stack.pop(); } } @@ -6644,6 +6744,22 @@ mod tests { .unwrap(); assert_eq!(editor.text(cx), "a.f(one, two) b"); assert_eq!(editor.selected_ranges::(cx), &[4..7]); + + // Can't move earlier than the first tab stop + assert!(!editor.move_to_prev_snippet_tabstop(cx)); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert_eq!(editor.selected_ranges::(cx), &[9..12]); + + assert!(editor.move_to_prev_snippet_tabstop(cx)); + assert_eq!(editor.selected_ranges::(cx), &[4..7]); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert_eq!(editor.selected_ranges::(cx), &[13..13]); + + // As soon as the last tab stop is reached, snippet state is gone + assert!(!editor.move_to_prev_snippet_tabstop(cx)); }); } From 79408464ea92e8be9ca0c2915c16655585cefe16 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 09:04:57 +0100 Subject: [PATCH 30/61] Navigate tabstops on tab even if the top of stack is a BracketPairState --- crates/editor/src/editor.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1d40fe4281..2bee666ef2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1810,6 +1810,14 @@ impl Editor { pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { let buffer = self.buffer.read(cx).snapshot(cx); let old_selections = self.local_selections::(cx); + + for (ix, region_state) in self.action_region_stack.iter().enumerate().rev() { + if let ActionRegionState::Snippet { .. } = region_state { + self.action_region_stack.truncate(ix + 1); + break; + } + } + if let Some(region_state) = self.action_region_stack.last_mut() { if let ActionRegionState::Snippet { ranges, From 74e5c5b89a2bee7efa9c4a337e5bcb22342fcf00 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 09:48:47 +0100 Subject: [PATCH 31/61] Separate autoclose pair stack from autocomplete stack --- crates/editor/src/editor.rs | 237 +++++++++++++++++++----------------- 1 file changed, 127 insertions(+), 110 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2bee666ef2..56e541603a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,7 +48,7 @@ use std::{ cmp::{self, Ordering, Reverse}, iter::{self, FromIterator}, mem, - ops::{Deref, Range, RangeInclusive, Sub}, + ops::{Deref, DerefMut, Range, RangeInclusive, Sub}, sync::Arc, time::{Duration, Instant}, }; @@ -312,6 +312,10 @@ trait SelectionExt { -> Range; } +trait InvalidationRegion { + fn ranges(&self) -> &[Range]; +} + #[derive(Clone, Debug)] pub enum SelectPhase { Begin { @@ -385,7 +389,8 @@ pub struct Editor { select_next_state: Option, selection_history: HashMap]>, Option]>>)>, - action_region_stack: Vec, + autoclose_stack: InvalidationStack, + snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, active_diagnostics: Option, scroll_position: Vector2F, @@ -431,30 +436,18 @@ struct SelectNextState { done: bool, } -#[derive(Debug)] -enum ActionRegionState { - BracketPair { - ranges: Vec>, - pair: BracketPair, - }, - Snippet { - ranges: Vec>>, - active_index: usize, - }, +struct BracketPairState { + ranges: Vec>, + pair: BracketPair, } -impl ActionRegionState { - fn ranges(&self) -> &[Range] { - match self { - ActionRegionState::BracketPair { ranges, .. } => ranges.as_slice(), - ActionRegionState::Snippet { - ranges, - active_index, - } => ranges[*active_index].as_slice(), - } - } +struct SnippetState { + ranges: Vec>>, + active_index: usize, } +struct InvalidationStack(Vec); + struct CompletionState { initial_position: Anchor, completions: Arc<[Completion]>, @@ -597,7 +590,8 @@ impl Editor { add_selections_state: None, select_next_state: None, selection_history: Default::default(), - action_region_stack: Vec::new(), + autoclose_stack: Default::default(), + snippet_stack: Default::default(), select_larger_syntax_node_stack: Vec::new(), active_diagnostics: None, build_settings, @@ -1528,7 +1522,7 @@ impl Editor { if pair.end.len() == 1 { let mut delta = 0; - bracket_pair_state = Some(ActionRegionState::BracketPair { + bracket_pair_state = Some(BracketPairState { ranges: selections .iter() .map(move |selection| { @@ -1547,33 +1541,27 @@ impl Editor { self.update_selections(new_selections, None, cx); } if let Some(bracket_pair_state) = bracket_pair_state { - self.action_region_stack.push(bracket_pair_state); + self.autoclose_stack.push(bracket_pair_state); } } fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { let old_selections = self.local_selections::(cx); - let mut autoclose_ranges = None; - if let Some(region_state) = self.action_region_stack.last() { - if let ActionRegionState::BracketPair { ranges, pair } = region_state { - if pair.end == text { - autoclose_ranges = Some(ranges.as_slice()); - } - } - } - - let autoclose_ranges = if let Some(ranges) = autoclose_ranges { - ranges + let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() { + autoclose_pair } else { return false; }; + if text != autoclose_pair.pair.end { + return false; + } - debug_assert_eq!(old_selections.len(), autoclose_ranges.len()); + debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); let buffer = self.buffer.read(cx).snapshot(cx); if old_selections .iter() - .zip(autoclose_ranges.iter().map(|r| r.to_offset(&buffer))) + .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer))) .all(|(selection, autoclose_range)| { let autoclose_range_end = autoclose_range.end.to_offset(&buffer); selection.is_empty() && selection.start == autoclose_range_end @@ -1592,7 +1580,7 @@ impl Editor { } }) .collect(); - self.action_region_stack.pop(); + self.autoclose_stack.pop(); self.update_selections(new_selections, Some(Autoscroll::Fit), cx); true } else { @@ -1790,7 +1778,7 @@ impl Editor { if let Some(tabstop) = tabstops.first() { self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx); - self.action_region_stack.push(ActionRegionState::Snippet { + self.snippet_stack.push(SnippetState { active_index: 0, ranges: tabstops, }); @@ -1811,61 +1799,48 @@ impl Editor { let buffer = self.buffer.read(cx).snapshot(cx); let old_selections = self.local_selections::(cx); - for (ix, region_state) in self.action_region_stack.iter().enumerate().rev() { - if let ActionRegionState::Snippet { .. } = region_state { - self.action_region_stack.truncate(ix + 1); - break; - } - } - - if let Some(region_state) = self.action_region_stack.last_mut() { - if let ActionRegionState::Snippet { - ranges, - active_index, - } = region_state - { - match bias { - Bias::Left => { - if *active_index > 0 { - *active_index -= 1; - } else { - return false; - } - } - Bias::Right => { - if *active_index + 1 < ranges.len() { - *active_index += 1; - } else { - return false; - } + if let Some(snippet) = self.snippet_stack.last_mut() { + match bias { + Bias::Left => { + if snippet.active_index > 0 { + snippet.active_index -= 1; + } else { + return false; } } - if let Some(current_ranges) = ranges.get(*active_index) { - let new_selections = old_selections - .into_iter() - .zip(current_ranges.iter()) - .map(|(selection, new_range)| { - let new_range = new_range.to_offset(&buffer); - Selection { - id: selection.id, - start: new_range.start, - end: new_range.end, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - - // Remove the snippet state when moving to the last tabstop. - if *active_index + 1 == ranges.len() { - self.action_region_stack.pop(); + Bias::Right => { + if snippet.active_index + 1 < snippet.ranges.len() { + snippet.active_index += 1; + } else { + return false; } - - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - return true; } - self.action_region_stack.pop(); } + if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { + let new_selections = old_selections + .into_iter() + .zip(current_ranges.iter()) + .map(|(selection, new_range)| { + let new_range = new_range.to_offset(&buffer); + Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + + // Remove the snippet state when moving to the last tabstop. + if snippet.active_index + 1 == snippet.ranges.len() { + self.snippet_stack.pop(); + } + + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + return true; + } + self.snippet_stack.pop(); } false @@ -3811,26 +3786,8 @@ impl Editor { self.add_selections_state = None; self.select_next_state = None; self.select_larger_syntax_node_stack.clear(); - while let Some(region_state) = self.action_region_stack.last() { - let region_ranges = region_state.ranges(); - let all_selections_inside_action_ranges = if selections.len() == region_ranges.len() { - selections - .iter() - .zip(region_ranges.iter().map(|r| r.to_point(&buffer))) - .all(|(selection, action_range)| { - let head = selection.head().to_point(&buffer); - action_range.start <= head && action_range.end >= head - }) - } else { - false - }; - - if all_selections_inside_action_ranges { - break; - } else { - self.action_region_stack.pop(); - } - } + self.autoclose_stack.invalidate(&selections, &buffer); + self.snippet_stack.invalidate(&selections, &buffer); let new_cursor_position = selections.iter().max_by_key(|s| s.id).map(|s| s.head()); if let Some(old_cursor_position) = old_cursor_position { @@ -4560,6 +4517,66 @@ impl SelectionExt for Selection { } } +impl InvalidationStack { + fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) + where + S: Clone + ToOffset, + { + while let Some(region) = self.last() { + let all_selections_inside_invalidation_ranges = + if selections.len() == region.ranges().len() { + selections + .iter() + .zip(region.ranges().iter().map(|r| r.to_offset(&buffer))) + .all(|(selection, invalidation_range)| { + let head = selection.head().to_offset(&buffer); + invalidation_range.start <= head && invalidation_range.end >= head + }) + } else { + false + }; + + if all_selections_inside_invalidation_ranges { + break; + } else { + self.pop(); + } + } + } +} + +impl Default for InvalidationStack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Deref for InvalidationStack { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for InvalidationStack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl InvalidationRegion for BracketPairState { + fn ranges(&self) -> &[Range] { + &self.ranges + } +} + +impl InvalidationRegion for SnippetState { + fn ranges(&self) -> &[Range] { + &self.ranges[self.active_index] + } +} + pub fn diagnostic_block_renderer( diagnostic: Diagnostic, is_valid: bool, From ae5aebf4053e88960a2b85aa91f0c66ecc16d512 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 10:09:12 +0100 Subject: [PATCH 32/61] Fix flaky `test_highlighted_ranges` due to random `TypeId` ordering --- crates/editor/src/editor.rs | 28 ++++++++++++++++------------ crates/gpui/src/color.rs | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 56e541603a..597128dd4a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7279,20 +7279,16 @@ mod tests { ); let snapshot = editor.snapshot(cx); + let mut highlighted_ranges = editor.highlighted_ranges_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + ); + // Enforce a consistent ordering based on color without relying on the ordering of the + // highlight's `TypeId` which is non-deterministic. + highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); assert_eq!( - editor.highlighted_ranges_in_range( - anchor_range(Point::new(3, 4)..Point::new(7, 4)), - &snapshot, - ), + highlighted_ranges, &[ - ( - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), - Color::red(), - ), - ( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), - ), ( DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), Color::green(), @@ -7301,6 +7297,14 @@ mod tests { DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), Color::green(), ), + ( + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + Color::red(), + ), + ( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + ), ] ); assert_eq!( diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 5adf03daef..4c301b4e48 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -12,7 +12,7 @@ use serde::{ }; use serde_json::json; -#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(transparent)] pub struct Color(ColorU); From ed549e352fdbfaf7a6bc299488d33b39b8e1a482 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 12:22:47 +0100 Subject: [PATCH 33/61] Start on requesting completions for remote buffers --- crates/editor/src/editor.rs | 2 +- crates/language/src/buffer.rs | 22 +++++++++++- crates/project/src/project.rs | 61 ++++++++++++++++++++++++++++++++++ crates/project/src/worktree.rs | 48 +++++++++++++++++++++++++- crates/rpc/proto/zed.proto | 45 +++++++++++++++++-------- crates/rpc/src/proto.rs | 4 +++ crates/server/src/rpc.rs | 25 ++++++++++++++ 7 files changed, 191 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 597128dd4a..a0e61f053b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1670,7 +1670,7 @@ impl Editor { .get(completion_state.selected_item)?; let completion = completion_state.completions.get(mat.candidate_id)?; - if completion.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) { + if completion.is_snippet() { self.insert_snippet(completion.old_range.clone(), &completion.new_text, cx) .log_err(); } else { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 187f1b00a9..d2d61de280 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -195,6 +195,13 @@ pub trait File { fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) -> Option>>; + fn completions( + &self, + buffer_id: u64, + position: Anchor, + cx: &mut MutableAppContext, + ) -> Task>>>; + fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext); fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); @@ -264,6 +271,15 @@ impl File for FakeFile { None } + fn completions( + &self, + _: u64, + _: Anchor, + _: &mut MutableAppContext, + ) -> Task>>> { + Task::ready(Ok(Default::default())) + } + fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} @@ -1773,7 +1789,7 @@ impl Buffer { }) }) } else { - Task::ready(Ok(Default::default())) + file.completions(self.remote_id(), self.anchor_before(position), cx.as_mut()) } } @@ -2555,6 +2571,10 @@ impl Completion { }; (kind_key, &self.label()[self.filter_range()]) } + + pub fn is_snippet(&self) -> bool { + self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) + } } pub fn contiguous_ranges( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d5f8f1a188..7109f02cec 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -334,6 +334,7 @@ impl Project { client.subscribe_to_entity(remote_id, cx, Self::handle_save_buffer), client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), client.subscribe_to_entity(remote_id, cx, Self::handle_format_buffer), + client.subscribe_to_entity(remote_id, cx, Self::handle_get_completions), client.subscribe_to_entity(remote_id, cx, Self::handle_get_definition), ]); } @@ -1683,6 +1684,66 @@ impl Project { Ok(()) } + fn handle_get_completions( + &mut self, + envelope: TypedEnvelope, + rpc: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let receipt = envelope.receipt(); + let sender_id = envelope.original_sender_id()?; + let buffer = self + .shared_buffers + .get(&sender_id) + .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let position = envelope + .payload + .position + .and_then(language::proto::deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + cx.spawn(|_, mut cx| async move { + match buffer + .update(&mut cx, |buffer, cx| buffer.completions(position, cx)) + .await + { + Ok(completions) => { + rpc.respond( + receipt, + proto::GetCompletionsResponse { + completions: completions + .into_iter() + .map(|completion| proto::Completion { + old_start: Some(language::proto::serialize_anchor( + &completion.old_range.start, + )), + old_end: Some(language::proto::serialize_anchor( + &completion.old_range.end, + )), + new_text: completion.new_text, + lsp_completion: serde_json::to_vec(&completion.lsp_completion) + .unwrap(), + }) + .collect(), + }, + ) + .await + } + Err(error) => { + rpc.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + ) + .await + } + } + }) + .detach_and_log_err(cx); + Ok(()) + } + pub fn handle_get_definition( &mut self, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 11006f32ba..77d9d4293f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -14,7 +14,7 @@ use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; -use language::{Buffer, DiagnosticEntry, Operation, PointUtf16, Rope}; +use language::{Anchor, Buffer, Completion, DiagnosticEntry, Operation, PointUtf16, Rope}; use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ @@ -1421,6 +1421,52 @@ impl language::File for File { })) } + fn completions( + &self, + buffer_id: u64, + position: Anchor, + cx: &mut MutableAppContext, + ) -> Task>>> { + let worktree = self.worktree.read(cx); + let worktree = if let Some(worktree) = worktree.as_remote() { + worktree + } else { + return Task::ready(Err(anyhow!( + "remote completions requested on a local worktree" + ))); + }; + let rpc = worktree.client.clone(); + let project_id = worktree.project_id; + cx.foreground().spawn(async move { + let response = rpc + .request(proto::GetCompletions { + project_id, + buffer_id, + position: Some(language::proto::serialize_anchor(&position)), + }) + .await?; + response + .completions + .into_iter() + .map(|completion| { + let old_start = completion + .old_start + .and_then(language::proto::deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old start"))?; + let old_end = completion + .old_end + .and_then(language::proto::deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old end"))?; + Ok(Completion { + old_range: old_start..old_end, + new_text: completion.new_text, + lsp_completion: serde_json::from_slice(&completion.lsp_completion)?, + }) + }) + .collect() + }) + } + fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { self.worktree.update(cx, |worktree, cx| { worktree.send_buffer_update(buffer_id, operation, cx); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5580d2a724..f9c92140a1 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -40,22 +40,24 @@ message Envelope { BufferSaved buffer_saved = 32; BufferReloaded buffer_reloaded = 33; FormatBuffer format_buffer = 34; + GetCompletions get_completions = 35; + GetCompletionsResponse get_completions_response = 36; - GetChannels get_channels = 35; - GetChannelsResponse get_channels_response = 36; - JoinChannel join_channel = 37; - JoinChannelResponse join_channel_response = 38; - LeaveChannel leave_channel = 39; - SendChannelMessage send_channel_message = 40; - SendChannelMessageResponse send_channel_message_response = 41; - ChannelMessageSent channel_message_sent = 42; - GetChannelMessages get_channel_messages = 43; - GetChannelMessagesResponse get_channel_messages_response = 44; + GetChannels get_channels = 37; + GetChannelsResponse get_channels_response = 38; + JoinChannel join_channel = 39; + JoinChannelResponse join_channel_response = 40; + LeaveChannel leave_channel = 41; + SendChannelMessage send_channel_message = 42; + SendChannelMessageResponse send_channel_message_response = 43; + ChannelMessageSent channel_message_sent = 44; + GetChannelMessages get_channel_messages = 45; + GetChannelMessagesResponse get_channel_messages_response = 46; - UpdateContacts update_contacts = 45; + UpdateContacts update_contacts = 47; - GetUsers get_users = 46; - GetUsersResponse get_users_response = 47; + GetUsers get_users = 48; + GetUsersResponse get_users_response = 49; } } @@ -203,6 +205,23 @@ message FormatBuffer { uint64 buffer_id = 2; } +message GetCompletions { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; +} + +message GetCompletionsResponse { + repeated Completion completions = 1; +} + +message Completion { + Anchor old_start = 1; + Anchor old_end = 2; + string new_text = 3; + bytes lsp_completion = 4; +} + message UpdateDiagnosticSummary { uint64 project_id = 1; uint64 worktree_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 509cefd46c..166d9e44b9 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -134,6 +134,8 @@ messages!( GetChannelMessagesResponse, GetChannels, GetChannelsResponse, + GetCompletions, + GetCompletionsResponse, GetDefinition, GetDefinitionResponse, GetUsers, @@ -170,6 +172,7 @@ request_messages!( (FormatBuffer, Ack), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), + (GetCompletions, GetCompletionsResponse), (GetDefinition, GetDefinitionResponse), (GetUsers, GetUsersResponse), (JoinChannel, JoinChannelResponse), @@ -194,6 +197,7 @@ entity_messages!( DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdating, FormatBuffer, + GetCompletions, GetDefinition, JoinProject, LeaveProject, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 8d5adfb3c5..5e14cafd2e 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -83,6 +83,7 @@ impl Server { .add_handler(Server::buffer_saved) .add_handler(Server::save_buffer) .add_handler(Server::format_buffer) + .add_handler(Server::get_completions) .add_handler(Server::get_channels) .add_handler(Server::get_users) .add_handler(Server::join_channel) @@ -722,6 +723,30 @@ impl Server { Ok(()) } + async fn get_completions( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host; + { + let state = self.state(); + let project = state + .read_project(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + host = project.host_connection_id; + } + + let sender = request.sender_id; + let receipt = request.receipt(); + let response = self + .peer + .forward_request(sender, host, request.payload.clone()) + .await?; + self.peer.respond(receipt, response).await?; + + Ok(()) + } + async fn update_buffer( self: Arc, request: TypedEnvelope, From 91e5c2dfac96f657a580f3ebadaa9e2b6774f030 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 14:07:41 +0100 Subject: [PATCH 34/61] Broadcast completion triggers to remote participants --- crates/editor/src/multi_buffer.rs | 21 +--- crates/language/src/buffer.rs | 170 +++++++++++++++++++----------- crates/language/src/proto.rs | 12 +++ crates/lsp/src/lsp.rs | 23 ++-- crates/rpc/proto/zed.proto | 6 ++ 5 files changed, 144 insertions(+), 88 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 024dd4e67a..6f74a419fe 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -911,22 +911,11 @@ impl MultiBuffer { let snapshot = self.snapshot(cx); let anchor = snapshot.anchor_before(position); let buffer = self.buffers.borrow()[&anchor.buffer_id].buffer.clone(); - if let Some(language_server) = buffer.read(cx).language_server() { - language_server - .capabilities() - .completion_provider - .as_ref() - .map_or(false, |provider| { - provider - .trigger_characters - .as_ref() - .map_or(false, |characters| { - characters.iter().any(|string| string == text) - }) - }) - } else { - false - } + buffer + .read(cx) + .completion_triggers() + .iter() + .any(|string| string == text) } pub fn apply_additional_edits_for_completion( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d2d61de280..29b29e26ed 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -74,6 +74,7 @@ pub struct Buffer { selections_update_count: usize, diagnostics_update_count: usize, language_server: Option, + completion_triggers: Vec, deferred_ops: OperationQueue, #[cfg(test)] pub(crate) operations: Vec, @@ -126,7 +127,7 @@ struct LanguageServerState { latest_snapshot: watch::Sender>, pending_snapshots: BTreeMap, next_version: usize, - _maintain_server: Task>, + _maintain_server: Task<()>, } #[derive(Clone)] @@ -148,6 +149,9 @@ pub enum Operation { selections: Arc<[Selection]>, lamport_timestamp: clock::Lamport, }, + UpdateCompletionTriggers { + triggers: Vec, + }, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -448,6 +452,8 @@ impl Buffer { cx, ); + this.completion_triggers = message.completion_triggers; + let deferred_ops = message .deferred_operations .into_iter() @@ -496,6 +502,7 @@ impl Buffer { .map(|op| proto::serialize_operation(&Operation::Buffer(op.clone()))), ) .collect(), + completion_triggers: self.completion_triggers.clone(), } } @@ -538,6 +545,7 @@ impl Buffer { diagnostics: Default::default(), diagnostics_update_count: 0, language_server: None, + completion_triggers: Default::default(), deferred_ops: OperationQueue::new(), #[cfg(test)] operations: Default::default(), @@ -639,75 +647,102 @@ impl Buffer { cx: &mut ModelContext, ) { self.language_server = if let Some(server) = language_server { - let (latest_snapshot_tx, mut latest_snapshot_rx) = watch::channel(); + let (latest_snapshot_tx, mut latest_snapshot_rx) = + watch::channel::>(); + + let maintain_changes = cx.background().spawn({ + let server = server.clone(); + async move { + let mut prev_snapshot: Option = None; + while let Some(snapshot) = latest_snapshot_rx.recv().await { + if let Some(snapshot) = snapshot { + let uri = lsp::Url::from_file_path(&snapshot.path).unwrap(); + if let Some(prev_snapshot) = prev_snapshot { + let changes = lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + uri, + snapshot.version as i32, + ), + content_changes: snapshot + .buffer_snapshot + .edits_since::<(PointUtf16, usize)>( + prev_snapshot.buffer_snapshot.version(), + ) + .map(|edit| { + let edit_start = edit.new.start.0; + let edit_end = + edit_start + (edit.old.end.0 - edit.old.start.0); + let new_text = snapshot + .buffer_snapshot + .text_for_range(edit.new.start.1..edit.new.end.1) + .collect(); + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + edit_start.to_lsp_position(), + edit_end.to_lsp_position(), + )), + range_length: None, + text: new_text, + } + }) + .collect(), + }; + server + .notify::(changes) + .await?; + } else { + server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + snapshot.version as i32, + snapshot.buffer_snapshot.text().to_string(), + ), + }, + ) + .await?; + } + + prev_snapshot = Some(snapshot); + } + } + Ok(()) + } + }); + Some(LanguageServerState { latest_snapshot: latest_snapshot_tx, pending_snapshots: Default::default(), next_version: 0, server: server.clone(), - _maintain_server: cx.background().spawn( - async move { - let mut prev_snapshot: Option = None; - while let Some(snapshot) = latest_snapshot_rx.recv().await { - if let Some(snapshot) = snapshot { - let uri = lsp::Url::from_file_path(&snapshot.path).unwrap(); - if let Some(prev_snapshot) = prev_snapshot { - let changes = lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - uri, - snapshot.version as i32, - ), - content_changes: snapshot - .buffer_snapshot - .edits_since::<(PointUtf16, usize)>( - prev_snapshot.buffer_snapshot.version(), - ) - .map(|edit| { - let edit_start = edit.new.start.0; - let edit_end = edit_start - + (edit.old.end.0 - edit.old.start.0); - let new_text = snapshot - .buffer_snapshot - .text_for_range( - edit.new.start.1..edit.new.end.1, - ) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - edit_start.to_lsp_position(), - edit_end.to_lsp_position(), - )), - range_length: None, - text: new_text, - } - }) - .collect(), - }; - server - .notify::(changes) - .await?; - } else { - server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri, - Default::default(), - snapshot.version as i32, - snapshot.buffer_snapshot.text().to_string(), - ), - }, - ) - .await?; - } - - prev_snapshot = Some(snapshot); + _maintain_server: cx.spawn_weak(|this, mut cx| async move { + let mut capabilities = server.capabilities(); + loop { + if let Some(capabilities) = capabilities.recv().await.flatten() { + if let Some(this) = this.upgrade(&cx) { + let triggers = capabilities + .completion_provider + .and_then(|c| c.trigger_characters) + .unwrap_or_default(); + this.update(&mut cx, |this, cx| { + this.completion_triggers = triggers.clone(); + this.send_operation( + Operation::UpdateCompletionTriggers { triggers }, + cx, + ); + }); + } else { + return; } + + break; } - Ok(()) } - .log_err(), - ), + + maintain_changes.log_err().await; + }), }) } else { None @@ -1591,6 +1626,7 @@ impl Buffer { Operation::UpdateSelections { selections, .. } => selections .iter() .all(|s| self.can_resolve(&s.start) && self.can_resolve(&s.end)), + Operation::UpdateCompletionTriggers { .. } => true, } } @@ -1630,6 +1666,9 @@ impl Buffer { self.text.lamport_clock.observe(lamport_timestamp); self.selections_update_count += 1; } + Operation::UpdateCompletionTriggers { triggers } => { + self.completion_triggers = triggers; + } } } @@ -1812,6 +1851,10 @@ impl Buffer { Ok::<_, anyhow::Error>(()) })) } + + pub fn completion_triggers(&self) -> &[String] { + &self.completion_triggers + } } #[cfg(any(test, feature = "test-support"))] @@ -2529,6 +2572,9 @@ impl operation_queue::Operation for Operation { | Operation::UpdateSelections { lamport_timestamp, .. } => *lamport_timestamp, + Operation::UpdateCompletionTriggers { .. } => { + unreachable!("updating completion triggers should never be deferred") + } } } } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 0f9ee69956..ec75018148 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -58,6 +58,13 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation { lamport_timestamp: lamport_timestamp.value, diagnostics: serialize_diagnostics(diagnostics.iter()), }), + Operation::UpdateCompletionTriggers { triggers } => { + proto::operation::Variant::UpdateCompletionTriggers( + proto::operation::UpdateCompletionTriggers { + triggers: triggers.clone(), + }, + ) + } }), } } @@ -238,6 +245,11 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { value: message.lamport_timestamp, }, }, + proto::operation::Variant::UpdateCompletionTriggers(message) => { + Operation::UpdateCompletionTriggers { + triggers: message.triggers, + } + } }, ) } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index cd235e8151..5cc1fee8aa 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Context, Result}; use futures::{io::BufWriter, AsyncRead, AsyncWrite}; use gpui::{executor, Task}; -use parking_lot::{Mutex, RwLock, RwLockReadGuard}; -use postage::{barrier, oneshot, prelude::Stream, sink::Sink}; +use parking_lot::{Mutex, RwLock}; +use postage::{barrier, oneshot, prelude::Stream, sink::Sink, watch}; use serde::{Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; use smol::{ @@ -34,7 +34,7 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, outbound_tx: RwLock>>>, - capabilities: RwLock, + capabilities: watch::Receiver>, notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, @@ -195,10 +195,11 @@ impl LanguageServer { ); let (initialized_tx, initialized_rx) = barrier::channel(); + let (mut capabilities_tx, capabilities_rx) = watch::channel(); let this = Arc::new(Self { notification_handlers, response_handlers, - capabilities: Default::default(), + capabilities: capabilities_rx, next_id: Default::default(), outbound_tx: RwLock::new(Some(outbound_tx)), executor: executor.clone(), @@ -212,7 +213,10 @@ impl LanguageServer { .spawn({ let this = this.clone(); async move { - this.init(root_uri).log_err().await; + if let Some(capabilities) = this.init(root_uri).log_err().await { + *capabilities_tx.borrow_mut() = Some(capabilities); + } + drop(initialized_tx); } }) @@ -221,7 +225,7 @@ impl LanguageServer { Ok(this) } - async fn init(self: Arc, root_uri: Url) -> Result<()> { + async fn init(self: Arc, root_uri: Url) -> Result { #[allow(deprecated)] let params = InitializeParams { process_id: Default::default(), @@ -269,12 +273,11 @@ impl LanguageServer { params, ); let response = request.await?; - *this.capabilities.write() = response.capabilities; Self::notify_internal::( this.outbound_tx.read().as_ref(), InitializedParams {}, )?; - Ok(()) + Ok(response.capabilities) } pub fn shutdown(&self) -> Option>> { @@ -328,8 +331,8 @@ impl LanguageServer { } } - pub fn capabilities(&self) -> RwLockReadGuard { - self.capabilities.read() + pub fn capabilities(&self) -> watch::Receiver> { + self.capabilities.clone() } pub fn request( diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f9c92140a1..beb41a2ec5 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -358,6 +358,7 @@ message BufferState { repeated Diagnostic diagnostics = 9; uint32 lamport_timestamp = 10; repeated Operation deferred_operations = 11; + repeated string completion_triggers = 12; } message BufferFragment { @@ -428,6 +429,7 @@ message Operation { Undo undo = 2; UpdateSelections update_selections = 3; UpdateDiagnostics update_diagnostics = 4; + UpdateCompletionTriggers update_completion_triggers = 5; } message Edit { @@ -453,6 +455,10 @@ message Operation { uint32 lamport_timestamp = 2; repeated Selection selections = 3; } + + message UpdateCompletionTriggers { + repeated string triggers = 1; + } } message UndoMapEntry { From d765e75bad1a9d8553b7939a877c125af48a5343 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 16:22:38 +0100 Subject: [PATCH 35/61] Apply additional edits for completion when the buffer is remote --- Cargo.lock | 1 + crates/editor/src/editor.rs | 4 +- crates/editor/src/multi_buffer.rs | 25 ++++-- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 121 +++++++++++++++++++++++------- crates/language/src/proto.rs | 27 ++++++- crates/project/src/project.rs | 71 +++++++++++++++--- crates/project/src/worktree.rs | 52 +++++++++---- crates/rpc/proto/zed.proto | 43 +++++++---- crates/rpc/src/proto.rs | 7 ++ crates/server/src/rpc.rs | 25 ++++++ crates/text/src/text.rs | 42 ++++++++++- 12 files changed, 342 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 986d46d545..719e231f96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2635,6 +2635,7 @@ dependencies = [ "rand 0.8.3", "rpc", "serde", + "serde_json", "similar", "smallvec", "smol", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a0e61f053b..0acfef3086 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1683,9 +1683,9 @@ impl Editor { }); } - self.buffer.update(cx, |buffer, cx| { + Some(self.buffer.update(cx, |buffer, cx| { buffer.apply_additional_edits_for_completion(completion.clone(), cx) - }) + })) } pub fn has_completions(&self) -> bool { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 6f74a419fe..bce223fb88 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -313,9 +313,9 @@ impl MultiBuffer { .map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot)); return buffer.update(cx, |buffer, cx| { if autoindent { - buffer.edit_with_autoindent(ranges, new_text, cx) + buffer.edit_with_autoindent(ranges, new_text, cx); } else { - buffer.edit(ranges, new_text, cx) + buffer.edit(ranges, new_text, cx); } }); } @@ -922,14 +922,18 @@ impl MultiBuffer { &self, completion: Completion, cx: &mut ModelContext, - ) -> Option>> { - let buffer = self + ) -> Task> { + let buffer = if let Some(buffer_state) = self .buffers .borrow() - .get(&completion.old_range.start.buffer_id)? - .buffer - .clone(); - buffer.update(cx, |buffer, cx| { + .get(&completion.old_range.start.buffer_id) + { + buffer_state.buffer.clone() + } else { + return Task::ready(Ok(())); + }; + + let apply_edits = buffer.update(cx, |buffer, cx| { buffer.apply_additional_edits_for_completion( Completion { old_range: completion.old_range.start.text_anchor @@ -937,8 +941,13 @@ impl MultiBuffer { new_text: completion.new_text, lsp_completion: completion.lsp_completion, }, + true, cx, ) + }); + cx.foreground().spawn(async move { + apply_edits.await?; + Ok(()) }) } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 6c29708b5e..f0a3096a93 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -36,6 +36,7 @@ parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = { version = "0.8.3", optional = true } serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["preserve_order"] } similar = "1.3" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 29b29e26ed..f4554e0d45 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -206,6 +206,13 @@ pub trait File { cx: &mut MutableAppContext, ) -> Task>>>; + fn apply_additional_edits_for_completion( + &self, + buffer_id: u64, + completion: Completion, + cx: &mut MutableAppContext, + ) -> Task>>; + fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext); fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); @@ -284,6 +291,15 @@ impl File for FakeFile { Task::ready(Ok(Default::default())) } + fn apply_additional_edits_for_completion( + &self, + _: u64, + _: Completion, + _: &mut MutableAppContext, + ) -> Task>> { + Task::ready(Ok(Default::default())) + } + fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} @@ -595,7 +611,8 @@ impl Buffer { if let Some(edits) = edits { this.update(&mut cx, |this, cx| { if this.version == version { - this.apply_lsp_edits(edits, cx) + this.apply_lsp_edits(edits, cx)?; + Ok(()) } else { Err(anyhow!("buffer edited since starting to format")) } @@ -1295,7 +1312,9 @@ impl Buffer { let range = offset..(offset + len); match tag { ChangeTag::Equal => offset += len, - ChangeTag::Delete => self.edit(Some(range), "", cx), + ChangeTag::Delete => { + self.edit(Some(range), "", cx); + } ChangeTag::Insert => { self.edit(Some(offset..offset), &diff.new_text[range], cx); offset += len; @@ -1409,7 +1428,12 @@ impl Buffer { .blocking_send(Some(snapshot)); } - pub fn edit(&mut self, ranges_iter: I, new_text: T, cx: &mut ModelContext) + pub fn edit( + &mut self, + ranges_iter: I, + new_text: T, + cx: &mut ModelContext, + ) -> Option where I: IntoIterator>, S: ToOffset, @@ -1423,7 +1447,8 @@ impl Buffer { ranges_iter: I, new_text: T, cx: &mut ModelContext, - ) where + ) -> Option + where I: IntoIterator>, S: ToOffset, T: Into, @@ -1437,7 +1462,8 @@ impl Buffer { new_text: T, autoindent: bool, cx: &mut ModelContext, - ) where + ) -> Option + where I: IntoIterator>, S: ToOffset, T: Into, @@ -1461,7 +1487,7 @@ impl Buffer { } } if ranges.is_empty() { - return; + return None; } self.start_transaction(); @@ -1488,6 +1514,7 @@ impl Buffer { let new_text_len = new_text.len(); let edit = self.text.edit(ranges.iter().cloned(), new_text); + let edit_id = edit.timestamp.local(); if let Some((before_edit, edited)) = autoindent_request { let mut inserted = None; @@ -1517,13 +1544,14 @@ impl Buffer { self.end_transaction(cx); self.send_operation(Operation::Buffer(text::Operation::Edit(edit)), cx); + Some(edit_id) } fn apply_lsp_edits( &mut self, edits: Vec, cx: &mut ModelContext, - ) -> Result<()> { + ) -> Result> { for edit in &edits { let range = range_from_lsp(edit.range); if self.clip_point_utf16(range.start, Bias::Left) != range.start @@ -1535,11 +1563,14 @@ impl Buffer { } } - for edit in edits.into_iter().rev() { - self.edit([range_from_lsp(edit.range)], edit.new_text, cx); - } - - Ok(()) + self.start_transaction(); + let edit_ids = edits + .into_iter() + .rev() + .filter_map(|edit| self.edit([range_from_lsp(edit.range)], edit.new_text, cx)) + .collect(); + self.end_transaction(cx); + Ok(edit_ids) } fn did_edit( @@ -1835,21 +1866,59 @@ impl Buffer { pub fn apply_additional_edits_for_completion( &mut self, completion: Completion, + push_to_history: bool, cx: &mut ModelContext, - ) -> Option>> { - self.file.as_ref()?.as_local()?; - let server = self.language_server.as_ref()?.server.clone(); - Some(cx.spawn(|this, mut cx| async move { - let resolved_completion = server - .request::(completion.lsp_completion) - .await?; - if let Some(additional_edits) = resolved_completion.additional_text_edits { - this.update(&mut cx, |this, cx| { - this.apply_lsp_edits(additional_edits, cx) - })?; - } - Ok::<_, anyhow::Error>(()) - })) + ) -> Task>> { + let file = if let Some(file) = self.file.as_ref() { + file + } else { + return Task::ready(Ok(Default::default())); + }; + + if file.is_local() { + let server = if let Some(lang) = self.language_server.as_ref() { + lang.server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; + + cx.spawn(|this, mut cx| async move { + let resolved_completion = server + .request::(completion.lsp_completion) + .await?; + if let Some(additional_edits) = resolved_completion.additional_text_edits { + this.update(&mut cx, |this, cx| { + this.avoid_grouping_next_transaction(); + this.start_transaction(); + let edit_ids = this.apply_lsp_edits(additional_edits, cx); + if let Some(transaction_id) = this.end_transaction(cx) { + if !push_to_history { + this.text.forget_transaction(transaction_id); + } + } + edit_ids + }) + } else { + Ok(Default::default()) + } + }) + } else { + let apply_edits = file.apply_additional_edits_for_completion( + self.remote_id(), + completion, + cx.as_mut(), + ); + cx.spawn(|this, mut cx| async move { + let edit_ids = apply_edits.await?; + if push_to_history { + this.update(&mut cx, |this, _| { + this.text + .push_transaction(edit_ids.iter().copied(), Instant::now()); + }); + } + Ok(edit_ids) + }) + } } pub fn completion_triggers(&self) -> &[String] { diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index ec75018148..c95735493d 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,4 +1,4 @@ -use crate::{diagnostic_set::DiagnosticEntry, Diagnostic, Operation}; +use crate::{diagnostic_set::DiagnosticEntry, Completion, Diagnostic, Operation}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::HashSet; @@ -377,3 +377,28 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { }, }) } + +pub fn serialize_completion(completion: &Completion) -> proto::Completion { + proto::Completion { + old_start: Some(serialize_anchor(&completion.old_range.start)), + old_end: Some(serialize_anchor(&completion.old_range.end)), + new_text: completion.new_text.clone(), + lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), + } +} + +pub fn deserialize_completion(completion: proto::Completion) -> Result> { + let old_start = completion + .old_start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old start"))?; + let old_end = completion + .old_end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old end"))?; + Ok(Completion { + old_range: old_start..old_end, + new_text: completion.new_text, + lsp_completion: serde_json::from_slice(&completion.lsp_completion)?, + }) +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7109f02cec..d01fb08ee7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -335,6 +335,11 @@ impl Project { client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), client.subscribe_to_entity(remote_id, cx, Self::handle_format_buffer), client.subscribe_to_entity(remote_id, cx, Self::handle_get_completions), + client.subscribe_to_entity( + remote_id, + cx, + Self::handle_apply_additional_edits_for_completion, + ), client.subscribe_to_entity(remote_id, cx, Self::handle_get_definition), ]); } @@ -1712,17 +1717,63 @@ impl Project { receipt, proto::GetCompletionsResponse { completions: completions + .iter() + .map(language::proto::serialize_completion) + .collect(), + }, + ) + .await + } + Err(error) => { + rpc.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + ) + .await + } + } + }) + .detach_and_log_err(cx); + Ok(()) + } + + fn handle_apply_additional_edits_for_completion( + &mut self, + envelope: TypedEnvelope, + rpc: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let receipt = envelope.receipt(); + let sender_id = envelope.original_sender_id()?; + let buffer = self + .shared_buffers + .get(&sender_id) + .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let completion = language::proto::deserialize_completion( + envelope + .payload + .completion + .ok_or_else(|| anyhow!("invalid position"))?, + )?; + cx.spawn(|_, mut cx| async move { + match buffer + .update(&mut cx, |buffer, cx| { + buffer.apply_additional_edits_for_completion(completion, false, cx) + }) + .await + { + Ok(edit_ids) => { + rpc.respond( + receipt, + proto::ApplyCompletionAdditionalEditsResponse { + additional_edits: edit_ids .into_iter() - .map(|completion| proto::Completion { - old_start: Some(language::proto::serialize_anchor( - &completion.old_range.start, - )), - old_end: Some(language::proto::serialize_anchor( - &completion.old_range.end, - )), - new_text: completion.new_text, - lsp_completion: serde_json::to_vec(&completion.lsp_completion) - .unwrap(), + .map(|edit_id| proto::AdditionalEdit { + replica_id: edit_id.replica_id as u32, + local_timestamp: edit_id.value, }) .collect(), }, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 77d9d4293f..1f654751cb 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1448,25 +1448,47 @@ impl language::File for File { response .completions .into_iter() - .map(|completion| { - let old_start = completion - .old_start - .and_then(language::proto::deserialize_anchor) - .ok_or_else(|| anyhow!("invalid old start"))?; - let old_end = completion - .old_end - .and_then(language::proto::deserialize_anchor) - .ok_or_else(|| anyhow!("invalid old end"))?; - Ok(Completion { - old_range: old_start..old_end, - new_text: completion.new_text, - lsp_completion: serde_json::from_slice(&completion.lsp_completion)?, - }) - }) + .map(language::proto::deserialize_completion) .collect() }) } + fn apply_additional_edits_for_completion( + &self, + buffer_id: u64, + completion: Completion, + cx: &mut MutableAppContext, + ) -> Task>> { + let worktree = self.worktree.read(cx); + let worktree = if let Some(worktree) = worktree.as_remote() { + worktree + } else { + return Task::ready(Err(anyhow!( + "remote additional edits application requested on a local worktree" + ))); + }; + let rpc = worktree.client.clone(); + let project_id = worktree.project_id; + cx.foreground().spawn(async move { + let response = rpc + .request(proto::ApplyCompletionAdditionalEdits { + project_id, + buffer_id, + completion: Some(language::proto::serialize_completion(&completion)), + }) + .await?; + + Ok(response + .additional_edits + .into_iter() + .map(|edit| clock::Local { + replica_id: edit.replica_id as ReplicaId, + value: edit.local_timestamp, + }) + .collect()) + }) + } + fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { self.worktree.update(cx, |worktree, cx| { worktree.send_buffer_update(buffer_id, operation, cx); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index beb41a2ec5..2f2364fc24 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -42,22 +42,24 @@ message Envelope { FormatBuffer format_buffer = 34; GetCompletions get_completions = 35; GetCompletionsResponse get_completions_response = 36; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 37; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 38; - GetChannels get_channels = 37; - GetChannelsResponse get_channels_response = 38; - JoinChannel join_channel = 39; - JoinChannelResponse join_channel_response = 40; - LeaveChannel leave_channel = 41; - SendChannelMessage send_channel_message = 42; - SendChannelMessageResponse send_channel_message_response = 43; - ChannelMessageSent channel_message_sent = 44; - GetChannelMessages get_channel_messages = 45; - GetChannelMessagesResponse get_channel_messages_response = 46; + GetChannels get_channels = 39; + GetChannelsResponse get_channels_response = 40; + JoinChannel join_channel = 41; + JoinChannelResponse join_channel_response = 42; + LeaveChannel leave_channel = 43; + SendChannelMessage send_channel_message = 44; + SendChannelMessageResponse send_channel_message_response = 45; + ChannelMessageSent channel_message_sent = 46; + GetChannelMessages get_channel_messages = 47; + GetChannelMessagesResponse get_channel_messages_response = 48; - UpdateContacts update_contacts = 47; + UpdateContacts update_contacts = 49; - GetUsers get_users = 48; - GetUsersResponse get_users_response = 49; + GetUsers get_users = 50; + GetUsersResponse get_users_response = 51; } } @@ -215,6 +217,21 @@ message GetCompletionsResponse { repeated Completion completions = 1; } +message ApplyCompletionAdditionalEdits { + uint64 project_id = 1; + uint64 buffer_id = 2; + Completion completion = 3; +} + +message ApplyCompletionAdditionalEditsResponse { + repeated AdditionalEdit additional_edits = 1; +} + +message AdditionalEdit { + uint32 replica_id = 1; + uint32 local_timestamp = 2; +} + message Completion { Anchor old_start = 1; Anchor old_end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 166d9e44b9..a940795265 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -122,6 +122,8 @@ macro_rules! entity_messages { messages!( Ack, AddProjectCollaborator, + ApplyCompletionAdditionalEdits, + ApplyCompletionAdditionalEditsResponse, BufferReloaded, BufferSaved, ChannelMessageSent, @@ -169,6 +171,10 @@ messages!( ); request_messages!( + ( + ApplyCompletionAdditionalEdits, + ApplyCompletionAdditionalEditsResponse + ), (FormatBuffer, Ack), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), @@ -191,6 +197,7 @@ request_messages!( entity_messages!( project_id, AddProjectCollaborator, + ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, CloseBuffer, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 5e14cafd2e..70e55c66f9 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -84,6 +84,7 @@ impl Server { .add_handler(Server::save_buffer) .add_handler(Server::format_buffer) .add_handler(Server::get_completions) + .add_handler(Server::apply_additional_edits_for_completion) .add_handler(Server::get_channels) .add_handler(Server::get_users) .add_handler(Server::join_channel) @@ -747,6 +748,30 @@ impl Server { Ok(()) } + async fn apply_additional_edits_for_completion( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host; + { + let state = self.state(); + let project = state + .read_project(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + host = project.host_connection_id; + } + + let sender = request.sender_id; + let receipt = request.receipt(); + let response = self + .peer + .forward_request(sender, host, request.payload.clone()) + .await?; + self.peer.respond(receipt, response).await?; + + Ok(()) + } + async fn update_buffer( self: Arc, request: TypedEnvelope, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index b619aba2ce..ad9857e264 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -233,6 +233,20 @@ impl History { } } + fn push_transaction(&mut self, edit_ids: impl IntoIterator, now: Instant) { + assert_eq!(self.transaction_depth, 0); + let mut edit_ids = edit_ids.into_iter().peekable(); + + if let Some(first_edit_id) = edit_ids.peek() { + let version = self.ops[first_edit_id].version.clone(); + self.start_transaction(version, now); + for edit_id in edit_ids { + self.push_undo(edit_id); + } + self.end_transaction(now); + } + } + fn push_undo(&mut self, edit_id: clock::Local) { assert_ne!(self.transaction_depth, 0); let last_transaction = self.undo_stack.last_mut().unwrap(); @@ -260,6 +274,17 @@ impl History { } } + fn forget(&mut self, transaction_id: TransactionId) { + assert_eq!(self.transaction_depth, 0); + if let Some(transaction_ix) = self.undo_stack.iter().rposition(|t| t.id == transaction_id) { + self.undo_stack.remove(transaction_ix); + } else if let Some(transaction_ix) = + self.redo_stack.iter().rposition(|t| t.id == transaction_id) + { + self.undo_stack.remove(transaction_ix); + } + } + fn pop_redo(&mut self) -> Option<&Transaction> { assert_eq!(self.transaction_depth, 0); if let Some(transaction) = self.redo_stack.pop() { @@ -377,14 +402,14 @@ pub struct InsertionTimestamp { } impl InsertionTimestamp { - fn local(&self) -> clock::Local { + pub fn local(&self) -> clock::Local { clock::Local { replica_id: self.replica_id, value: self.local, } } - fn lamport(&self) -> clock::Lamport { + pub fn lamport(&self) -> clock::Lamport { clock::Lamport { replica_id: self.replica_id, value: self.lamport, @@ -1188,6 +1213,7 @@ impl Buffer { pub fn undo(&mut self) -> Option<(TransactionId, Operation)> { if let Some(transaction) = self.history.pop_undo().cloned() { + dbg!(&transaction); let transaction_id = transaction.id; let op = self.undo_or_redo(transaction).unwrap(); Some((transaction_id, op)) @@ -1205,6 +1231,10 @@ impl Buffer { } } + pub fn forget_transaction(&mut self, transaction_id: TransactionId) { + self.history.forget(transaction_id); + } + pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { if let Some(transaction) = self.history.pop_redo().cloned() { let transaction_id = transaction.id; @@ -1245,6 +1275,14 @@ impl Buffer { }) } + pub fn push_transaction( + &mut self, + edit_ids: impl IntoIterator, + now: Instant, + ) { + self.history.push_transaction(edit_ids, now); + } + pub fn subscribe(&mut self) -> Subscription { self.subscriptions.subscribe() } From 924eb622ae791f56dfbc6f797eb3bc84d95e9904 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 17:01:23 +0100 Subject: [PATCH 36/61] Wait for additional edits before pushing transaction in remote buffer --- Cargo.lock | 1 + crates/language/src/buffer.rs | 2 ++ crates/text/Cargo.toml | 1 + crates/text/src/text.rs | 38 ++++++++++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 719e231f96..ce979fb313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4904,6 +4904,7 @@ dependencies = [ "lazy_static", "log", "parking_lot", + "postage", "rand 0.8.3", "smallvec", "sum_tree", diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f4554e0d45..00dee0a57c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1910,6 +1910,8 @@ impl Buffer { ); cx.spawn(|this, mut cx| async move { let edit_ids = apply_edits.await?; + this.update(&mut cx, |this, _| this.text.wait_for_edits(&edit_ids)) + .await; if push_to_history { this.update(&mut cx, |this, _| { this.text diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index edc1ca7846..04648d203f 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -18,6 +18,7 @@ arrayvec = "0.7.1" lazy_static = "1.4" log = "0.4" parking_lot = "0.11" +postage = { version = "0.4.1", features = ["futures-traits"] } rand = { version = "0.8.3", optional = true } smallvec = { version = "1.6", features = ["union"] } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index ad9857e264..088cd51cbe 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -21,6 +21,7 @@ use operation_queue::OperationQueue; pub use patch::Patch; pub use point::*; pub use point_utf16::*; +use postage::{oneshot, prelude::*}; #[cfg(any(test, feature = "test-support"))] pub use random_char_iter::*; use rope::TextDimension; @@ -28,6 +29,7 @@ pub use rope::{Chunks, Rope, TextSummary}; pub use selection::*; use std::{ cmp::{self, Ordering}, + future::Future, iter::Iterator, ops::{self, Deref, Range, Sub}, str, @@ -50,6 +52,7 @@ pub struct Buffer { local_clock: clock::Local, pub lamport_clock: clock::Lamport, subscriptions: Topic, + edit_id_resolvers: HashMap>>, } #[derive(Clone, Debug)] @@ -538,6 +541,7 @@ impl Buffer { local_clock, lamport_clock, subscriptions: Default::default(), + edit_id_resolvers: Default::default(), } } @@ -579,6 +583,7 @@ impl Buffer { value: lamport_timestamp, }, subscriptions: Default::default(), + edit_id_resolvers: Default::default(), snapshot: BufferSnapshot { replica_id, visible_text, @@ -833,6 +838,7 @@ impl Buffer { edit.timestamp, ); self.snapshot.version.observe(edit.timestamp.local()); + self.resolve_edit(edit.timestamp.local()); self.history.push(edit); } } @@ -1213,7 +1219,6 @@ impl Buffer { pub fn undo(&mut self) -> Option<(TransactionId, Operation)> { if let Some(transaction) = self.history.pop_undo().cloned() { - dbg!(&transaction); let transaction_id = transaction.id; let op = self.undo_or_redo(transaction).unwrap(); Some((transaction_id, op)) @@ -1286,6 +1291,37 @@ impl Buffer { pub fn subscribe(&mut self) -> Subscription { self.subscriptions.subscribe() } + + pub fn wait_for_edits( + &mut self, + edit_ids: &[clock::Local], + ) -> impl 'static + Future { + let mut futures = Vec::new(); + for edit_id in edit_ids { + if !self.version.observed(*edit_id) { + let (tx, rx) = oneshot::channel(); + self.edit_id_resolvers.entry(*edit_id).or_default().push(tx); + futures.push(rx); + } + } + + async move { + for mut future in futures { + future.recv().await; + } + } + } + + fn resolve_edit(&mut self, edit_id: clock::Local) { + for mut tx in self + .edit_id_resolvers + .remove(&edit_id) + .into_iter() + .flatten() + { + let _ = tx.try_send(()); + } + } } #[cfg(any(test, feature = "test-support"))] From 8d7815456c6bd02606d9f7a151b202340ca2c092 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 18:00:30 +0100 Subject: [PATCH 37/61] Don't apply completion's edit when it wouldn't change the buffer Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 13 ++++++++----- crates/language/src/buffer.rs | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0acfef3086..49612a59f9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1675,11 +1675,14 @@ impl Editor { .log_err(); } else { self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent( - [completion.old_range.clone()], - &completion.new_text, - cx, - ); + let snapshot = buffer.read(cx); + let old_range = completion.old_range.to_offset(&snapshot); + if old_range.len() != completion.new_text.len() + || !snapshot.contains_str_at(old_range.start, &completion.new_text) + { + drop(snapshot); + buffer.edit_with_autoindent([old_range], &completion.new_text, cx); + } }); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 00dee0a57c..f5ea67f5fe 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1888,7 +1888,9 @@ impl Buffer { .await?; if let Some(additional_edits) = resolved_completion.additional_text_edits { this.update(&mut cx, |this, cx| { - this.avoid_grouping_next_transaction(); + if !push_to_history { + this.avoid_grouping_next_transaction(); + } this.start_transaction(); let edit_ids = this.apply_lsp_edits(additional_edits, cx); if let Some(transaction_id) = this.end_transaction(cx) { From 8149bcbb133494aa3caae0e735d92a14a46d61f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 18:43:55 +0100 Subject: [PATCH 38/61] Improve formatting of function autocompletion labels in Rust Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/editor/src/multi_buffer.rs | 2 ++ crates/language/src/buffer.rs | 18 ++++++++++++++---- crates/language/src/language.rs | 21 +++++++++++++++------ crates/language/src/proto.rs | 11 ++++++++--- crates/project/src/project.rs | 2 ++ crates/project/src/worktree.rs | 9 +++++++-- crates/zed/src/language.rs | 21 +++++++++++++++------ 7 files changed, 63 insertions(+), 21 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index bce223fb88..ebac70b73b 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -883,6 +883,7 @@ impl MultiBuffer { completion.old_range.end, ), new_text: completion.new_text, + label: completion.label, lsp_completion: completion.lsp_completion, }) .collect() @@ -939,6 +940,7 @@ impl MultiBuffer { old_range: completion.old_range.start.text_anchor ..completion.old_range.end.text_anchor, new_text: completion.new_text, + label: completion.label, lsp_completion: completion.lsp_completion, }, true, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f5ea67f5fe..23b2a752c6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -119,6 +119,7 @@ pub struct Diagnostic { pub struct Completion { pub old_range: Range, pub new_text: String, + pub label: Option, pub lsp_completion: lsp::CompletionItem, } @@ -203,6 +204,7 @@ pub trait File { &self, buffer_id: u64, position: Anchor, + language: Option>, cx: &mut MutableAppContext, ) -> Task>>>; @@ -286,6 +288,7 @@ impl File for FakeFile { &self, _: u64, _: Anchor, + _: Option>, _: &mut MutableAppContext, ) -> Task>>> { Task::ready(Ok(Default::default())) @@ -1800,10 +1803,11 @@ impl Buffer { } else { return Task::ready(Ok(Default::default())); }; + let language = self.language.clone(); if let Some(file) = file.as_local() { - let server = if let Some(lang) = self.language_server.as_ref() { - lang.server.clone() + let server = if let Some(language_server) = self.language_server.as_ref() { + language_server.server.clone() } else { return Task::ready(Ok(Default::default())); }; @@ -1850,6 +1854,7 @@ impl Buffer { Some(Completion { old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end), new_text, + label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)), lsp_completion, }) } else { @@ -1859,7 +1864,12 @@ impl Buffer { }) }) } else { - file.completions(self.remote_id(), self.anchor_before(position), cx.as_mut()) + file.completions( + self.remote_id(), + self.anchor_before(position), + language, + cx.as_mut(), + ) } } @@ -2668,7 +2678,7 @@ impl Default for Diagnostic { impl Completion { pub fn label(&self) -> &str { - &self.lsp_completion.label + self.label.as_deref().unwrap_or(&self.lsp_completion.label) } pub fn filter_range(&self) -> Range { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 22accb422c..b694e82df1 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -43,8 +43,11 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub trait DiagnosticProcessor: 'static + Send + Sync { +pub trait LspPostProcessor: 'static + Send + Sync { fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); + fn label_for_completion(&self, _completion: &lsp::CompletionItem) -> Option { + None + } } #[derive(Default, Deserialize)] @@ -77,7 +80,7 @@ pub struct BracketPair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) diagnostic_processor: Option>, + pub(crate) lsp_post_processor: Option>, } pub struct Grammar { @@ -144,7 +147,7 @@ impl Language { highlight_map: Default::default(), }) }), - diagnostic_processor: None, + lsp_post_processor: None, } } @@ -188,8 +191,8 @@ impl Language { Ok(self) } - pub fn with_diagnostics_processor(mut self, processor: impl DiagnosticProcessor) -> Self { - self.diagnostic_processor = Some(Box::new(processor)); + pub fn with_lsp_post_processor(mut self, processor: impl LspPostProcessor) -> Self { + self.lsp_post_processor = Some(Box::new(processor)); self } @@ -241,11 +244,17 @@ impl Language { } pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { - if let Some(processor) = self.diagnostic_processor.as_ref() { + if let Some(processor) = self.lsp_post_processor.as_ref() { processor.process_diagnostics(diagnostics); } } + pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { + self.lsp_post_processor + .as_ref() + .and_then(|p| p.label_for_completion(completion)) + } + pub fn brackets(&self) -> &[BracketPair] { &self.config.brackets } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c95735493d..c934e0f3e4 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,4 +1,4 @@ -use crate::{diagnostic_set::DiagnosticEntry, Completion, Diagnostic, Operation}; +use crate::{diagnostic_set::DiagnosticEntry, Completion, Diagnostic, Language, Operation}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::HashSet; @@ -387,7 +387,10 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completio } } -pub fn deserialize_completion(completion: proto::Completion) -> Result> { +pub fn deserialize_completion( + completion: proto::Completion, + language: Option<&Arc>, +) -> Result> { let old_start = completion .old_start .and_then(deserialize_anchor) @@ -396,9 +399,11 @@ pub fn deserialize_completion(completion: proto::Completion) -> Result>, cx: &mut MutableAppContext, ) -> Task>>> { let worktree = self.worktree.read(cx); @@ -1448,7 +1451,9 @@ impl language::File for File { response .completions .into_iter() - .map(language::proto::deserialize_completion) + .map(|completion| { + language::proto::deserialize_completion(completion, language.as_ref()) + }) .collect() }) } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 4901f6ceaa..b268647c0b 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -9,9 +9,9 @@ use std::{str, sync::Arc}; #[folder = "languages"] struct LanguageDir; -struct RustDiagnosticProcessor; +struct RustPostProcessor; -impl DiagnosticProcessor for RustDiagnosticProcessor { +impl LspPostProcessor for RustPostProcessor { fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { lazy_static! { static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); @@ -31,6 +31,15 @@ impl DiagnosticProcessor for RustDiagnosticProcessor { } } } + + fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { + let detail = completion.detail.as_ref()?; + if detail.starts_with("fn(") { + Some(completion.label.replace("(…)", &detail[2..])) + } else { + None + } + } } pub fn build_language_registry() -> LanguageRegistry { @@ -52,7 +61,7 @@ fn rust() -> Language { .unwrap() .with_outline_query(load_query("rust/outline.scm").as_ref()) .unwrap() - .with_diagnostics_processor(RustDiagnosticProcessor) + .with_lsp_post_processor(RustPostProcessor) } fn markdown() -> Language { @@ -72,9 +81,9 @@ fn load_query(path: &str) -> Cow<'static, str> { #[cfg(test)] mod tests { - use language::DiagnosticProcessor; + use language::LspPostProcessor; - use super::RustDiagnosticProcessor; + use super::RustPostProcessor; #[test] fn test_process_rust_diagnostics() { @@ -100,7 +109,7 @@ mod tests { }, ], }; - RustDiagnosticProcessor.process_diagnostics(&mut params); + RustPostProcessor.process_diagnostics(&mut params); assert_eq!(params.diagnostics[0].message, "use of moved value `a`"); From 474ec2199cd75b71d6cefe98d99a5b7e64d47667 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 18:56:04 +0100 Subject: [PATCH 39/61] Improve formatting of variable/field completions for Rust Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/zed/src/language.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index b268647c0b..9a27e117b6 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -34,10 +34,29 @@ impl LspPostProcessor for RustPostProcessor { fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { let detail = completion.detail.as_ref()?; - if detail.starts_with("fn(") { - Some(completion.label.replace("(…)", &detail[2..])) - } else { - None + match completion.kind { + Some( + lsp::CompletionItemKind::CONSTANT + | lsp::CompletionItemKind::FIELD + | lsp::CompletionItemKind::VARIABLE, + ) => { + let mut label = completion.label.clone(); + label.push_str(": "); + label.push_str(detail); + Some(label) + } + Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) => { + lazy_static! { + static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap(); + } + + if detail.starts_with("fn(") { + Some(REGEX.replace(&completion.label, &detail[2..]).to_string()) + } else { + None + } + } + _ => None, } } } From 101add8da3ab4c04d06e387a4a0054bf99ada0be Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Feb 2022 19:12:36 +0100 Subject: [PATCH 40/61] Hide completions when using the mouse Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 49612a59f9..9db6fa0a0c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -867,6 +867,8 @@ impl Editor { } fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { + self.hide_completions(cx); + match phase { SelectPhase::Begin { position, From 88adddb3247d5735d31adb7b6a191055fdad87b9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Feb 2022 16:33:04 -0800 Subject: [PATCH 41/61] Remove theme parameters from buffer/display map's chunks methods Change Chunks to contain highlight ids instead of actual highlight styles. Retrieve the actual highlight style from the theme in the editor element layer. This is to set us up to perform syntax highlighting in other code paths where the theme is not available. --- crates/editor/src/display_map.rs | 17 ++- crates/editor/src/display_map/block_map.rs | 15 +-- crates/editor/src/display_map/fold_map.rs | 17 +-- crates/editor/src/display_map/tab_map.rs | 19 ++- crates/editor/src/display_map/wrap_map.rs | 19 +-- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 49 ++++---- crates/editor/src/multi_buffer.rs | 30 ++--- crates/find/src/find.rs | 2 +- crates/language/src/buffer.rs | 137 +++++++++++---------- crates/language/src/tests.rs | 2 +- 11 files changed, 141 insertions(+), 168 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 8757e7593b..b1f11f4275 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -12,7 +12,6 @@ use language::{Point, Subscription as BufferSubscription}; use std::ops::Range; use sum_tree::Bias; use tab_map::TabMap; -use theme::SyntaxTheme; use wrap_map::WrapMap; pub use block_map::{ @@ -251,16 +250,12 @@ impl DisplaySnapshot { pub fn text_chunks(&self, display_row: u32) -> impl Iterator { self.blocks_snapshot - .chunks(display_row..self.max_point().row() + 1, None) + .chunks(display_row..self.max_point().row() + 1) .map(|h| h.text) } - pub fn chunks<'a>( - &'a self, - display_rows: Range, - theme: Option<&'a SyntaxTheme>, - ) -> DisplayChunks<'a> { - self.blocks_snapshot.chunks(display_rows, theme) + pub fn chunks<'a>(&'a self, display_rows: Range) -> DisplayChunks<'a> { + self.blocks_snapshot.chunks(display_rows) } pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator + 'a { @@ -1122,8 +1117,10 @@ mod tests { ) -> Vec<(String, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in snapshot.chunks(rows, Some(theme)) { - let color = chunk.highlight_style.map(|s| s.color); + for chunk in snapshot.chunks(rows) { + let color = chunk + .highlight_id + .and_then(|id| id.style(theme).map(|s| s.color)); if let Some((last_chunk, last_color)) = chunks.last_mut() { if color == *last_color { last_chunk.push_str(chunk.text); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index f00dd89651..600bfbfad1 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -15,7 +15,6 @@ use std::{ }; use sum_tree::{Bias, SumTree}; use text::{Edit, Point}; -use theme::SyntaxTheme; const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize]; @@ -461,16 +460,12 @@ impl<'a> BlockMapWriter<'a> { impl BlockSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(0..self.transforms.summary().output_rows, None) + self.chunks(0..self.transforms.summary().output_rows) .map(|chunk| chunk.text) .collect() } - pub fn chunks<'a>( - &'a self, - rows: Range, - theme: Option<&'a SyntaxTheme>, - ) -> BlockChunks<'a> { + pub fn chunks<'a>(&'a self, rows: Range) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); let input_end = { @@ -498,7 +493,7 @@ impl BlockSnapshot { cursor.start().1 .0 + overshoot }; BlockChunks { - input_chunks: self.wrap_snapshot.chunks(input_start..input_end, theme), + input_chunks: self.wrap_snapshot.chunks(input_start..input_end), input_chunk: Default::default(), transforms: cursor, output_row: rows.start, @@ -715,7 +710,7 @@ impl<'a> Iterator for BlockChunks<'a> { return Some(Chunk { text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) }, - highlight_style: None, + highlight_id: None, diagnostic: None, }); } @@ -1340,7 +1335,7 @@ mod tests { for start_row in 0..expected_row_count { let expected_text = expected_lines[start_row..].join("\n"); let actual_text = blocks_snapshot - .chunks(start_row as u32..expected_row_count as u32, None) + .chunks(start_row as u32..expected_row_count as u32) .map(|chunk| chunk.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index fd5f1de8f8..ab21977dfa 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -11,7 +11,6 @@ use std::{ sync::atomic::{AtomicUsize, Ordering::SeqCst}, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; -use theme::SyntaxTheme; pub trait ToFoldPoint { fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint; @@ -490,7 +489,7 @@ impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), None) + self.chunks(FoldOffset(0)..self.len()) .map(|c| c.text) .collect() } @@ -630,15 +629,11 @@ impl FoldSnapshot { pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { let start = start.to_offset(self); - self.chunks(start..self.len(), None) + self.chunks(start..self.len()) .flat_map(|chunk| chunk.text.chars()) } - pub fn chunks<'a>( - &'a self, - range: Range, - theme: Option<&'a SyntaxTheme>, - ) -> FoldChunks<'a> { + pub fn chunks<'a>(&'a self, range: Range) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>(); transform_cursor.seek(&range.end, Bias::Right, &()); @@ -651,7 +646,7 @@ impl FoldSnapshot { FoldChunks { transform_cursor, - buffer_chunks: self.buffer_snapshot.chunks(buffer_start..buffer_end, theme), + buffer_chunks: self.buffer_snapshot.chunks(buffer_start..buffer_end), buffer_chunk: None, buffer_offset: buffer_start, output_offset: range.start.0, @@ -976,7 +971,7 @@ impl<'a> Iterator for FoldChunks<'a> { self.output_offset += output_text.len(); return Some(Chunk { text: output_text, - highlight_style: None, + highlight_id: None, diagnostic: None, }); } @@ -1398,7 +1393,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, None) + .chunks(start..end) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 9b33a10b14..9d58e87d8d 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -5,7 +5,6 @@ use parking_lot::Mutex; use std::{cmp, mem, ops::Range}; use sum_tree::Bias; use text::Point; -use theme::SyntaxTheme; pub struct TabMap(Mutex); @@ -35,7 +34,7 @@ impl TabMap { let mut delta = 0; for chunk in old_snapshot .fold_snapshot - .chunks(fold_edit.old.end..max_offset, None) + .chunks(fold_edit.old.end..max_offset) { let patterns: &[_] = &['\t', '\n']; if let Some(ix) = chunk.text.find(patterns) { @@ -110,7 +109,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, None) + .chunks(range.start..line_end) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -124,7 +123,7 @@ impl TabSnapshot { last_line_chars = first_line_chars; } else { for _ in self - .chunks(TabPoint::new(range.end.row(), 0)..range.end, None) + .chunks(TabPoint::new(range.end.row(), 0)..range.end) .flat_map(|chunk| chunk.text.chars()) { last_line_chars += 1; @@ -144,11 +143,7 @@ impl TabSnapshot { self.fold_snapshot.version } - pub fn chunks<'a>( - &'a self, - range: Range, - theme: Option<&'a SyntaxTheme>, - ) -> TabChunks<'a> { + pub fn chunks<'a>(&'a self, range: Range) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); let input_start = input_start.to_offset(&self.fold_snapshot); @@ -163,7 +158,7 @@ impl TabSnapshot { }; TabChunks { - fold_chunks: self.fold_snapshot.chunks(input_start..input_end, theme), + fold_chunks: self.fold_snapshot.chunks(input_start..input_end), column: expanded_char_column, output_position: range.start.0, max_output_position: range.end.0, @@ -182,7 +177,7 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), None) + self.chunks(TabPoint::zero()..self.max_point()) .map(|chunk| chunk.text) .collect() } @@ -495,7 +490,7 @@ mod tests { assert_eq!( expected_text, tabs_snapshot - .chunks(start..end, None) + .chunks(start..end) .map(|c| c.text) .collect::(), "chunks({:?}..{:?})", diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 8b02dbbd15..df1d17c500 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -13,7 +13,6 @@ use smol::future::yield_now; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; use sum_tree::{Bias, Cursor, SumTree}; use text::Patch; -use theme::SyntaxTheme; pub use super::tab_map::TextSummary; pub type WrapEdit = text::Edit; @@ -434,10 +433,8 @@ impl WrapSnapshot { let mut line = String::new(); let mut remaining = None; - let mut chunks = new_tab_snapshot.chunks( - TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), - None, - ); + let mut chunks = new_tab_snapshot + .chunks(TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point()); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { while let Some(chunk) = @@ -562,15 +559,11 @@ impl WrapSnapshot { } pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1, None) + self.chunks(wrap_row..self.max_point().row() + 1) .map(|h| h.text) } - pub fn chunks<'a>( - &'a self, - rows: Range, - theme: Option<&'a SyntaxTheme>, - ) -> WrapChunks<'a> { + pub fn chunks<'a>(&'a self, rows: Range) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(); @@ -583,7 +576,7 @@ impl WrapSnapshot { .to_tab_point(output_end) .min(self.tab_snapshot.max_point()); WrapChunks { - input_chunks: self.tab_snapshot.chunks(input_start..input_end, theme), + input_chunks: self.tab_snapshot.chunks(input_start..input_end), input_chunk: Default::default(), output_position: output_start, max_output_row: rows.end, @@ -1295,7 +1288,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, None) + .chunks(start_row..end_row) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9db6fa0a0c..d97d1f864b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1628,7 +1628,7 @@ impl Editor { .map(|(id, completion)| { StringMatchCandidate::new( id, - completion.label()[completion.filter_range()].into(), + completion.lsp_completion.label[completion.filter_range()].into(), ) }) .collect(), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5fc294bac4..53175c917c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -598,31 +598,32 @@ impl EditorElement { .collect(); } else { let style = &self.settings.style; - let chunks = snapshot - .chunks(rows.clone(), Some(&style.syntax)) - .map(|chunk| { - let highlight = if let Some(severity) = chunk.diagnostic { - let diagnostic_style = super::diagnostic_style(severity, true, style); - let underline = Some(Underline { - color: diagnostic_style.message.text.color, - thickness: 1.0.into(), - squiggly: true, - }); - if let Some(mut highlight) = chunk.highlight_style { - highlight.underline = underline; - Some(highlight) - } else { - Some(HighlightStyle { - underline, - color: style.text.color, - font_properties: style.text.font_properties, - }) - } + let chunks = snapshot.chunks(rows.clone()).map(|chunk| { + let highlight_style = chunk + .highlight_id + .and_then(|highlight_id| highlight_id.style(&style.syntax)); + let highlight = if let Some(severity) = chunk.diagnostic { + let diagnostic_style = super::diagnostic_style(severity, true, style); + let underline = Some(Underline { + color: diagnostic_style.message.text.color, + thickness: 1.0.into(), + squiggly: true, + }); + if let Some(mut highlight) = highlight_style { + highlight.underline = underline; + Some(highlight) } else { - chunk.highlight_style - }; - (chunk.text, highlight) - }); + Some(HighlightStyle { + underline, + color: style.text.color, + font_properties: style.text.font_properties, + }) + } + } else { + highlight_style + }; + (chunk.text, highlight) + }); layout_highlighted_chunks( chunks, &style.text, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index ebac70b73b..8e649b919f 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -125,7 +125,6 @@ pub struct MultiBufferChunks<'a> { range: Range, excerpts: Cursor<'a, Excerpt, usize>, excerpt_chunks: Option>, - theme: Option<&'a SyntaxTheme>, } pub struct MultiBufferBytes<'a> { @@ -1113,9 +1112,7 @@ impl Entity for MultiBuffer { impl MultiBufferSnapshot { pub fn text(&self) -> String { - self.chunks(0..self.len(), None) - .map(|chunk| chunk.text) - .collect() + self.chunks(0..self.len()).map(|chunk| chunk.text).collect() } pub fn reversed_chars_at<'a, T: ToOffset>( @@ -1165,7 +1162,7 @@ impl MultiBufferSnapshot { &'a self, range: Range, ) -> impl Iterator { - self.chunks(range, None).map(|chunk| chunk.text) + self.chunks(range).map(|chunk| chunk.text) } pub fn is_line_blank(&self, row: u32) -> bool { @@ -1323,17 +1320,12 @@ impl MultiBufferSnapshot { result } - pub fn chunks<'a, T: ToOffset>( - &'a self, - range: Range, - theme: Option<&'a SyntaxTheme>, - ) -> MultiBufferChunks<'a> { + pub fn chunks<'a, T: ToOffset>(&'a self, range: Range) -> MultiBufferChunks<'a> { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut chunks = MultiBufferChunks { range: range.clone(), excerpts: self.excerpts.cursor(), excerpt_chunks: None, - theme, }; chunks.seek(range.start); chunks @@ -2116,11 +2108,7 @@ impl Excerpt { } } - fn chunks_in_range<'a>( - &'a self, - range: Range, - theme: Option<&'a SyntaxTheme>, - ) -> ExcerptChunks<'a> { + fn chunks_in_range<'a>(&'a self, range: Range) -> ExcerptChunks<'a> { let content_start = self.range.start.to_offset(&self.buffer); let chunks_start = content_start + range.start; let chunks_end = content_start + cmp::min(range.end, self.text_summary.bytes); @@ -2134,7 +2122,7 @@ impl Excerpt { 0 }; - let content_chunks = self.buffer.chunks(chunks_start..chunks_end, theme); + let content_chunks = self.buffer.chunks(chunks_start..chunks_end); ExcerptChunks { content_chunks, @@ -2333,7 +2321,6 @@ impl<'a> MultiBufferChunks<'a> { if let Some(excerpt) = self.excerpts.item() { self.excerpt_chunks = Some(excerpt.chunks_in_range( self.range.start - self.excerpts.start()..self.range.end - self.excerpts.start(), - self.theme, )); } else { self.excerpt_chunks = None; @@ -2353,9 +2340,8 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } else { self.excerpts.next(&()); let excerpt = self.excerpts.item()?; - self.excerpt_chunks = Some( - excerpt.chunks_in_range(0..self.range.end - self.excerpts.start(), self.theme), - ); + self.excerpt_chunks = + Some(excerpt.chunks_in_range(0..self.range.end - self.excerpts.start())); self.next() } } @@ -3110,7 +3096,7 @@ mod tests { let mut buffer_point_utf16 = buffer_start_point_utf16; for ch in buffer .snapshot() - .chunks(buffer_range.clone(), None) + .chunks(buffer_range.clone()) .flat_map(|c| c.text.chars()) { for _ in 0..ch.len_utf8() { diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 859e9f4923..354016e576 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -607,7 +607,7 @@ async fn regex_search( let mut line = String::new(); let mut line_offset = 0; for (chunk_ix, chunk) in buffer - .chunks(0..buffer.len(), None) + .chunks(0..buffer.len()) .map(|c| c.text) .chain(["\n"]) .enumerate() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 23b2a752c6..635f2973c6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -12,7 +12,7 @@ use crate::{ use anyhow::{anyhow, Result}; use clock::ReplicaId; use futures::FutureExt as _; -use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; +use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use lsp::LanguageServer; use parking_lot::Mutex; @@ -358,7 +358,6 @@ struct BufferChunkHighlights<'a> { next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>, stack: Vec<(usize, HighlightId)>, highlight_map: HighlightMap, - theme: &'a SyntaxTheme, _query_cursor: QueryCursorHandle, } @@ -376,7 +375,7 @@ pub struct BufferChunks<'a> { #[derive(Clone, Copy, Debug, Default)] pub struct Chunk<'a> { pub text: &'a str, - pub highlight_style: Option, + pub highlight_id: Option, pub diagnostic: Option, } @@ -387,7 +386,7 @@ pub(crate) struct Diff { } #[derive(Clone, Copy)] -struct DiagnosticEndpoint { +pub(crate) struct DiagnosticEndpoint { offset: usize, is_start: bool, severity: DiagnosticSeverity, @@ -2117,67 +2116,31 @@ impl BufferSnapshot { None } - pub fn chunks<'a, T: ToOffset>( - &'a self, - range: Range, - theme: Option<&'a SyntaxTheme>, - ) -> BufferChunks<'a> { + pub fn chunks<'a, T: ToOffset>(&'a self, range: Range) -> BufferChunks<'a> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut highlights = None; let mut diagnostic_endpoints = Vec::::new(); - if let Some(theme) = theme { - for entry in self.diagnostics_in_range::<_, usize>(range.clone()) { - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.start, - is_start: true, - severity: entry.diagnostic.severity, - }); - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.end, - is_start: false, - severity: entry.diagnostic.severity, - }); - } - diagnostic_endpoints - .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); - - if let Some((grammar, tree)) = self.grammar().zip(self.tree.as_ref()) { - let mut query_cursor = QueryCursorHandle::new(); - - // TODO - add a Tree-sitter API to remove the need for this. - let cursor = unsafe { - std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut()) - }; - let captures = cursor.set_byte_range(range.clone()).captures( - &grammar.highlights_query, - tree.root_node(), - TextProvider(self.text.as_rope()), - ); - highlights = Some(BufferChunkHighlights { - captures, - next_capture: None, - stack: Default::default(), - highlight_map: grammar.highlight_map(), - _query_cursor: query_cursor, - theme, - }) - } + for entry in self.diagnostics_in_range::<_, usize>(range.clone()) { + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.start, + is_start: true, + severity: entry.diagnostic.severity, + }); + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.end, + is_start: false, + severity: entry.diagnostic.severity, + }); } + diagnostic_endpoints.sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); - let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable(); - let chunks = self.text.as_rope().chunks_in_range(range.clone()); - - BufferChunks { + BufferChunks::new( + self.text.as_rope(), range, - chunks, + self.tree.as_ref(), + self.grammar(), diagnostic_endpoints, - error_depth: 0, - warning_depth: 0, - information_depth: 0, - hint_depth: 0, - highlights, - } + ) } pub fn language(&self) -> Option<&Arc> { @@ -2218,7 +2181,7 @@ impl BufferSnapshot { TextProvider(self.as_rope()), ); - let mut chunks = self.chunks(0..self.len(), theme); + let mut chunks = self.chunks(0..self.len()); let item_capture_ix = grammar.outline_query.capture_index_for_name("item")?; let name_capture_ix = grammar.outline_query.capture_index_for_name("name")?; @@ -2272,7 +2235,11 @@ impl BufferSnapshot { } else { offset += chunk.text.len(); } - if let Some(style) = chunk.highlight_style { + let style = chunk + .highlight_id + .zip(theme) + .and_then(|(highlight, theme)| highlight.style(theme)); + if let Some(style) = style { let start = text.len(); let end = start + chunk.text.len(); highlight_ranges.push((start..end, style)); @@ -2460,6 +2427,50 @@ impl<'a> Iterator for ByteChunks<'a> { unsafe impl<'a> Send for BufferChunks<'a> {} impl<'a> BufferChunks<'a> { + pub(crate) fn new( + text: &'a Rope, + range: Range, + tree: Option<&'a Tree>, + grammar: Option<&'a Arc>, + diagnostic_endpoints: Vec, + ) -> Self { + let mut highlights = None; + if let Some((grammar, tree)) = grammar.zip(tree) { + let mut query_cursor = QueryCursorHandle::new(); + + // TODO - add a Tree-sitter API to remove the need for this. + let cursor = unsafe { + std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut()) + }; + let captures = cursor.set_byte_range(range.clone()).captures( + &grammar.highlights_query, + tree.root_node(), + TextProvider(text), + ); + highlights = Some(BufferChunkHighlights { + captures, + next_capture: None, + stack: Default::default(), + highlight_map: grammar.highlight_map(), + _query_cursor: query_cursor, + }) + } + + let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable(); + let chunks = text.chunks_in_range(range.clone()); + + BufferChunks { + range, + chunks, + diagnostic_endpoints, + error_depth: 0, + warning_depth: 0, + information_depth: 0, + hint_depth: 0, + highlights, + } + } + pub fn seek(&mut self, offset: usize) { self.range.start = offset; self.chunks.seek(self.range.start); @@ -2568,11 +2579,11 @@ impl<'a> Iterator for BufferChunks<'a> { let mut chunk_end = (self.chunks.offset() + chunk.len()) .min(next_capture_start) .min(next_diagnostic_endpoint); - let mut highlight_style = None; + let mut highlight_id = None; if let Some(highlights) = self.highlights.as_ref() { if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() { chunk_end = chunk_end.min(*parent_capture_end); - highlight_style = parent_highlight_id.style(highlights.theme); + highlight_id = Some(*parent_highlight_id); } } @@ -2585,7 +2596,7 @@ impl<'a> Iterator for BufferChunks<'a> { Some(Chunk { text: slice, - highlight_style, + highlight_id, diagnostic: self.current_diagnostic_severity(), }) } else { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 065ca28cec..bab62c2350 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -1090,7 +1090,7 @@ fn chunks_with_diagnostics( range: Range, ) -> Vec<(String, Option)> { let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) { + for chunk in buffer.snapshot().chunks(range) { if chunks .last() .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic) From bbdf62f2635791b4576b50aa7642424d70667418 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Feb 2022 17:01:48 -0800 Subject: [PATCH 42/61] Introduce Language::highlight_text method --- crates/language/src/buffer.rs | 33 +++-------------------- crates/language/src/language.rs | 47 ++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 635f2973c6..bcc3d1c6c1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,7 +21,6 @@ use similar::{ChangeTag, TextDiff}; use smol::future::yield_now; use std::{ any::Any, - cell::RefCell, cmp::{self, Ordering}, collections::{BTreeMap, HashMap}, ffi::OsString, @@ -38,7 +37,7 @@ use sum_tree::TreeMap; use text::{operation_queue::OperationQueue, rope::TextDimension}; pub use text::{Buffer as TextBuffer, Operation as _, *}; use theme::SyntaxTheme; -use tree_sitter::{InputEdit, Parser, QueryCursor, Tree}; +use tree_sitter::{InputEdit, QueryCursor, Tree}; use util::{post_inc, TryFutureExt as _}; #[cfg(any(test, feature = "test-support"))] @@ -46,10 +45,6 @@ pub use tree_sitter_rust; pub use lsp::DiagnosticSeverity; -thread_local! { - static PARSER: RefCell = RefCell::new(Parser::new()); -} - lazy_static! { static ref QUERY_CURSORS: Mutex> = Default::default(); } @@ -351,7 +346,7 @@ struct IndentSuggestion { indent: bool, } -struct TextProvider<'a>(&'a Rope); +pub(crate) struct TextProvider<'a>(pub(crate) &'a Rope); struct BufferChunkHighlights<'a> { captures: tree_sitter::QueryCaptures<'a, 'a, TextProvider<'a>>, @@ -938,7 +933,7 @@ impl Buffer { let parsed_version = self.version(); let parse_task = cx.background().spawn({ let grammar = grammar.clone(); - async move { Self::parse_text(&text, old_tree, &grammar) } + async move { grammar.parse_text(&text, old_tree) } }); match cx @@ -974,26 +969,6 @@ impl Buffer { false } - fn parse_text(text: &Rope, old_tree: Option, grammar: &Grammar) -> Tree { - PARSER.with(|parser| { - let mut parser = parser.borrow_mut(); - parser - .set_language(grammar.ts_language) - .expect("incompatible grammar"); - let mut chunks = text.chunks_in_range(0..text.len()); - let tree = parser - .parse_with( - &mut move |offset, _| { - chunks.seek(offset); - chunks.next().unwrap_or("").as_bytes() - }, - old_tree.as_ref(), - ) - .unwrap(); - tree - }) - } - fn interpolate_tree(&self, tree: &mut SyntaxTree) { for edit in self.edits_since::<(usize, Point)>(&tree.version) { let (bytes, lines) = edit.flatten(); @@ -2414,7 +2389,7 @@ impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> { } } -struct ByteChunks<'a>(rope::Chunks<'a>); +pub(crate) struct ByteChunks<'a>(rope::Chunks<'a>); impl<'a> Iterator for ByteChunks<'a> { type Item = &'a [u8]; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b694e82df1..1dc35e0efa 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -17,11 +17,15 @@ use lazy_static::lazy_static; pub use outline::{Outline, OutlineItem}; use parking_lot::Mutex; use serde::Deserialize; -use std::{ops::Range, path::Path, str, sync::Arc}; +use std::{cell::RefCell, ops::Range, path::Path, str, sync::Arc}; use theme::SyntaxTheme; use tree_sitter::{self, Query}; pub use tree_sitter::{Parser, Tree}; +thread_local! { + static PARSER: RefCell = RefCell::new(Parser::new()); +} + lazy_static! { pub static ref PLAIN_TEXT: Arc = Arc::new(Language::new( LanguageConfig { @@ -255,6 +259,28 @@ impl Language { .and_then(|p| p.label_for_completion(completion)) } + pub fn highlight_text<'a>(&'a self, text: &'a Rope) -> Vec<(Range, HighlightId)> { + let mut result = Vec::new(); + if let Some(grammar) = &self.grammar { + let tree = grammar.parse_text(text, None); + let mut offset = 0; + for chunk in BufferChunks::new( + text, + 0..text.len(), + Some(&tree), + self.grammar.as_ref(), + vec![], + ) { + let end_offset = offset + chunk.text.len(); + if let Some(highlight_id) = chunk.highlight_id { + result.push((offset..end_offset, highlight_id)); + } + offset = end_offset; + } + } + result + } + pub fn brackets(&self) -> &[BracketPair] { &self.config.brackets } @@ -268,6 +294,25 @@ impl Language { } impl Grammar { + fn parse_text(&self, text: &Rope, old_tree: Option) -> Tree { + PARSER.with(|parser| { + let mut parser = parser.borrow_mut(); + parser + .set_language(self.ts_language) + .expect("incompatible grammar"); + let mut chunks = text.chunks_in_range(0..text.len()); + parser + .parse_with( + &mut move |offset, _| { + chunks.seek(offset); + chunks.next().unwrap_or("").as_bytes() + }, + old_tree.as_ref(), + ) + .unwrap() + }) + } + pub fn highlight_map(&self) -> HighlightMap { self.highlight_map.lock().clone() } From 45898daf83df32ba2f79e065838e62d26a1b6e58 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Feb 2022 17:09:36 -0800 Subject: [PATCH 43/61] Fix hang in editor completion unit test --- crates/editor/src/editor.rs | 12 +++++++----- crates/language/src/buffer.rs | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d97d1f864b..acb489efff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6826,6 +6826,7 @@ mod tests { .with_language_server(language_server, cx) }); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + buffer.next_notification(&cx).await; let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); @@ -6881,6 +6882,7 @@ mod tests { ); apply_additional_edits }); + let (id, _) = fake .receive_request::() .await; @@ -6900,11 +6902,11 @@ mod tests { assert_eq!( editor.read_with(&cx, |editor, cx| editor.text(cx)), " - one.second_completion - two - three - additional edit - " + one.second_completion + two + three + additional edit + " .unindent() ); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index bcc3d1c6c1..9342e956fb 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -746,6 +746,7 @@ impl Buffer { Operation::UpdateCompletionTriggers { triggers }, cx, ); + cx.notify(); }); } else { return; From 439d12cb8550a095c43c7e2eaa425d4fc4df7ec5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Feb 2022 18:14:30 -0800 Subject: [PATCH 44/61] Start work on syntax highlighting completions --- crates/editor/src/editor.rs | 178 ++++++++++++++++++++++++--- crates/language/src/buffer.rs | 24 +--- crates/language/src/highlight_map.rs | 2 +- crates/language/src/language.rs | 64 ++++++++-- crates/language/src/proto.rs | 8 +- crates/outline/src/outline.rs | 158 +----------------------- crates/zed/src/language.rs | 136 +++++++++++++++++--- 7 files changed, 349 insertions(+), 221 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index acb489efff..8d99bd0e47 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20,7 +20,7 @@ use gpui::{ color::Color, elements::*, executor, - fonts::TextStyle, + fonts::{self, HighlightStyle, TextStyle}, geometry::vector::{vec2f, Vector2F}, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, @@ -489,7 +489,7 @@ impl CompletionState { }); for mat in &mut matches { - let filter_start = self.completions[mat.candidate_id].filter_range().start; + let filter_start = self.completions[mat.candidate_id].label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } @@ -1628,7 +1628,7 @@ impl Editor { .map(|(id, completion)| { StringMatchCandidate::new( id, - completion.lsp_completion.label[completion.filter_range()].into(), + completion.label.text[completion.label.filter_range.clone()].into(), ) }) .collect(), @@ -1710,15 +1710,6 @@ impl Editor { move |range, items, cx| { let settings = build_settings(cx); let start_ix = range.start; - let label_style = LabelStyle { - text: settings.style.text.clone(), - highlight_text: settings - .style - .text - .clone() - .highlight(settings.style.autocomplete.match_highlight, cx.font_cache()) - .log_err(), - }; for (ix, mat) in matches[range].iter().enumerate() { let item_style = if start_ix + ix == selected_item { settings.style.autocomplete.selected_item @@ -1727,8 +1718,20 @@ impl Editor { }; let completion = &completions[mat.candidate_id]; items.push( - Label::new(completion.label().to_string(), label_style.clone()) - .with_highlights(mat.positions.clone()) + Text::new(completion.label.text.clone(), settings.style.text.clone()) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + settings.style.text.color.into(), + completion.label.runs.iter().filter_map( + |(range, highlight_id)| { + highlight_id + .style(&settings.style.syntax) + .map(|style| (range.clone(), style)) + }, + ), + &mat.positions, + )) .contained() .with_style(item_style) .boxed(), @@ -1742,7 +1745,11 @@ impl Editor { .iter() .enumerate() .max_by_key(|(_, mat)| { - state.completions[mat.candidate_id].label().chars().count() + state.completions[mat.candidate_id] + .label + .text + .chars() + .count() }) .map(|(ix, _)| ix), ) @@ -4699,6 +4706,77 @@ pub fn settings_builder( }) } +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: HighlightStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); + + for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) + { + syntax_highlight.font_properties.weight(Default::default()); + + // Add highlights for any fuzzy match characters before the next + // syntax highlight range. + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.start { + break; + } + match_indices.next(); + let end_index = char_ix_after(match_index, text); + let mut match_style = default_style; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + } + + if range.start == usize::MAX { + break; + } + + // Add highlights for any fuzzy match characters within the + // syntax highlight range. + let mut offset = range.start; + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.end { + break; + } + + match_indices.next(); + if match_index > offset { + result.push((offset..match_index, syntax_highlight)); + } + + let mut end_index = char_ix_after(match_index, text); + while let Some(&next_match_index) = match_indices.peek() { + if next_match_index == end_index && next_match_index < range.end { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } + + let mut match_style = syntax_highlight; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + offset = end_index; + } + + if offset < range.end { + result.push((offset..range.end, syntax_highlight)); + } + } + + fn char_ix_after(ix: usize, text: &str) -> usize { + ix + text[ix..].chars().next().unwrap().len_utf8() + } + + result +} + #[cfg(test)] mod tests { use super::*; @@ -7327,6 +7405,76 @@ mod tests { }); } + #[test] + fn test_combine_syntax_and_fuzzy_match_highlights() { + let string = "abcdefghijklmnop"; + let default = HighlightStyle::default(); + let syntax_ranges = [ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..8, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ]; + let match_indices = [4, 6, 7, 8]; + assert_eq!( + combine_syntax_and_fuzzy_match_highlights( + &string, + default, + syntax_ranges.into_iter(), + &match_indices, + ), + &[ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..5, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 5..6, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ( + 6..8, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 8..9, + HighlightStyle { + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ] + ); + } + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9342e956fb..8c026b08bf 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -7,7 +7,7 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, outline::OutlineItem, - range_from_lsp, Outline, ToLspPosition, + range_from_lsp, CompletionLabel, Outline, ToLspPosition, }; use anyhow::{anyhow, Result}; use clock::ReplicaId; @@ -114,7 +114,7 @@ pub struct Diagnostic { pub struct Completion { pub old_range: Range, pub new_text: String, - pub label: Option, + pub label: CompletionLabel, pub lsp_completion: lsp::CompletionItem, } @@ -1829,7 +1829,7 @@ impl Buffer { Some(Completion { old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end), new_text, - label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)), + label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)).unwrap_or_else(|| CompletionLabel::plain(&lsp_completion)), lsp_completion, }) } else { @@ -2664,28 +2664,12 @@ impl Default for Diagnostic { } impl Completion { - pub fn label(&self) -> &str { - self.label.as_deref().unwrap_or(&self.lsp_completion.label) - } - - pub fn filter_range(&self) -> Range { - if let Some(filter_text) = self.lsp_completion.filter_text.as_deref() { - if let Some(start) = self.label().find(filter_text) { - start..start + filter_text.len() - } else { - 0..self.label().len() - } - } else { - 0..self.label().len() - } - } - pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { Some(lsp::CompletionItemKind::VARIABLE) => 0, _ => 1, }; - (kind_key, &self.label()[self.filter_range()]) + (kind_key, &self.label.text[self.label.filter_range.clone()]) } pub fn is_snippet(&self) -> bool { diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 5e1c7e4304..75e3c8526c 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -5,7 +5,7 @@ use theme::SyntaxTheme; #[derive(Clone, Debug)] pub struct HighlightMap(Arc<[HighlightId]>); -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct HighlightId(pub u32); const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1dc35e0efa..e54e0f49a4 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -49,11 +49,23 @@ pub trait ToLspPosition { pub trait LspPostProcessor: 'static + Send + Sync { fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); - fn label_for_completion(&self, _completion: &lsp::CompletionItem) -> Option { + fn label_for_completion( + &self, + _: &lsp::CompletionItem, + _: &Language, + ) -> Option { None } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CompletionLabel { + pub text: String, + pub runs: Vec<(Range, HighlightId)>, + pub filter_range: Range, + pub left_aligned_len: usize, +} + #[derive(Default, Deserialize)] pub struct LanguageConfig { pub name: String, @@ -253,24 +265,26 @@ impl Language { } } - pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { + pub fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + ) -> Option { self.lsp_post_processor - .as_ref() - .and_then(|p| p.label_for_completion(completion)) + .as_ref()? + .label_for_completion(completion, self) } - pub fn highlight_text<'a>(&'a self, text: &'a Rope) -> Vec<(Range, HighlightId)> { + pub fn highlight_text<'a>( + &'a self, + text: &'a Rope, + range: Range, + ) -> Vec<(Range, HighlightId)> { let mut result = Vec::new(); if let Some(grammar) = &self.grammar { let tree = grammar.parse_text(text, None); let mut offset = 0; - for chunk in BufferChunks::new( - text, - 0..text.len(), - Some(&tree), - self.grammar.as_ref(), - vec![], - ) { + for chunk in BufferChunks::new(text, range, Some(&tree), self.grammar.as_ref(), vec![]) + { let end_offset = offset + chunk.text.len(); if let Some(highlight_id) = chunk.highlight_id { result.push((offset..end_offset, highlight_id)); @@ -291,6 +305,10 @@ impl Language { HighlightMap::new(grammar.highlights_query.capture_names(), theme); } } + + pub fn grammar(&self) -> Option<&Arc> { + self.grammar.as_ref() + } } impl Grammar { @@ -316,6 +334,28 @@ impl Grammar { pub fn highlight_map(&self) -> HighlightMap { self.highlight_map.lock().clone() } + + pub fn highlight_id_for_name(&self, name: &str) -> Option { + let capture_id = self.highlights_query.capture_index_for_name(name)?; + Some(self.highlight_map.lock().get(capture_id)) + } +} + +impl CompletionLabel { + fn plain(completion: &lsp::CompletionItem) -> Self { + let mut result = Self { + text: completion.label.clone(), + runs: Vec::new(), + left_aligned_len: completion.label.len(), + filter_range: 0..completion.label.len(), + }; + if let Some(filter_text) = &completion.filter_text { + if let Some(ix) = completion.label.find(filter_text) { + result.filter_range = ix..ix + filter_text.len(); + } + } + result + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c934e0f3e4..82787ec571 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,4 +1,6 @@ -use crate::{diagnostic_set::DiagnosticEntry, Completion, Diagnostic, Language, Operation}; +use crate::{ + diagnostic_set::DiagnosticEntry, Completion, CompletionLabel, Diagnostic, Language, Operation, +}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::HashSet; @@ -403,7 +405,9 @@ pub fn deserialize_completion( Ok(Completion { old_range: old_start..old_end, new_text: completion.new_text, - label: language.and_then(|l| l.label_for_completion(&lsp_completion)), + label: language + .and_then(|l| l.label_for_completion(&lsp_completion)) + .unwrap_or(CompletionLabel::plain(&lsp_completion)), lsp_completion, }) } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index b6921ce69f..4518ae6afc 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,12 +1,11 @@ use editor::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, - EditorSettings, ToPoint, + combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, Anchor, AnchorRangeExt, + Autoscroll, DisplayPoint, Editor, EditorSettings, ToPoint, }; use fuzzy::StringMatch; use gpui::{ action, elements::*, - fonts::{self, HighlightStyle}, geometry::vector::Vector2F, keymap::{self, Binding}, AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, @@ -17,7 +16,6 @@ use ordered_float::OrderedFloat; use postage::watch; use std::{ cmp::{self, Reverse}, - ops::Range, sync::Arc, }; use workspace::{ @@ -362,7 +360,7 @@ impl OutlineView { .with_highlights(combine_syntax_and_fuzzy_match_highlights( &outline_item.text, style.label.text.clone().into(), - &outline_item.highlight_ranges, + outline_item.highlight_ranges.iter().cloned(), &string_match.positions, )) .contained() @@ -372,153 +370,3 @@ impl OutlineView { .boxed() } } - -fn combine_syntax_and_fuzzy_match_highlights( - text: &str, - default_style: HighlightStyle, - syntax_ranges: &[(Range, HighlightStyle)], - match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut result = Vec::new(); - let mut match_indices = match_indices.iter().copied().peekable(); - - for (range, mut syntax_highlight) in syntax_ranges - .iter() - .cloned() - .chain([(usize::MAX..0, Default::default())]) - { - syntax_highlight.font_properties.weight(Default::default()); - - // Add highlights for any fuzzy match characters before the next - // syntax highlight range. - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.start { - break; - } - match_indices.next(); - let end_index = char_ix_after(match_index, text); - let mut match_style = default_style; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - } - - if range.start == usize::MAX { - break; - } - - // Add highlights for any fuzzy match characters within the - // syntax highlight range. - let mut offset = range.start; - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.end { - break; - } - - match_indices.next(); - if match_index > offset { - result.push((offset..match_index, syntax_highlight)); - } - - let mut end_index = char_ix_after(match_index, text); - while let Some(&next_match_index) = match_indices.peek() { - if next_match_index == end_index && next_match_index < range.end { - end_index = char_ix_after(next_match_index, text); - match_indices.next(); - } else { - break; - } - } - - let mut match_style = syntax_highlight; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - offset = end_index; - } - - if offset < range.end { - result.push((offset..range.end, syntax_highlight)); - } - } - - result -} - -fn char_ix_after(ix: usize, text: &str) -> usize { - ix + text[ix..].chars().next().unwrap().len_utf8() -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{color::Color, fonts::HighlightStyle}; - - #[test] - fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let default = HighlightStyle::default(); - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..8, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - &string, - default, - &syntax_ranges, - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..5, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 5..6, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ( - 6..8, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 8..9, - HighlightStyle { - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ] - ); - } -} diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 9a27e117b6..b0634b4eee 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -32,18 +32,36 @@ impl LspPostProcessor for RustPostProcessor { } } - fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { + fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Language, + ) -> Option { let detail = completion.detail.as_ref()?; match completion.kind { - Some( - lsp::CompletionItemKind::CONSTANT - | lsp::CompletionItemKind::FIELD - | lsp::CompletionItemKind::VARIABLE, - ) => { - let mut label = completion.label.clone(); - label.push_str(": "); - label.push_str(detail); - Some(label) + Some(lsp::CompletionItemKind::FIELD) => { + let name = &completion.label; + let text = format!("{}: {}", name, detail); + let source = Rope::from(format!("struct S {{ {} }}", text).as_str()); + let runs = language.highlight_text(&source, 11..11 + text.len()); + return Some(CompletionLabel { + text, + runs, + filter_range: 0..name.len(), + left_aligned_len: name.len(), + }); + } + Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE) => { + let name = &completion.label; + let text = format!("{}: {}", name, detail); + let source = Rope::from(format!("let {} = ();", text).as_str()); + let runs = language.highlight_text(&source, 4..4 + text.len()); + return Some(CompletionLabel { + text, + runs, + filter_range: 0..name.len(), + left_aligned_len: name.len(), + }); } Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) => { lazy_static! { @@ -51,13 +69,20 @@ impl LspPostProcessor for RustPostProcessor { } if detail.starts_with("fn(") { - Some(REGEX.replace(&completion.label, &detail[2..]).to_string()) - } else { - None + let text = REGEX.replace(&completion.label, &detail[2..]).to_string(); + let source = Rope::from(format!("fn {} {{}}", text).as_str()); + let runs = language.highlight_text(&source, 3..3 + text.len()); + return Some(CompletionLabel { + left_aligned_len: text.find("->").unwrap_or(text.len()), + filter_range: 0..completion.label.find('(').unwrap_or(text.len()), + text, + runs, + }); } } - _ => None, + _ => {} } + None } } @@ -100,9 +125,10 @@ fn load_query(path: &str) -> Cow<'static, str> { #[cfg(test)] mod tests { + use super::*; + use gpui::color::Color; use language::LspPostProcessor; - - use super::RustPostProcessor; + use theme::SyntaxTheme; #[test] fn test_process_rust_diagnostics() { @@ -144,4 +170,82 @@ mod tests { "cannot borrow `self.d` as mutable\n`self` is a `&` reference" ); } + + #[test] + fn test_process_rust_completions() { + let language = rust(); + let grammar = language.grammar().unwrap(); + let theme = SyntaxTheme::new(vec![ + ("type".into(), Color::green().into()), + ("keyword".into(), Color::blue().into()), + ("function".into(), Color::red().into()), + ("property".into(), Color::white().into()), + ]); + + language.set_theme(&theme); + + let highlight_function = grammar.highlight_id_for_name("function").unwrap(); + let highlight_type = grammar.highlight_id_for_name("type").unwrap(); + let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); + let highlight_field = grammar.highlight_id_for_name("property").unwrap(); + + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }), + Some(CompletionLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + left_aligned_len: 22, + }) + ); + + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FIELD), + label: "len".to_string(), + detail: Some("usize".to_string()), + ..Default::default() + }), + Some(CompletionLabel { + text: "len: usize".to_string(), + filter_range: 0..3, + runs: vec![(0..3, highlight_field), (5..10, highlight_type),], + left_aligned_len: 3, + }) + ); + + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }), + Some(CompletionLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + left_aligned_len: 22, + }) + ); + } } From 8fc89da5737c04a62f3702c3594851b44e450ca2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 08:46:45 +0100 Subject: [PATCH 45/61] Don't overflow selection index when completions are filtered --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d99bd0e47..8cb2891fa2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2603,7 +2603,7 @@ impl Editor { pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { if let Some(completion_state) = &mut self.completion_state { - if completion_state.selected_item + 1 < completion_state.completions.len() { + if completion_state.selected_item + 1 < completion_state.matches.len() { completion_state.selected_item += 1; completion_state .list From d246a39b57313909ea02964d181f6e0f66bc0d05 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 09:39:21 +0100 Subject: [PATCH 46/61] Syntax highlight even when the label doesn't contain a detail --- crates/language/src/language.rs | 2 +- crates/zed/src/language.rs | 34 +++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e54e0f49a4..4ebe6552cd 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -342,7 +342,7 @@ impl Grammar { } impl CompletionLabel { - fn plain(completion: &lsp::CompletionItem) -> Self { + pub fn plain(completion: &lsp::CompletionItem) -> Self { let mut result = Self { text: completion.label.clone(), runs: Vec::new(), diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index b0634b4eee..a1ad424c11 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -37,9 +37,9 @@ impl LspPostProcessor for RustPostProcessor { completion: &lsp::CompletionItem, language: &Language, ) -> Option { - let detail = completion.detail.as_ref()?; match completion.kind { - Some(lsp::CompletionItemKind::FIELD) => { + Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => { + let detail = completion.detail.as_ref().unwrap(); let name = &completion.label; let text = format!("{}: {}", name, detail); let source = Rope::from(format!("struct S {{ {} }}", text).as_str()); @@ -51,7 +51,10 @@ impl LspPostProcessor for RustPostProcessor { left_aligned_len: name.len(), }); } - Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE) => { + Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE) + if completion.detail.is_some() => + { + let detail = completion.detail.as_ref().unwrap(); let name = &completion.label; let text = format!("{}: {}", name, detail); let source = Rope::from(format!("let {} = ();", text).as_str()); @@ -63,11 +66,14 @@ impl LspPostProcessor for RustPostProcessor { left_aligned_len: name.len(), }); } - Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) => { + Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) + if completion.detail.is_some() => + { lazy_static! { static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap(); } + let detail = completion.detail.as_ref().unwrap(); if detail.starts_with("fn(") { let text = REGEX.replace(&completion.label, &detail[2..]).to_string(); let source = Rope::from(format!("fn {} {{}}", text).as_str()); @@ -80,6 +86,26 @@ impl LspPostProcessor for RustPostProcessor { }); } } + Some(kind) => { + let highlight_name = match kind { + lsp::CompletionItemKind::STRUCT + | lsp::CompletionItemKind::INTERFACE + | lsp::CompletionItemKind::ENUM => Some("type"), + lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"), + lsp::CompletionItemKind::KEYWORD => Some("keyword"), + lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => { + Some("constant") + } + _ => None, + }; + let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?; + let mut label = CompletionLabel::plain(&completion); + label.runs.push(( + 0..label.text.rfind('(').unwrap_or(label.text.len()), + highlight_id, + )); + return Some(label); + } _ => {} } None From 426ca94b520b48dbcfacfa43b80afb90df3f27a0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 10:23:34 +0100 Subject: [PATCH 47/61] Allow completions to bleed off the editor's bounds --- crates/editor/src/element.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 53175c917c..31926eb972 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -897,7 +897,10 @@ impl Element for EditorElement { completions_list.layout( SizeConstraint { min: Vector2F::zero(), - max: vec2f(800., (12. * line_height).min((size.y() - line_height) / 2.)), + max: vec2f( + f32::INFINITY, + (12. * line_height).min((size.y() - line_height) / 2.), + ), }, cx, ); From bd441723a0466cf8d7b7f11c2c20d356979d9340 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 10:37:15 +0100 Subject: [PATCH 48/61] Cancel innermost snippet insertion when hitting `esc` --- crates/editor/src/editor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8cb2891fa2..30ef12b886 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1171,6 +1171,10 @@ impl Editor { return; } + if self.snippet_stack.pop().is_some() { + return; + } + if self.mode != EditorMode::Full { cx.propagate_action(); return; From ab26a175a4f904ed40b40266b5270e90ebf979a4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 11:21:30 +0100 Subject: [PATCH 49/61] Opt into language-aware features when getting buffer chunks We use chunks a lot to transform points and sync the various display maps, and always querying tree-sitter or the LSP diagnostics in those cases is unnecessarily expensive. --- crates/editor/src/display_map.rs | 12 ++++--- crates/editor/src/display_map/block_map.rs | 10 +++--- crates/editor/src/display_map/fold_map.rs | 12 ++++--- crates/editor/src/display_map/tab_map.rs | 16 +++++---- crates/editor/src/display_map/wrap_map.rs | 16 +++++---- crates/editor/src/element.rs | 2 +- crates/editor/src/multi_buffer.rs | 31 +++++++++++----- crates/find/src/find.rs | 2 +- crates/language/src/buffer.rs | 41 +++++++++++++--------- crates/language/src/tests.rs | 2 +- 10 files changed, 91 insertions(+), 53 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index b1f11f4275..697dc5ea62 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -250,12 +250,16 @@ impl DisplaySnapshot { pub fn text_chunks(&self, display_row: u32) -> impl Iterator { self.blocks_snapshot - .chunks(display_row..self.max_point().row() + 1) + .chunks(display_row..self.max_point().row() + 1, false) .map(|h| h.text) } - pub fn chunks<'a>(&'a self, display_rows: Range) -> DisplayChunks<'a> { - self.blocks_snapshot.chunks(display_rows) + pub fn chunks<'a>( + &'a self, + display_rows: Range, + language_aware: bool, + ) -> DisplayChunks<'a> { + self.blocks_snapshot.chunks(display_rows, language_aware) } pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator + 'a { @@ -1117,7 +1121,7 @@ mod tests { ) -> Vec<(String, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in snapshot.chunks(rows) { + for chunk in snapshot.chunks(rows, true) { let color = chunk .highlight_id .and_then(|id| id.style(theme).map(|s| s.color)); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 600bfbfad1..96848016a9 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -460,12 +460,12 @@ impl<'a> BlockMapWriter<'a> { impl BlockSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(0..self.transforms.summary().output_rows) + self.chunks(0..self.transforms.summary().output_rows, false) .map(|chunk| chunk.text) .collect() } - pub fn chunks<'a>(&'a self, rows: Range) -> BlockChunks<'a> { + pub fn chunks<'a>(&'a self, rows: Range, language_aware: bool) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); let input_end = { @@ -493,7 +493,9 @@ impl BlockSnapshot { cursor.start().1 .0 + overshoot }; BlockChunks { - input_chunks: self.wrap_snapshot.chunks(input_start..input_end), + input_chunks: self + .wrap_snapshot + .chunks(input_start..input_end, language_aware), input_chunk: Default::default(), transforms: cursor, output_row: rows.start, @@ -1335,7 +1337,7 @@ mod tests { for start_row in 0..expected_row_count { let expected_text = expected_lines[start_row..].join("\n"); let actual_text = blocks_snapshot - .chunks(start_row as u32..expected_row_count as u32) + .chunks(start_row as u32..expected_row_count as u32, false) .map(|chunk| chunk.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index ab21977dfa..a23f6ad010 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -489,7 +489,7 @@ impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len()) + self.chunks(FoldOffset(0)..self.len(), false) .map(|c| c.text) .collect() } @@ -629,11 +629,11 @@ impl FoldSnapshot { pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { let start = start.to_offset(self); - self.chunks(start..self.len()) + self.chunks(start..self.len(), false) .flat_map(|chunk| chunk.text.chars()) } - pub fn chunks<'a>(&'a self, range: Range) -> FoldChunks<'a> { + pub fn chunks<'a>(&'a self, range: Range, language_aware: bool) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>(); transform_cursor.seek(&range.end, Bias::Right, &()); @@ -646,7 +646,9 @@ impl FoldSnapshot { FoldChunks { transform_cursor, - buffer_chunks: self.buffer_snapshot.chunks(buffer_start..buffer_end), + buffer_chunks: self + .buffer_snapshot + .chunks(buffer_start..buffer_end, language_aware), buffer_chunk: None, buffer_offset: buffer_start, output_offset: range.start.0, @@ -1393,7 +1395,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end) + .chunks(start..end, false) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 9d58e87d8d..e2239e7671 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -34,7 +34,7 @@ impl TabMap { let mut delta = 0; for chunk in old_snapshot .fold_snapshot - .chunks(fold_edit.old.end..max_offset) + .chunks(fold_edit.old.end..max_offset, false) { let patterns: &[_] = &['\t', '\n']; if let Some(ix) = chunk.text.find(patterns) { @@ -109,7 +109,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end) + .chunks(range.start..line_end, false) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -123,7 +123,7 @@ impl TabSnapshot { last_line_chars = first_line_chars; } else { for _ in self - .chunks(TabPoint::new(range.end.row(), 0)..range.end) + .chunks(TabPoint::new(range.end.row(), 0)..range.end, false) .flat_map(|chunk| chunk.text.chars()) { last_line_chars += 1; @@ -143,7 +143,7 @@ impl TabSnapshot { self.fold_snapshot.version } - pub fn chunks<'a>(&'a self, range: Range) -> TabChunks<'a> { + pub fn chunks<'a>(&'a self, range: Range, language_aware: bool) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); let input_start = input_start.to_offset(&self.fold_snapshot); @@ -158,7 +158,9 @@ impl TabSnapshot { }; TabChunks { - fold_chunks: self.fold_snapshot.chunks(input_start..input_end), + fold_chunks: self + .fold_snapshot + .chunks(input_start..input_end, language_aware), column: expanded_char_column, output_position: range.start.0, max_output_position: range.end.0, @@ -177,7 +179,7 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point()) + self.chunks(TabPoint::zero()..self.max_point(), false) .map(|chunk| chunk.text) .collect() } @@ -490,7 +492,7 @@ mod tests { assert_eq!( expected_text, tabs_snapshot - .chunks(start..end) + .chunks(start..end, false) .map(|c| c.text) .collect::(), "chunks({:?}..{:?})", diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index df1d17c500..1d5e64c8a5 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -433,8 +433,10 @@ impl WrapSnapshot { let mut line = String::new(); let mut remaining = None; - let mut chunks = new_tab_snapshot - .chunks(TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point()); + let mut chunks = new_tab_snapshot.chunks( + TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), + false, + ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { while let Some(chunk) = @@ -559,11 +561,11 @@ impl WrapSnapshot { } pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1) + self.chunks(wrap_row..self.max_point().row() + 1, false) .map(|h| h.text) } - pub fn chunks<'a>(&'a self, rows: Range) -> WrapChunks<'a> { + pub fn chunks<'a>(&'a self, rows: Range, language_aware: bool) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(); @@ -576,7 +578,9 @@ impl WrapSnapshot { .to_tab_point(output_end) .min(self.tab_snapshot.max_point()); WrapChunks { - input_chunks: self.tab_snapshot.chunks(input_start..input_end), + input_chunks: self + .tab_snapshot + .chunks(input_start..input_end, language_aware), input_chunk: Default::default(), output_position: output_start, max_output_row: rows.end, @@ -1288,7 +1292,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row) + .chunks(start_row..end_row, true) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 31926eb972..a22e6021e0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -598,7 +598,7 @@ impl EditorElement { .collect(); } else { let style = &self.settings.style; - let chunks = snapshot.chunks(rows.clone()).map(|chunk| { + let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| { let highlight_style = chunk .highlight_id .and_then(|highlight_id| highlight_id.style(&style.syntax)); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8e649b919f..2bdf819956 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -125,6 +125,7 @@ pub struct MultiBufferChunks<'a> { range: Range, excerpts: Cursor<'a, Excerpt, usize>, excerpt_chunks: Option>, + language_aware: bool, } pub struct MultiBufferBytes<'a> { @@ -1112,7 +1113,9 @@ impl Entity for MultiBuffer { impl MultiBufferSnapshot { pub fn text(&self) -> String { - self.chunks(0..self.len()).map(|chunk| chunk.text).collect() + self.chunks(0..self.len(), false) + .map(|chunk| chunk.text) + .collect() } pub fn reversed_chars_at<'a, T: ToOffset>( @@ -1162,7 +1165,7 @@ impl MultiBufferSnapshot { &'a self, range: Range, ) -> impl Iterator { - self.chunks(range).map(|chunk| chunk.text) + self.chunks(range, false).map(|chunk| chunk.text) } pub fn is_line_blank(&self, row: u32) -> bool { @@ -1320,12 +1323,17 @@ impl MultiBufferSnapshot { result } - pub fn chunks<'a, T: ToOffset>(&'a self, range: Range) -> MultiBufferChunks<'a> { + pub fn chunks<'a, T: ToOffset>( + &'a self, + range: Range, + language_aware: bool, + ) -> MultiBufferChunks<'a> { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut chunks = MultiBufferChunks { range: range.clone(), excerpts: self.excerpts.cursor(), excerpt_chunks: None, + language_aware: language_aware, }; chunks.seek(range.start); chunks @@ -2108,7 +2116,11 @@ impl Excerpt { } } - fn chunks_in_range<'a>(&'a self, range: Range) -> ExcerptChunks<'a> { + fn chunks_in_range<'a>( + &'a self, + range: Range, + language_aware: bool, + ) -> ExcerptChunks<'a> { let content_start = self.range.start.to_offset(&self.buffer); let chunks_start = content_start + range.start; let chunks_end = content_start + cmp::min(range.end, self.text_summary.bytes); @@ -2122,7 +2134,7 @@ impl Excerpt { 0 }; - let content_chunks = self.buffer.chunks(chunks_start..chunks_end); + let content_chunks = self.buffer.chunks(chunks_start..chunks_end, language_aware); ExcerptChunks { content_chunks, @@ -2321,6 +2333,7 @@ impl<'a> MultiBufferChunks<'a> { if let Some(excerpt) = self.excerpts.item() { self.excerpt_chunks = Some(excerpt.chunks_in_range( self.range.start - self.excerpts.start()..self.range.end - self.excerpts.start(), + self.language_aware, )); } else { self.excerpt_chunks = None; @@ -2340,8 +2353,10 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } else { self.excerpts.next(&()); let excerpt = self.excerpts.item()?; - self.excerpt_chunks = - Some(excerpt.chunks_in_range(0..self.range.end - self.excerpts.start())); + self.excerpt_chunks = Some(excerpt.chunks_in_range( + 0..self.range.end - self.excerpts.start(), + self.language_aware, + )); self.next() } } @@ -3096,7 +3111,7 @@ mod tests { let mut buffer_point_utf16 = buffer_start_point_utf16; for ch in buffer .snapshot() - .chunks(buffer_range.clone()) + .chunks(buffer_range.clone(), false) .flat_map(|c| c.text.chars()) { for _ in 0..ch.len_utf8() { diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 354016e576..3de2234da1 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -607,7 +607,7 @@ async fn regex_search( let mut line = String::new(); let mut line_offset = 0; for (chunk_ix, chunk) in buffer - .chunks(0..buffer.len()) + .chunks(0..buffer.len(), false) .map(|c| c.text) .chain(["\n"]) .enumerate() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8c026b08bf..2efcbbf0d6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2092,28 +2092,37 @@ impl BufferSnapshot { None } - pub fn chunks<'a, T: ToOffset>(&'a self, range: Range) -> BufferChunks<'a> { + pub fn chunks<'a, T: ToOffset>( + &'a self, + range: Range, + language_aware: bool, + ) -> BufferChunks<'a> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut diagnostic_endpoints = Vec::::new(); - for entry in self.diagnostics_in_range::<_, usize>(range.clone()) { - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.start, - is_start: true, - severity: entry.diagnostic.severity, - }); - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.end, - is_start: false, - severity: entry.diagnostic.severity, - }); + let mut tree = None; + let mut diagnostic_endpoints = Vec::new(); + if language_aware { + tree = self.tree.as_ref(); + for entry in self.diagnostics_in_range::<_, usize>(range.clone()) { + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.start, + is_start: true, + severity: entry.diagnostic.severity, + }); + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.end, + is_start: false, + severity: entry.diagnostic.severity, + }); + } + diagnostic_endpoints + .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); } - diagnostic_endpoints.sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); BufferChunks::new( self.text.as_rope(), range, - self.tree.as_ref(), + tree, self.grammar(), diagnostic_endpoints, ) @@ -2157,7 +2166,7 @@ impl BufferSnapshot { TextProvider(self.as_rope()), ); - let mut chunks = self.chunks(0..self.len()); + let mut chunks = self.chunks(0..self.len(), true); let item_capture_ix = grammar.outline_query.capture_index_for_name("item")?; let name_capture_ix = grammar.outline_query.capture_index_for_name("name")?; diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index bab62c2350..94001c591a 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -1090,7 +1090,7 @@ fn chunks_with_diagnostics( range: Range, ) -> Vec<(String, Option)> { let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in buffer.snapshot().chunks(range) { + for chunk in buffer.snapshot().chunks(range, true) { if chunks .last() .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic) From 7865c327276d4eb7d97f431cc4b64b60f5894adb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 11:22:55 +0100 Subject: [PATCH 50/61] Optimize `summaries_for_anchors` when `MultiBuffer` is a singleton --- crates/editor/src/multi_buffer.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 2bdf819956..1904e00edc 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1543,6 +1543,13 @@ impl MultiBufferSnapshot { D: TextDimension + Ord + Sub, I: 'a + IntoIterator, { + if let Some(excerpt) = self.as_singleton() { + return excerpt + .buffer + .summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor)) + .collect(); + } + let mut anchors = anchors.into_iter().peekable(); let mut cursor = self.excerpts.cursor::(); let mut summaries = Vec::new(); From efcbf2714c49dea16697ade9ed7d53b535bbbc70 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 11:39:19 +0100 Subject: [PATCH 51/61] Support clicking on a completion to confirm it --- crates/editor/src/editor.rs | 95 +++++++++++++++++++---------- crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 4 ++ crates/zed/assets/themes/black.toml | 1 + crates/zed/assets/themes/dark.toml | 2 +- crates/zed/assets/themes/light.toml | 2 +- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30ef12b886..1cf156a72f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -23,6 +23,7 @@ use gpui::{ fonts::{self, HighlightStyle, TextStyle}, geometry::vector::{vec2f, Vector2F}, keymap::Binding, + platform::CursorStyle, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, WeakModelHandle, WeakViewHandle, }; @@ -123,7 +124,7 @@ action!(FoldSelectedRanges); action!(Scroll, Vector2F); action!(Select, SelectPhase); action!(ShowCompletions); -action!(ConfirmCompletion); +action!(ConfirmCompletion, Option); pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { path_openers.push(Box::new(items::BufferOpener)); @@ -139,7 +140,11 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec) -> Option>> { + fn confirm_completion( + &mut self, + completion_ix: Option, + cx: &mut ViewContext, + ) -> Option>> { let completion_state = self.hide_completions(cx)?; let mat = completion_state .matches - .get(completion_state.selected_item)?; + .get(completion_ix.unwrap_or(completion_state.selected_item))?; let completion = completion_state.completions.get(mat.candidate_id)?; if completion.is_snippet() { @@ -1702,6 +1713,8 @@ impl Editor { } pub fn render_completions(&self, cx: &AppContext) -> Option { + enum CompletionTag {} + self.completion_state.as_ref().map(|state| { let build_settings = self.build_settings.clone(); let settings = build_settings(cx); @@ -1715,30 +1728,48 @@ impl Editor { let settings = build_settings(cx); let start_ix = range.start; for (ix, mat) in matches[range].iter().enumerate() { - let item_style = if start_ix + ix == selected_item { - settings.style.autocomplete.selected_item - } else { - settings.style.autocomplete.item - }; let completion = &completions[mat.candidate_id]; + let item_ix = start_ix + ix; items.push( - Text::new(completion.label.text.clone(), settings.style.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - settings.style.text.color.into(), - completion.label.runs.iter().filter_map( - |(range, highlight_id)| { - highlight_id - .style(&settings.style.syntax) - .map(|style| (range.clone(), style)) - }, - ), - &mat.positions, - )) - .contained() - .with_style(item_style) - .boxed(), + MouseEventHandler::new::( + mat.candidate_id, + cx, + |state, _| { + let item_style = if item_ix == selected_item { + settings.style.autocomplete.selected_item + } else if state.hovered { + settings.style.autocomplete.hovered_item + } else { + settings.style.autocomplete.item + }; + + Text::new( + completion.label.text.clone(), + settings.style.text.clone(), + ) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + settings.style.text.color.into(), + completion.label.runs.iter().filter_map( + |(range, highlight_id)| { + highlight_id + .style(&settings.style.syntax) + .map(|style| (range.clone(), style)) + }, + ), + &mat.positions, + )) + .contained() + .with_style(item_style) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(move |cx| { + cx.dispatch_action(ConfirmCompletion(Some(item_ix))); + }) + .boxed(), ); } }, @@ -6952,7 +6983,7 @@ mod tests { let apply_additional_edits = editor.update(&mut cx, |editor, cx| { editor.move_down(&MoveDown, cx); - let apply_additional_edits = editor.confirm_completion(cx).unwrap(); + let apply_additional_edits = editor.confirm_completion(None, cx).unwrap(); assert_eq!( editor.text(cx), " diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d097e1b003..339cabbbf3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -328,6 +328,7 @@ pub struct AutocompleteStyle { pub container: ContainerStyle, pub item: ContainerStyle, pub selected_item: ContainerStyle, + pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 2475dd9531..150561e3c6 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -322,6 +322,10 @@ match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.sy [editor.autocomplete.selected_item] extends = "$editor.autocomplete.item" +background = "$state.selected" + +[editor.autocomplete.hovered_item] +extends = "$editor.autocomplete.item" background = "$state.hover" [project_diagnostics] diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml index a197673f67..bc9f6c75d6 100644 --- a/crates/zed/assets/themes/black.toml +++ b/crates/zed/assets/themes/black.toml @@ -40,6 +40,7 @@ bad = "#b7372e" active_line = "#161313" highlighted_line = "#faca5033" hover = "#00000033" +selected = "#00000088" [editor.syntax] keyword = { color = "#0086c0", weight = "bold" } diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml index 9450d22641..acfbf083c0 100644 --- a/crates/zed/assets/themes/dark.toml +++ b/crates/zed/assets/themes/dark.toml @@ -40,6 +40,7 @@ bad = "#b7372e" active_line = "#00000022" highlighted_line = "#faca5033" hover = "#00000033" +selected = "#00000088" [editor.syntax] keyword = { color = "#0086c0", weight = "bold" } @@ -51,7 +52,6 @@ comment = "#6a9955" property = "#4e94ce" variant = "#4fc1ff" constant = "#9cdcfe" - title = { color = "#9cdcfe", weight = "bold" } emphasis = "#4ec9b0" "emphasis.strong" = { color = "#4ec9b0", weight = "bold" } diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml index 3113a69110..cf8ebe34e6 100644 --- a/crates/zed/assets/themes/light.toml +++ b/crates/zed/assets/themes/light.toml @@ -40,6 +40,7 @@ bad = "#b7372e" active_line = "#00000008" highlighted_line = "#faca5033" hover = "#0000000D" +selected = "#0000001c" [editor.syntax] keyword = { color = "#0000fa", weight = "bold" } @@ -51,7 +52,6 @@ comment = "#6a9955" property = "#4e94ce" variant = "#4fc1ff" constant = "#5a9ccc" - title = { color = "#5a9ccc", weight = "bold" } emphasis = "#267f29" "emphasis.strong" = { color = "#267f29", weight = "bold" } From f41f1e51a8b225a5d328c6dcdf915cb4ebb5b355 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 11:42:52 +0100 Subject: [PATCH 52/61] Hide completions when {un,re}doing past initial insertion boundaries --- crates/editor/src/editor.rs | 78 ++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1cf156a72f..f45580b6c1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3810,7 +3810,6 @@ impl Editor { T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { let buffer = self.buffer.read(cx).snapshot(cx); - let old_cursor_position = self.newest_anchor_selection().map(|s| s.head()); selections.sort_unstable_by_key(|s| s.start); // Merge overlapping selections. @@ -3829,28 +3828,9 @@ impl Editor { } } - self.pending_selection = None; - self.add_selections_state = None; - self.select_next_state = None; - self.select_larger_syntax_node_stack.clear(); - self.autoclose_stack.invalidate(&selections, &buffer); - self.snippet_stack.invalidate(&selections, &buffer); - - let new_cursor_position = selections.iter().max_by_key(|s| s.id).map(|s| s.head()); - if let Some(old_cursor_position) = old_cursor_position { - if let Some(new_cursor_position) = new_cursor_position { - self.push_to_nav_history( - old_cursor_position, - Some(new_cursor_position.to_point(&buffer)), - cx, - ); - } - } - if let Some(autoscroll) = autoscroll { self.request_autoscroll(autoscroll, cx); } - self.pause_cursor_blinking(cx); self.set_selections( Arc::from_iter(selections.into_iter().map(|selection| { @@ -3869,22 +3849,6 @@ impl Editor { })), cx, ); - - if let Some((completion_state, cursor_position)) = - self.completion_state.as_mut().zip(new_cursor_position) - { - let cursor_position = cursor_position.to_offset(&buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_state.initial_position.clone()); - if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) - { - let query = Self::completion_query(&buffer, cursor_position); - smol::block_on(completion_state.filter(query.as_deref(), cx.background().clone())); - self.show_completions(&ShowCompletions, cx); - } else { - self.hide_completions(cx); - } - } } /// Compute new ranges for any selections that were located in excerpts that have @@ -3937,12 +3901,54 @@ impl Editor { } fn set_selections(&mut self, selections: Arc<[Selection]>, cx: &mut ViewContext) { + let old_cursor_position = self.newest_anchor_selection().map(|s| s.head()); self.selections = selections; if self.focused { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections(&self.selections, cx) }); } + + let buffer = self.buffer.read(cx).snapshot(cx); + self.pending_selection = None; + self.add_selections_state = None; + self.select_next_state = None; + self.select_larger_syntax_node_stack.clear(); + self.autoclose_stack.invalidate(&self.selections, &buffer); + self.snippet_stack.invalidate(&self.selections, &buffer); + + let new_cursor_position = self + .selections + .iter() + .max_by_key(|s| s.id) + .map(|s| s.head()); + if let Some(old_cursor_position) = old_cursor_position { + if let Some(new_cursor_position) = new_cursor_position.as_ref() { + self.push_to_nav_history( + old_cursor_position, + Some(new_cursor_position.to_point(&buffer)), + cx, + ); + } + } + + if let Some((completion_state, cursor_position)) = + self.completion_state.as_mut().zip(new_cursor_position) + { + let cursor_position = cursor_position.to_offset(&buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_state.initial_position.clone()); + if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) + { + let query = Self::completion_query(&buffer, cursor_position); + smol::block_on(completion_state.filter(query.as_deref(), cx.background().clone())); + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_completions(cx); + } + } + + self.pause_cursor_blinking(cx); cx.emit(Event::SelectionsChanged); } From 85e6ea10913281efce5d90e81d5db2d9a4e5ff99 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 12:10:22 +0100 Subject: [PATCH 53/61] Transact on applying completion to allow restoring of selections on undo --- crates/editor/src/editor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f45580b6c1..d3b365329f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1687,6 +1687,7 @@ impl Editor { .get(completion_ix.unwrap_or(completion_state.selected_item))?; let completion = completion_state.completions.get(mat.candidate_id)?; + self.start_transaction(cx); if completion.is_snippet() { self.insert_snippet(completion.old_range.clone(), &completion.new_text, cx) .log_err(); @@ -1702,6 +1703,7 @@ impl Editor { } }); } + self.end_transaction(cx); Some(self.buffer.update(cx, |buffer, cx| { buffer.apply_additional_edits_for_completion(completion.clone(), cx) From 1375c7b7e4068fd6ec3c27545d420f2ba7c34512 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 14:36:25 +0100 Subject: [PATCH 54/61] Fix panic when trying to show completions but none match the query --- crates/editor/src/editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d3b365329f..7469665202 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1711,7 +1711,9 @@ impl Editor { } pub fn has_completions(&self) -> bool { - self.completion_state.is_some() + self.completion_state + .as_ref() + .map_or(false, |c| !c.matches.is_empty()) } pub fn render_completions(&self, cx: &AppContext) -> Option { From d56e721fab4c0a7503670b2d6fdfb2c3be85d64b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 15:39:53 +0100 Subject: [PATCH 55/61] Mute color of completion's text that can't be filtered --- crates/editor/src/editor.rs | 63 ++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7469665202..efd8452d9d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -30,8 +30,8 @@ use gpui::{ use items::BufferItemHandle; use itertools::Itertools as _; use language::{ - AnchorRangeExt as _, BracketPair, Buffer, Completion, Diagnostic, DiagnosticSeverity, Language, - Point, Selection, SelectionGoal, TransactionId, + AnchorRangeExt as _, BracketPair, Buffer, Completion, CompletionLabel, Diagnostic, + DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ @@ -1755,12 +1755,10 @@ impl Editor { .with_highlights(combine_syntax_and_fuzzy_match_highlights( &completion.label.text, settings.style.text.color.into(), - completion.label.runs.iter().filter_map( - |(range, highlight_id)| { - highlight_id - .style(&settings.style.syntax) - .map(|style| (range.clone(), style)) - }, + styled_runs_for_completion_label( + &completion.label, + settings.style.text.color, + &settings.style.syntax, ), &mat.positions, )) @@ -4822,6 +4820,55 @@ pub fn combine_syntax_and_fuzzy_match_highlights( result } +fn styled_runs_for_completion_label<'a>( + label: &'a CompletionLabel, + default_color: Color, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + const MUTED_OPACITY: usize = 165; + + let mut muted_default_style = HighlightStyle { + color: default_color, + ..Default::default() + }; + muted_default_style.color.a = ((default_color.a as usize * MUTED_OPACITY) / 255) as u8; + + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style.clone(); + muted_style.color.a = ((style.color.a as usize * MUTED_OPACITY) / 255) as u8; + + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, muted_default_style)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); + + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), muted_default_style)); + } + + runs + }) +} + #[cfg(test)] mod tests { use super::*; From 71ef052c066b4c583a080d51ebdb7b33dd5d261f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 15:48:00 +0100 Subject: [PATCH 56/61] Put rounded corners around autocomplete and give it some padding --- crates/zed/assets/themes/_base.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 150561e3c6..754ba18346 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -316,10 +316,15 @@ message.highlight_text.color = "$text.3.color" [editor.autocomplete] background = "$surface.2" -border = { width = 1, color = "$border.1" } -item.padding = 2 +border = { width = 2, color = "$border.1" } +corner_radius = 6 +padding = 6 match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" } +[editor.autocomplete.item] +padding = { left = 6, right = 6, top = 2, bottom = 2 } +corner_radius = 6 + [editor.autocomplete.selected_item] extends = "$editor.autocomplete.item" background = "$state.selected" From 60595a64bd2a5e30a8e0bdd2615b64dca6650a8f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 17:21:24 +0100 Subject: [PATCH 57/61] Align completion labels with cursor Co-Authored-By: Nathan Sobo --- crates/zed/assets/themes/_base.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 754ba18346..2f3541dc1b 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -320,6 +320,7 @@ border = { width = 2, color = "$border.1" } corner_radius = 6 padding = 6 match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" } +margin.left = -14 [editor.autocomplete.item] padding = { left = 6, right = 6, top = 2, bottom = 2 } From 3e8707ebf66e5dab911f0e9d60ac3066872723ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 18:58:36 +0100 Subject: [PATCH 58/61] Support multi-cursor autocompletion Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 120 +++++++++++++++++++++--------- crates/editor/src/multi_buffer.rs | 6 ++ crates/snippet/src/snippet.rs | 8 +- 3 files changed, 93 insertions(+), 41 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index efd8452d9d..37f00fcd6e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1687,20 +1687,56 @@ impl Editor { .get(completion_ix.unwrap_or(completion_state.selected_item))?; let completion = completion_state.completions.get(mat.candidate_id)?; - self.start_transaction(cx); + let snippet; + let text; if completion.is_snippet() { - self.insert_snippet(completion.old_range.clone(), &completion.new_text, cx) - .log_err(); + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + text = completion.new_text.clone(); + }; + let snapshot = self.buffer.read(cx).snapshot(cx); + let old_range = completion.old_range.to_offset(&snapshot); + + let selections = self.local_selections::(cx); + let mut common_prefix_len = None; + let mut ranges = Vec::new(); + for selection in &selections { + let start = selection.start.saturating_sub(old_range.len()); + let prefix_len = snapshot + .bytes_at(start) + .zip(completion.new_text.bytes()) + .take_while(|(a, b)| a == b) + .count(); + if common_prefix_len.is_none() { + common_prefix_len = Some(prefix_len); + } + + if common_prefix_len == Some(prefix_len) { + ranges.push(start + prefix_len..selection.end); + } else { + common_prefix_len.take(); + ranges.clear(); + ranges.extend(selections.iter().map(|s| s.start..s.end)); + break; + } + } + let common_prefix_len = common_prefix_len.unwrap_or(0); + let text = &text[common_prefix_len..]; + + self.start_transaction(cx); + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } + + self.insert_snippet(&ranges, snippet, cx).log_err(); } else { self.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.read(cx); - let old_range = completion.old_range.to_offset(&snapshot); - if old_range.len() != completion.new_text.len() - || !snapshot.contains_str_at(old_range.start, &completion.new_text) - { - drop(snapshot); - buffer.edit_with_autoindent([old_range], &completion.new_text, cx); - } + buffer.edit_with_autoindent(ranges, text, cx); }); } self.end_transaction(cx); @@ -1796,29 +1832,37 @@ impl Editor { }) } - pub fn insert_snippet( + pub fn insert_snippet( &mut self, - range: Range, - text: &str, + insertion_ranges: &[Range], + snippet: Snippet, cx: &mut ViewContext, - ) -> Result<()> - where - S: Clone + ToOffset, - { - let snippet = Snippet::parse(text)?; + ) -> Result<()> { let tabstops = self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent([range.clone()], snippet.text, cx); - let snapshot = buffer.read(cx); - let start = range.start.to_offset(&snapshot); + buffer.edit_with_autoindent(insertion_ranges.iter().cloned(), &snippet.text, cx); + + let snapshot = &*buffer.read(cx); + let snippet = &snippet; snippet .tabstops .iter() - .map(|ranges| { - ranges - .into_iter() - .map(|range| { - snapshot.anchor_before(start + range.start) - ..snapshot.anchor_after(start + range.end) + .map(|tabstop| { + tabstop + .iter() + .flat_map(|tabstop_range| { + let mut delta = 0 as isize; + insertion_ranges.iter().map(move |insertion_range| { + let insertion_start = insertion_range.start as isize + delta; + delta += + snippet.text.len() as isize - insertion_range.len() as isize; + + let start = snapshot.anchor_before( + (insertion_start + tabstop_range.start) as usize, + ); + let end = snapshot + .anchor_after((insertion_start + tabstop_range.end) as usize); + start..end + }) }) .collect::>() }) @@ -1840,8 +1884,8 @@ impl Editor { self.move_to_snippet_tabstop(Bias::Right, cx) } - pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { - self.move_to_snippet_tabstop(Bias::Left, cx) + pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) { + self.move_to_snippet_tabstop(Bias::Left, cx); } pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { @@ -2017,7 +2061,8 @@ impl Editor { } pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { - if self.move_to_prev_snippet_tabstop(cx) { + if !self.snippet_stack.is_empty() { + self.move_to_prev_snippet_tabstop(cx); return; } @@ -6939,19 +6984,19 @@ mod tests { let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); editor.update(&mut cx, |editor, cx| { - editor - .insert_snippet(2..2, "f(${1:one}, ${2:two})$0", cx) - .unwrap(); + let snippet = Snippet::parse("f(${1:one}, ${2:two})$0").unwrap(); + editor.insert_snippet(&[2..2], snippet, cx).unwrap(); assert_eq!(editor.text(cx), "a.f(one, two) b"); assert_eq!(editor.selected_ranges::(cx), &[4..7]); // Can't move earlier than the first tab stop - assert!(!editor.move_to_prev_snippet_tabstop(cx)); + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!(editor.selected_ranges::(cx), &[4..7]); assert!(editor.move_to_next_snippet_tabstop(cx)); assert_eq!(editor.selected_ranges::(cx), &[9..12]); - assert!(editor.move_to_prev_snippet_tabstop(cx)); + editor.move_to_prev_snippet_tabstop(cx); assert_eq!(editor.selected_ranges::(cx), &[4..7]); assert!(editor.move_to_next_snippet_tabstop(cx)); @@ -6959,7 +7004,8 @@ mod tests { assert_eq!(editor.selected_ranges::(cx), &[13..13]); // As soon as the last tab stop is reached, snippet state is gone - assert!(!editor.move_to_prev_snippet_tabstop(cx)); + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!(editor.selected_ranges::(cx), &[13..13]); }); } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 1904e00edc..417e0bc69b 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1314,6 +1314,12 @@ impl MultiBufferSnapshot { } } + pub fn bytes_at<'a, T: ToOffset>(&'a self, position: T) -> impl 'a + Iterator { + self.bytes_in_range(position.to_offset(self)..self.len()) + .flatten() + .copied() + } + pub fn buffer_rows<'a>(&'a self, start_row: u32) -> MultiBufferRows<'a> { let mut result = MultiBufferRows { buffer_row_range: 0..0, diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 88e8aa2f5f..82ec12f5ff 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -8,7 +8,7 @@ pub struct Snippet { pub tabstops: Vec, } -type TabStop = SmallVec<[Range; 2]>; +type TabStop = SmallVec<[Range; 2]>; impl Snippet { pub fn parse(source: &str) -> Result { @@ -19,7 +19,7 @@ impl Snippet { let last_tabstop = tabstops .remove(&0) - .unwrap_or_else(|| SmallVec::from_iter([text.len()..text.len()])); + .unwrap_or_else(|| SmallVec::from_iter([text.len() as isize..text.len() as isize])); Ok(Snippet { text, tabstops: tabstops.into_values().chain(Some(last_tabstop)).collect(), @@ -87,7 +87,7 @@ fn parse_tabstop<'a>( tabstops .entry(tabstop_index) .or_default() - .push(tabstop_start..text.len()); + .push(tabstop_start as isize..text.len() as isize); Ok(source) } @@ -161,7 +161,7 @@ mod tests { ); } - fn tabstops(snippet: &Snippet) -> Vec>> { + fn tabstops(snippet: &Snippet) -> Vec>> { snippet.tabstops.iter().map(|t| t.to_vec()).collect() } } From 39152bc85f8c9a146b8ed24401a4b0313d2a9fc0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Feb 2022 19:26:24 +0100 Subject: [PATCH 59/61] Add unit test for multi-cursor snippet Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 110 ++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 37f00fcd6e..01b966cb7c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1847,7 +1847,7 @@ impl Editor { .tabstops .iter() .map(|tabstop| { - tabstop + let mut tabstop_ranges = tabstop .iter() .flat_map(|tabstop_range| { let mut delta = 0 as isize; @@ -1864,7 +1864,10 @@ impl Editor { start..end }) }) - .collect::>() + .collect::>(); + tabstop_ranges + .sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot).unwrap()); + tabstop_ranges }) .collect::>() }); @@ -1890,7 +1893,6 @@ impl Editor { pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.local_selections::(cx); if let Some(snippet) = self.snippet_stack.last_mut() { match bias { @@ -1910,13 +1912,12 @@ impl Editor { } } if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { - let new_selections = old_selections - .into_iter() - .zip(current_ranges.iter()) - .map(|(selection, new_range)| { + let new_selections = current_ranges + .iter() + .map(|new_range| { let new_range = new_range.to_offset(&buffer); Selection { - id: selection.id, + id: post_inc(&mut self.next_selection_id), start: new_range.start, end: new_range.end, reversed: false, @@ -6979,33 +6980,106 @@ mod tests { async fn test_snippets(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let text = "a. b"; + let text = " + a. b + a. b + a. b + " + .unindent(); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); editor.update(&mut cx, |editor, cx| { - let snippet = Snippet::parse("f(${1:one}, ${2:two})$0").unwrap(); - editor.insert_snippet(&[2..2], snippet, cx).unwrap(); - assert_eq!(editor.text(cx), "a.f(one, two) b"); - assert_eq!(editor.selected_ranges::(cx), &[4..7]); + let buffer = &editor.snapshot(cx).buffer_snapshot; + let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); + let insertion_ranges = [ + Point::new(0, 2).to_offset(buffer)..Point::new(0, 2).to_offset(buffer), + Point::new(1, 2).to_offset(buffer)..Point::new(1, 2).to_offset(buffer), + Point::new(2, 2).to_offset(buffer)..Point::new(2, 2).to_offset(buffer), + ]; + + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); + assert_eq!( + editor.text(cx), + " + a.f(one, two, three) b + a.f(one, two, three) b + a.f(one, two, three) b + " + .unindent() + ); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); // Can't move earlier than the first tab stop editor.move_to_prev_snippet_tabstop(cx); - assert_eq!(editor.selected_ranges::(cx), &[4..7]); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); assert!(editor.move_to_next_snippet_tabstop(cx)); - assert_eq!(editor.selected_ranges::(cx), &[9..12]); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 9)..Point::new(0, 12), + Point::new(1, 9)..Point::new(1, 12), + Point::new(2, 9)..Point::new(2, 12) + ] + ); editor.move_to_prev_snippet_tabstop(cx); - assert_eq!(editor.selected_ranges::(cx), &[4..7]); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); assert!(editor.move_to_next_snippet_tabstop(cx)); assert!(editor.move_to_next_snippet_tabstop(cx)); - assert_eq!(editor.selected_ranges::(cx), &[13..13]); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 20)..Point::new(0, 20), + Point::new(1, 20)..Point::new(1, 20), + Point::new(2, 20)..Point::new(2, 20) + ] + ); // As soon as the last tab stop is reached, snippet state is gone editor.move_to_prev_snippet_tabstop(cx); - assert_eq!(editor.selected_ranges::(cx), &[13..13]); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 20)..Point::new(0, 20), + Point::new(1, 20)..Point::new(1, 20), + Point::new(2, 20)..Point::new(2, 20) + ] + ); }); } From 6e33f1421804df70b30a026257881edeb5d2de59 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 3 Feb 2022 12:17:03 -0800 Subject: [PATCH 60/61] Confirm completion on tab key as well as enter key --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 01b966cb7c..aae60d3c17 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -146,6 +146,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec Date: Thu, 3 Feb 2022 15:24:16 -0800 Subject: [PATCH 61/61] Add integration test for getting and resolving completions --- crates/editor/src/editor.rs | 2 +- crates/editor/src/multi_buffer.rs | 2 +- crates/language/src/language.rs | 10 +- crates/server/src/rpc.rs | 227 +++++++++++++++++++++++++++++- 4 files changed, 237 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aae60d3c17..8c45d2e590 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1677,7 +1677,7 @@ impl Editor { self.completion_state.take() } - fn confirm_completion( + pub fn confirm_completion( &mut self, completion_ix: Option, cx: &mut ViewContext, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 417e0bc69b..0554e1b8b5 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1339,7 +1339,7 @@ impl MultiBufferSnapshot { range: range.clone(), excerpts: self.excerpts.cursor(), excerpt_chunks: None, - language_aware: language_aware, + language_aware, }; chunks.seek(range.start); chunks diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 4ebe6552cd..e4d22346bf 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -363,7 +363,15 @@ impl LanguageServerConfig { pub async fn fake( executor: Arc, ) -> (Self, lsp::FakeLanguageServer) { - let (server, fake) = lsp::LanguageServer::fake(executor).await; + Self::fake_with_capabilities(Default::default(), executor).await + } + + pub async fn fake_with_capabilities( + capabilites: lsp::ServerCapabilities, + executor: Arc, + ) -> (Self, lsp::FakeLanguageServer) { + let (server, fake) = + lsp::LanguageServer::fake_with_capabilities(capabilites, executor).await; fake.started .store(false, std::sync::atomic::Ordering::SeqCst); let started = fake.started.clone(); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 70e55c66f9..1d9f784f47 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -343,7 +343,7 @@ impl Server { self.peer.send( conn_id, proto::AddProjectCollaborator { - project_id: project_id, + project_id, collaborator: Some(proto::Collaborator { peer_id: request.sender_id.0, replica_id: response.replica_id, @@ -2297,6 +2297,231 @@ mod tests { }); } + #[gpui::test] + async fn test_collaborating_with_completion( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + let mut lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new(cx_a.background())); + + // Set up a fake language server. + let (language_server_config, mut fake_language_server) = + LanguageServerConfig::fake_with_capabilities( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx_a.background(), + ) + .await; + Arc::get_mut(&mut lang_registry) + .unwrap() + .add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "main.rs": "fn main() { a }", + "other.rs": "", + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", false, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id()); + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(&mut cx_b, |p, cx| { + p.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let (window_b, _) = cx_b.add_window(|_| EmptyView); + let editor_b = cx_b.add_view(window_b, |cx| { + Editor::for_buffer( + cx.add_model(|cx| MultiBuffer::singleton(buffer_b.clone(), cx)), + Arc::new(|cx| EditorSettings::test(cx)), + cx, + ) + }); + + // Type a completion trigger character as the guest. + editor_b.update(&mut cx_b, |editor, cx| { + editor.select_ranges([13..13], None, cx); + editor.handle_input(&Input(".".into()), cx); + cx.focus(&editor_b); + }); + + // Receive a completion request as the host's language server. + let (request_id, params) = fake_language_server + .receive_request::() + .await; + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + // Return some completions from the host's language server. + fake_language_server + .respond( + request_id, + Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + lsp::CompletionItem { + label: "second_method(…)".into(), + detail: Some("fn(&mut self, C) -> D".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "second_method()".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + ])), + ) + .await; + + // Open the buffer on the host. + let buffer_a = project_a + .update(&mut cx_a, |p, cx| { + p.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + buffer_a + .condition(&cx_a, |buffer, _| buffer.text() == "fn main() { a. }") + .await; + + // Confirm a completion on the guest. + editor_b.next_notification(&cx_b).await; + editor_b.update(&mut cx_b, |editor, cx| { + assert!(editor.has_completions()); + editor.confirm_completion(Some(0), cx); + assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); + }); + + buffer_a + .condition(&cx_a, |buffer, _| { + buffer.text() == "fn main() { a.first_method() }" + }) + .await; + + // Receive a request resolve the selected completion on the host's language server. + let (request_id, params) = fake_language_server + .receive_request::() + .await; + assert_eq!(params.label, "first_method(…)"); + + // Return a resolved completion from the host's language server. + // The resolved completion has an additional text edit. + fake_language_server + .respond( + request_id, + lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + new_text: "use d::SomeTrait;\n".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + }]), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + ) + .await; + + // The additional edit is applied. + buffer_b + .condition(&cx_b, |buffer, _| { + buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }" + }) + .await; + assert_eq!( + buffer_a.read_with(&cx_a, |buffer, _| buffer.text()), + buffer_b.read_with(&cx_b, |buffer, _| buffer.text()), + ); + } + #[gpui::test] async fn test_formatting_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking();