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 d5a87f1aff180e6f7ef51fab371566f97791451a..b2c658dbd6ff38fdce25a8724786a9875aae1d27 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 92084b9dc78b026f1e12f5043a6195efc41f0578..dbbef435f627778cc7495f6b5bd58c6c438bd04e 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 05128776b640527a9162175ec1f8ffa2ee3cbe0c..014f48f462c7d2a762ef987e4b01e61f2662f753 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 91d774f2a436cb3a2d630484d5a8765ee9daa650..22accb422c2970b723a0cf19f2bb116100ce9bf4 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 07f0196c9f59725855f624b486a079947bf7e1d0..0460d122b7736e92370b4db8f53b3da7dd4d1e76 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 db0c099c4b8e228eea8c2349e945770c836bda82..ed900bc9e2918848a3c98eb2b61e9b64519617f7 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 a8867f68d50f3428cd0e6912b7fa8b3b7b9b8853..aa118c55cb4e8b1a3d02a262976a3006e3d46b16 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 014f48f462c7d2a762ef987e4b01e61f2662f753..257d066b0875334a80669e237aaf5386d0336b48 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 | 61 ++++++++++++----------------------- 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 257d066b0875334a80669e237aaf5386d0336b48..9dc7c112bed05c29fcee8dd7a49d7592e1d87e16 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) - }); - - 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()) + 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 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 ed900bc9e2918848a3c98eb2b61e9b64519617f7..5a14ab4642ddd92709b9cbcbba84df9d9cf30924 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 5a14ab4642ddd92709b9cbcbba84df9d9cf30924..3bfddcc48d865c77436cfa94a2755a5b3b40d55d 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 8ef66568970ac5037711d258a8b502352054f62c..5bba59191be63a68c203ca94084e92c1430a80aa 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 9dc7c112bed05c29fcee8dd7a49d7592e1d87e16..290cb4f1b6e1a1d2f9a536798eda1c4ae100c28a 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 3bfddcc48d865c77436cfa94a2755a5b3b40d55d..0f95ef07ac05ac94ce25a2fdb18be89f3b4cdef7 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 5bba59191be63a68c203ca94084e92c1430a80aa..2e01a2758e0d188205cd329d41dc3286fafeb639 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 | 99 ++++++++++++++++-------- crates/theme/src/theme.rs | 9 +++ crates/zed/assets/themes/_base.toml | 7 +- 5 files changed, 96 insertions(+), 37 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0f95ef07ac05ac94ce25a2fdb18be89f3b4cdef7..c36eccb6823b8872b88b60e71d3e85665d4c0be9 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 2e01a2758e0d188205cd329d41dc3286fafeb639..512c6325f9d7d250eac41bc4619498903c49fa1d 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 945340e4c0539bddb4e8a47b2cf9b072b008677b..9248a8d146e07a4b85f627644fa696dd7a616a9a 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); + + if self.item_count == 0 { + return ( + constraint.min, + LayoutState { + item_height: 0., + scroll_max: 0., + items, + }, + ); + } + + 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 { + (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()); - 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 item_constraint = SizeConstraint { + min: item_size, + max: vec2f(constraint.max.x(), item_size.y()), + }; + let item_height = item_size.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); + 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())); + } - items.clear(); - 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, - ); - (self.append_items)(start..end, &mut items, cx); - for item in &mut items { - item.layout(item_constraint, cx); + 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()); } - } else { - size = constraint.min; } ( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index fec42853dcb146360c3211352260d66dd7035c0c..1c669c57f46d430bef78f16fec284c6188400814 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 93019db6e41218ad611e5b0bdb3f4817816537d9..4281812d0e218ddebd08ebae56822f385164cfdf 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 512c6325f9d7d250eac41bc4619498903c49fa1d..5acd46b0f8db6bab25f5a0c0c6a4018cfe32bb35 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 5acd46b0f8db6bab25f5a0c0c6a4018cfe32bb35..5fc294bac414c84f71924b94af12fc82f2289a6c 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 c36eccb6823b8872b88b60e71d3e85665d4c0be9..813f087f54dfcf7da980fc4d4ac1bf0111183d28 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 1c669c57f46d430bef78f16fec284c6188400814..aca5e7d245bf1745cccfe38084f8e145eea4743f 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 4281812d0e218ddebd08ebae56822f385164cfdf..6a0c98c67f37d2ca270c718f9ba8067760f881aa 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 813f087f54dfcf7da980fc4d4ac1bf0111183d28..89e99354d68c5b6d12b991b697c527a512382afe 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 aa118c55cb4e8b1a3d02a262976a3006e3d46b16..a5c2a64beab47d13d59e000240305130992a0e7d 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 290cb4f1b6e1a1d2f9a536798eda1c4ae100c28a..a5fc6a8b844760459e0930f02d5c7e85cba30ce3 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 4b5780af68b494aa12a86e53fe5bc8768a8d185f..3301fb24a90f4a87908421caade36dd2ccf39eb7 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 f0c2c1a2c7939b94b3771e21b490c044fe4d3269..d7c811bc53681305b58a096c1c9f7d7bfb06a67f 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 b953d160ad261293455be5085c8ab76936cd53bb..6450db7d39cf16d1ee919d818acd066d3c73a568 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 89e99354d68c5b6d12b991b697c527a512382afe..4e383d5518f5ad6525e6b0c3caf412c03c171e79 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 a5c2a64beab47d13d59e000240305130992a0e7d..11aec10d5c3865ec1ebe25306a1b794370e510cb 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 a5fc6a8b844760459e0930f02d5c7e85cba30ce3..a8a642920cad5d1930b79c87e7a6447eae107fb8 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 b2c658dbd6ff38fdce25a8724786a9875aae1d27..803148427f4140e4ea8a769f708d55e8129381d6 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 a8a642920cad5d1930b79c87e7a6447eae107fb8..446bd60b87be912e8b55dd81b8b6540863ae63a8 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 11aec10d5c3865ec1ebe25306a1b794370e510cb..ccae93351707d7fc436216ff4341c153080410d2 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 4e383d5518f5ad6525e6b0c3caf412c03c171e79..1f892369eee35ef86bcc0e93521083077867f2aa 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 1f892369eee35ef86bcc0e93521083077867f2aa..c01383bb8aba952bf5277c2c73e02ef47ec97bed 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 c01383bb8aba952bf5277c2c73e02ef47ec97bed..cfc00fa7e62e3acfa33282192f1a7b3c45897c02 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 9a800f9abba9bc63dfa53db2f3a2aae6192ea486..36b384fadbdffcd69e33721f9c1f4099939d7273 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 d7c811bc53681305b58a096c1c9f7d7bfb06a67f..5de5b7ce1cb55f40334b2b9b85210087a5edb2bb 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 6450db7d39cf16d1ee919d818acd066d3c73a568..2ce10b4d81652988f5f43f84d62256fd8ab4ef65 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 cfc00fa7e62e3acfa33282192f1a7b3c45897c02..9cdb6700675186c27bb509f8b937565d4cc79172 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 36b384fadbdffcd69e33721f9c1f4099939d7273..7eee1e627cb4567e8d0e22b9bca7d294148695f1 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 ccae93351707d7fc436216ff4341c153080410d2..eafb87d9c154114c57e7247738d532360afe25d5 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 571d9f4c4b1e985a9b2a089ecb332152e02d508d..859e9f4923f1804daf5de5202b59d278de90e61b 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 dbbef435f627778cc7495f6b5bd58c6c438bd04e..7458f27c91521e24f7ff68478deba1c592659b66 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 2768b9f986cf1f7b11802972445630aa0b63e5e4..7dc11730488964fdb13405f4e47dccc2d4c90266 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 446bd60b87be912e8b55dd81b8b6540863ae63a8..5b66d4c8e88976f02f6a174aaba48e454e771e55 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 aca5e7d245bf1745cccfe38084f8e145eea4743f..d097e1b003c150ca99a52df5e22338bec1dccfe2 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 6a0c98c67f37d2ca270c718f9ba8067760f881aa..2475dd95312e90a09b4fc2180a5f55ef1736b89d 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 9cdb6700675186c27bb509f8b937565d4cc79172..97bdfd1df1bb65431767d6f2c7181996758c518c 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(), + }; - 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(), - }); - } + completion_state + .filter(query.as_deref(), cx.background()) + .await; - cx.notify(); - }); + 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>(()) } - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); + .log_err() + })); + } + + 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 e2b6d2f2adf36b477b11236019488e258e7b4fcd..03d16db1efc86ff8e80aff87e871feebdc258682 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 5b66d4c8e88976f02f6a174aaba48e454e771e55..f431da14764ff13db6c5490f3c1380851c3c2dfd 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 97bdfd1df1bb65431767d6f2c7181996758c518c..cd6ef78417356c4eb22d13da581e086b5abb5cbe 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 eafb87d9c154114c57e7247738d532360afe25d5..c70ea5d3951ca0a9160892f981d7767f94b794b5 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 f431da14764ff13db6c5490f3c1380851c3c2dfd..79a076c6fba9db9d86a500a80042d4c464cf401d 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 803148427f4140e4ea8a769f708d55e8129381d6..cd235e8151663b36399e6c340593bd92756b7caa 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 5de5b7ce1cb55f40334b2b9b85210087a5edb2bb..53d7651e3200b7119673f757b6edd1750f79c2e8 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 0000000000000000000000000000000000000000..daba92f848ea7b9917bcc185a5d6fe575af706b6 --- /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 0000000000000000000000000000000000000000..83defad87941de5255262ba744e5e7dbf38e7eb7 --- /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 0000000000000000000000000000000000000000..a99fa70f98fff465d4dd04796987e3701bf9f6d8 --- /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 0000000000000000000000000000000000000000..c5ea00c7afd1edc048bef4bc31f50b5bcb1a7041 --- /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 0000000000000000000000000000000000000000..c6061f0995320f044faeac56bcac458a09747f1d --- /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 0000000000000000000000000000000000000000..680963541375ed1fac524192718fbb90da35448c --- /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 0000000000000000000000000000000000000000..25dedf4aa0aee2666dfd638da185b2bb9cfcbcb4 --- /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 0000000000000000000000000000000000000000..817cb1cae164dea4d8c3e4e11c5afe4e39bf2093 --- /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 0000000000000000000000000000000000000000..0d6aa3acf3bf148279fad4ca26b10ec07f421b85 --- /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 0000000000000000000000000000000000000000..ea5dde3575f66b9d968d785ade674658d4d695f3 --- /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 0000000000000000000000000000000000000000..00c34b67dc6701c88825901546147fe6cbf7e842 --- /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 0000000000000000000000000000000000000000..2b14ac1046bb3b45543a62e41d8ee77d2cfae34c --- /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 0000000000000000000000000000000000000000..51d58aadeb85f1f073cace9137a6ee03914a6de8 --- /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 | 140 +++-- 15 files changed, 84 insertions(+), 1283 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 53d7651e3200b7119673f757b6edd1750f79c2e8..8f4ebb8b89b4beeda0afdad255739012e83496da 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 daba92f848ea7b9917bcc185a5d6fe575af706b6..b3712cd6a083306cb6e9a42c24172662b61238c3 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 83defad87941de5255262ba744e5e7dbf38e7eb7..0000000000000000000000000000000000000000 --- 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 a99fa70f98fff465d4dd04796987e3701bf9f6d8..0000000000000000000000000000000000000000 --- 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 c5ea00c7afd1edc048bef4bc31f50b5bcb1a7041..0000000000000000000000000000000000000000 --- 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 c6061f0995320f044faeac56bcac458a09747f1d..0000000000000000000000000000000000000000 --- 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 680963541375ed1fac524192718fbb90da35448c..0000000000000000000000000000000000000000 --- 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 25dedf4aa0aee2666dfd638da185b2bb9cfcbcb4..0000000000000000000000000000000000000000 --- 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 817cb1cae164dea4d8c3e4e11c5afe4e39bf2093..0000000000000000000000000000000000000000 --- 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 0d6aa3acf3bf148279fad4ca26b10ec07f421b85..0000000000000000000000000000000000000000 --- 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 ea5dde3575f66b9d968d785ade674658d4d695f3..0000000000000000000000000000000000000000 --- 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 00c34b67dc6701c88825901546147fe6cbf7e842..0000000000000000000000000000000000000000 --- 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 2b14ac1046bb3b45543a62e41d8ee77d2cfae34c..0000000000000000000000000000000000000000 --- 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 51d58aadeb85f1f073cace9137a6ee03914a6de8..f838721ad516a3e8e0e8d954f26afa0311216928 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(); + Some(_) => { + let chunk_end = source.find(&['}', '$']).unwrap_or(source.len()); + let (chunk, rest) = source.split_at(chunk_end); + text.push_str(chunk); + source = rest; + } + } + } +} - parse_snippet_node(cursor, text, tabstops, source)?; - tabstops - .entry(index) - .or_insert(SmallVec::new()) - .push(range_start..text.len()); +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; - cursor.goto_parent(); - } - _ => {} + if source.chars().next() == Some(':') { + source = parse_snippet(&source[1..], true, text, tabstops)?; } - if !cursor.goto_next_sibling() { - break; + 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; } - cursor.goto_parent(); - Ok(()) + + 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]] ); + + // 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]] + ); } #[test] fn test_parse_snippet_with_placeholders() { - let snippet = Snippet::parse("one${1:two}three").unwrap(); - assert_eq!(snippet.text, "onetwothree"); + 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![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 cd6ef78417356c4eb22d13da581e086b5abb5cbe..120ed69af19c45edff368b4641260dad2c901552 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 c70ea5d3951ca0a9160892f981d7767f94b794b5..e9e839726da9a1ac415b542c38a27aa1b3b8fd5f 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 79a076c6fba9db9d86a500a80042d4c464cf401d..eb9f82ea29ad5edf713390242f31350aba02c879 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 f838721ad516a3e8e0e8d954f26afa0311216928..47330c74af8d35329368f9d508b91ad0b01496b6 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 | 68 +++++++++++++++++------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 47330c74af8d35329368f9d508b91ad0b01496b6..88e8aa2f5f9f6eaf469ccd2b83e273531dfb6dfc 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() { + 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_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]] - ); + assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]); // 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]] - ); + 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_parse_snippet_with_placeholders() { + 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 8f4ebb8b89b4beeda0afdad255739012e83496da..986d46d545e7cc03e8a7027c6ff5b397dac1a34b 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 2ce10b4d81652988f5f43f84d62256fd8ab4ef65..77dcbab3df86a0d461144de72ee30881e441815f 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 120ed69af19c45edff368b4641260dad2c901552..ada7d1f9af1078d0552cb4a69e5f73dbcd37d30a 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 e9e839726da9a1ac415b542c38a27aa1b3b8fd5f..024dd4e67ab51ae9cdbc451bc051d7480f33e277 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 eb9f82ea29ad5edf713390242f31350aba02c879..187f1b00a9f09a3697bd03662b1bb80615b43cc7 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 ada7d1f9af1078d0552cb4a69e5f73dbcd37d30a..1d40fe42815a1dfce019ff7b892fefc8e305c4ff 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 1d40fe42815a1dfce019ff7b892fefc8e305c4ff..2bee666ef266184c67b7caa7d45e6e085f12bb8a 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 | 235 +++++++++++++++++++----------------- 1 file changed, 126 insertions(+), 109 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2bee666ef266184c67b7caa7d45e6e085f12bb8a..56e541603ad4e16f83bb23553c200fc7475a0422 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; } + } + } + 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(); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - return true; + // Remove the snippet state when moving to the last tabstop. + if snippet.active_index + 1 == snippet.ranges.len() { + self.snippet_stack.pop(); } - self.action_region_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 56e541603ad4e16f83bb23553c200fc7475a0422..597128dd4af6f784cf6e4a5dbebf950b816d41e0 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 5adf03daef73e3a9449255bb425d197427ebfba5..4c301b4e48bea2bc512e7e7e6f1df4aefcabfd91 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 | 51 +++++++++++++++++++--------- crates/rpc/src/proto.rs | 4 +++ crates/server/src/rpc.rs | 25 ++++++++++++++ 7 files changed, 194 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 597128dd4af6f784cf6e4a5dbebf950b816d41e0..a0e61f053b628e9c9e1bd7c03d44e19af1f5aafa 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 187f1b00a9f09a3697bd03662b1bb80615b43cc7..d2d61de2801365880026690082039e6d8f79dc3a 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 d5f8f1a1884fc41fe86ce08045bcef96d0fc6781..7109f02cecc13f59604f90e8ac89ccf75835e34b 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 11006f32baf6ec1ef06ad0688518b75c604693fe..77d9d4293f9ec4f5d8da22b3489b5639746ab9a0 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 5580d2a724a3aa1d74bbe10cb1905f4ae6861075..f9c92140a1127d6b008f810f77f2cce464d12603 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; - - 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; - - UpdateContacts update_contacts = 45; - - GetUsers get_users = 46; - GetUsersResponse get_users_response = 47; + GetCompletions get_completions = 35; + GetCompletionsResponse get_completions_response = 36; + + 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 = 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 509cefd46c912d6310b727c5e3654ed7d1c712fc..166d9e44b966f9bd8ab89df4ef76d7a882d1c443 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 8d5adfb3c5ab9b23a402c2dbdcc760b68b66fb29..5e14cafd2eda38f5ab291046c278b398cadce05e 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 024dd4e67ab51ae9cdbc451bc051d7480f33e277..6f74a419feb9da59548ccb983df74c979120063f 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 d2d61de2801365880026690082039e6d8f79dc3a..29b29e26ed8b7a16132cdc9b936000f6b374c276 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 0f9ee69956910083f9145955ddba37cd47174749..ec75018148f24a445acd56a17551248b0a045e47 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 cd235e8151663b36399e6c340593bd92756b7caa..5cc1fee8aae162e2973711bda7c596501a4f17e0 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 f9c92140a1127d6b008f810f77f2cce464d12603..beb41a2ec586f7c4c168bb842dfaa932a29f0bdd 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 | 49 ++++++++---- crates/rpc/src/proto.rs | 7 ++ crates/server/src/rpc.rs | 25 ++++++ crates/text/src/text.rs | 42 ++++++++++- 12 files changed, 345 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 986d46d545e7cc03e8a7027c6ff5b397dac1a34b..719e231f96ebd0895c7a8bd605608b993e2fa355 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 a0e61f053b628e9c9e1bd7c03d44e19af1f5aafa..0acfef3086f2c60b2f91180ba355bd5499fffc26 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 6f74a419feb9da59548ccb983df74c979120063f..bce223fb88872d675d85da320edba85094373e93 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 6c29708b5ee26228be1523801d67fd25e301cae1..f0a3096a9308d76f66571fb1321630a21242754d 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 29b29e26ed8b7a16132cdc9b936000f6b374c276..f4554e0d4592ba28aca189e63d693a1db6cbfdd8 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 ec75018148f24a445acd56a17551248b0a045e47..c95735493dcf5e26e85e4a877b83715d09e1141b 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 7109f02cecc13f59604f90e8ac89ccf75835e34b..d01fb08ee7301f7ae808211f4e8a4c899a130373 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 77d9d4293f9ec4f5d8da22b3489b5639746ab9a0..1f654751cb489474af7b93219dc94d8c5ef83a89 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 beb41a2ec586f7c4c168bb842dfaa932a29f0bdd..2f2364fc245d5d8534c0dbe842624869e8beb46d 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; - - 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 = 47; - - GetUsers get_users = 48; - GetUsersResponse get_users_response = 49; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 37; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 38; + + 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 = 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 166d9e44b966f9bd8ab89df4ef76d7a882d1c443..a94079526516d468537de754ea2fbdcec13d3878 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 5e14cafd2eda38f5ab291046c278b398cadce05e..70e55c66f9bac024846af1166e526e1542816cbb 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 b619aba2cebaa575e8478221539193311a00ae41..ad9857e2640dc2375bc857cadac4a4de77ae282a 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 719e231f96ebd0895c7a8bd605608b993e2fa355..ce979fb313bba31ba04b3741700e668bc8b94548 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 f4554e0d4592ba28aca189e63d693a1db6cbfdd8..00dee0a57c44028cffcef6ce5480524e11e0f184 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 edc1ca784648559bf56c9cf98ece252e2f762529..04648d203f891fd3b225e2ab8b179ee24592c4a8 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 ad9857e2640dc2375bc857cadac4a4de77ae282a..088cd51cbe0b54e70f32ccc9f3bad94c6c3fefca 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 0acfef3086f2c60b2f91180ba355bd5499fffc26..49612a59f91680108f07035183959450cf0e19f1 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 00dee0a57c44028cffcef6ce5480524e11e0f184..f5ea67f5fed29ad5f6a89700cea4a17b5f511155 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 bce223fb88872d675d85da320edba85094373e93..ebac70b73b1ddd5cb20f8bda0e1bb400fe44a640 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 f5ea67f5fed29ad5f6a89700cea4a17b5f511155..23b2a752c611a4bf28550e46ae1e9f018dfd8fcc 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 22accb422c2970b723a0cf19f2bb116100ce9bf4..b694e82df135c1d9812d566bdeb7efb59795f605 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 c95735493dcf5e26e85e4a877b83715d09e1141b..c934e0f3e4be7681d12b26a098fa9d78e08033e1 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 4901f6ceaa1395d326e5f11a86a001b12b19fad7..b268647c0bfbb6ac8ddbf4da23196c3625fae20a 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 b268647c0bfbb6ac8ddbf4da23196c3625fae20a..9a27e117b6fdf50713a37f114366ca480434fe53 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 49612a59f91680108f07035183959450cf0e19f1..9db6fa0a0cd262284f876132496b483f4692299c 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 8757e7593b58137d57c9cb341149e1e069576726..b1f11f42757269293a8d1a2c27fd7572229749e1 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 f00dd8965186034b8cda189670c87b0fed0b9032..600bfbfad1159b00739dc528ddf474d927fcabb1 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 fd5f1de8f810ca0d1eb0d1c86f90627a0985cc83..ab21977dfa64aa3f090a4d574175316dfc31521b 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 9b33a10b149e9eb1e3431c4bd8f0ae8c36cd066a..9d58e87d8d348355ef59ee22a7484d78d9cc13d6 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 8b02dbbd15c72297ee28ba02632d097dde8ec8e8..df1d17c5006c63c006c208fe35bd2a766d78a955 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 9db6fa0a0cd262284f876132496b483f4692299c..d97d1f864bbea1180fa5f4f30d8ca07d503fe9d6 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 5fc294bac414c84f71924b94af12fc82f2289a6c..53175c917c2e32234130270c26733e27c495c881 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 ebac70b73b1ddd5cb20f8bda0e1bb400fe44a640..8e649b919fa1a47676c7c6e674ff139e27b5ff57 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 859e9f4923f1804daf5de5202b59d278de90e61b..354016e576e98e2a2cf899fa55d133c00ecfc151 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 23b2a752c611a4bf28550e46ae1e9f018dfd8fcc..635f2973c663d77f7b8bcf0d03477513313321c0 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 065ca28cec4d5aa3474e975e6290e3baac377bcf..bab62c23502ce39e01d506e9108b85160ca4b627 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 635f2973c663d77f7b8bcf0d03477513313321c0..bcc3d1c6c12c7d2a7b3dafdbd702168203e909cb 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 b694e82df135c1d9812d566bdeb7efb59795f605..1dc35e0efa6d0c8ff90e094dd3837c4ef58b4b46 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 d97d1f864bbea1180fa5f4f30d8ca07d503fe9d6..acb489efffbd16db24e2c8ceb00375f7d50a1174 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 bcc3d1c6c12c7d2a7b3dafdbd702168203e909cb..9342e956fb660a222444eb3b71ec5132688f38ba 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 acb489efffbd16db24e2c8ceb00375f7d50a1174..8d99bd0e475c4e57c0fa0cdb413658c006bb10a5 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 9342e956fb660a222444eb3b71ec5132688f38ba..8c026b08bfe8ffc57eb5b8f71554d155fcfb032d 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 5e1c7e430465ccc596c8cc09a19948ccb116408f..75e3c8526c5969ecabfe49b24929025a13848f3e 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 1dc35e0efa6d0c8ff90e094dd3837c4ef58b4b46..e54e0f49a40422b0195fd16bfc3dd868f1976b58 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 c934e0f3e4be7681d12b26a098fa9d78e08033e1..82787ec5712291041665d8de4280d12598d61866 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 b6921ce69f63c18d9cb92df28bbd61af97701de8..4518ae6afc65ca7587d7918c9be81146082a3581 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 9a27e117b6fdf50713a37f114366ca480434fe53..b0634b4eee4a6a35997a29a9545a5b69b65c40f8 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 8d99bd0e475c4e57c0fa0cdb413658c006bb10a5..8cb2891fa2cd0118691cd76218198f2b0bc8367a 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 e54e0f49a40422b0195fd16bfc3dd868f1976b58..4ebe6552cdfaf40c1815b8876c633e5af0056428 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 b0634b4eee4a6a35997a29a9545a5b69b65c40f8..a1ad424c117fd7dd498aa79b4d48fe36bb345534 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 53175c917c2e32234130270c26733e27c495c881..31926eb972d3c15028e11d95df2a52376ef06665 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 8cb2891fa2cd0118691cd76218198f2b0bc8367a..30ef12b886067455df722320a13884216d9cb9f5 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 b1f11f42757269293a8d1a2c27fd7572229749e1..697dc5ea6256f29a0bab8eb88b5d01bfdb7eff81 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 600bfbfad1159b00739dc528ddf474d927fcabb1..96848016a9f7ff44268e21960ec49d9837fce730 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 ab21977dfa64aa3f090a4d574175316dfc31521b..a23f6ad01022c4775f4bc60686873606885c9abc 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 9d58e87d8d348355ef59ee22a7484d78d9cc13d6..e2239e76715278ba84e0ade5d3b56c7bc9e0f082 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 df1d17c5006c63c006c208fe35bd2a766d78a955..1d5e64c8a565798948a46c08b4b32dbaa08b96e7 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 31926eb972d3c15028e11d95df2a52376ef06665..a22e6021e06bce11d7993518a59b3d5da08f6aae 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 8e649b919fa1a47676c7c6e674ff139e27b5ff57..2bdf8199569d52630414324f9ef507bc78e53966 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 354016e576e98e2a2cf899fa55d133c00ecfc151..3de2234da1837b1f870ead6e5722e51afd102e3d 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 8c026b08bfe8ffc57eb5b8f71554d155fcfb032d..2efcbbf0d6ac2bcbdd05b42339da4ad1cb74f8a1 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 bab62c23502ce39e01d506e9108b85160ca4b627..94001c591ad77bd9582f6da81fba9b03bb26f4ae 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 2bdf8199569d52630414324f9ef507bc78e53966..1904e00edc7fe09d2f8480b9caa1df5ccb65efb8 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 30ef12b886067455df722320a13884216d9cb9f5..1cf156a72f3b10ebcbc40549c8fe96635ea9ed82 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 d097e1b003c150ca99a52df5e22338bec1dccfe2..339cabbbf3241516685f34899ca7f2cd857d6220 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 2475dd95312e90a09b4fc2180a5f55ef1736b89d..150561e3c65356740195b46aeffd0c11a2ce41aa 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 a197673f6794e1abce516ad0b97963a7171df393..bc9f6c75d6430b65571ecea79c2d3c8bba9f1be8 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 9450d22641aba4c765b6fdf65c437751ddc17a05..acfbf083c0a03d296502448d272aab6eb50b93aa 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 3113a6911012544ace4a3aa453238aa1388e6f6b..cf8ebe34e608605ee57254b1b92310c173ddadc1 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 1cf156a72f3b10ebcbc40549c8fe96635ea9ed82..f45580b6c1585eaedb210638ca768f5d0f6746ec 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 f45580b6c1585eaedb210638ca768f5d0f6746ec..d3b365329fe3d56c0288005a317a7a2c25ac3870 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 d3b365329fe3d56c0288005a317a7a2c25ac3870..7469665202441fc4e540a6d73b6934f56cbef284 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 7469665202441fc4e540a6d73b6934f56cbef284..efd8452d9db52e1dd81153d08a454862940dbc4b 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 150561e3c65356740195b46aeffd0c11a2ce41aa..754ba183465b4fff1aa562186c5b3594d983d88a 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 754ba183465b4fff1aa562186c5b3594d983d88a..2f3541dc1b3b0943034dab8dcca00100a07ad989 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 efd8452d9db52e1dd81153d08a454862940dbc4b..37f00fcd6ec9e15f904890a038feefc6767fab13 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 1904e00edc7fe09d2f8480b9caa1df5ccb65efb8..417e0bc69b1617a9ac3c8cf2fa79d825366bcf60 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 88e8aa2f5f9f6eaf469ccd2b83e273531dfb6dfc..82ec12f5ff881546e46f269f786c76217cbc38f8 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 37f00fcd6ec9e15f904890a038feefc6767fab13..01b966cb7c0ecb88818083dee75eb460a1bb6cd7 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 01b966cb7c0ecb88818083dee75eb460a1bb6cd7..aae60d3c177247341b7fa4e00cdfb876b2364ef2 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 aae60d3c177247341b7fa4e00cdfb876b2364ef2..8c45d2e590bdbe7abc3e6851796bbcbff128a29d 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 417e0bc69b1617a9ac3c8cf2fa79d825366bcf60..0554e1b8b5fbc7b39e0e9acc91ee2bf5288f9e06 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 4ebe6552cdfaf40c1815b8876c633e5af0056428..e4d22346bf623687ef40ae572d876ce4ba2ccd35 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 70e55c66f9bac024846af1166e526e1542816cbb..1d9f784f47672ed50ba9f65738074ed2caf91b2c 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();