From 1c46749ad707ad562f0ec099a7ccd06d8a585636 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Aug 2023 12:58:09 -0700 Subject: [PATCH] Fix regression in Buffer::language_scope_at Co-authored-by: Julia --- crates/editor/src/editor.rs | 6 +-- crates/language/src/buffer.rs | 47 ++++++++++++++------ crates/language/src/buffer_tests.rs | 69 ++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 45a37db983..a38145f48c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2103,12 +2103,12 @@ impl Editor { for (selection, autoclose_region) in self.selections_with_autoclose_regions(selections, &snapshot) { - if let Some(language) = snapshot.language_scope_at(selection.head()) { + if let Some(scope) = snapshot.language_scope_at(selection.head()) { // Determine if the inserted text matches the opening or closing // bracket of any of this language's bracket pairs. let mut bracket_pair = None; let mut is_bracket_pair_start = false; - for (pair, enabled) in language.brackets() { + for (pair, enabled) in scope.brackets() { if enabled && pair.close && pair.start.ends_with(text.as_ref()) { bracket_pair = Some(pair.clone()); is_bracket_pair_start = true; @@ -2130,7 +2130,7 @@ impl Editor { let following_text_allows_autoclose = snapshot .chars_at(selection.start) .next() - .map_or(true, |c| language.should_autoclose_before(c)); + .map_or(true, |c| scope.should_autoclose_before(c)); let preceding_text_matches_prefix = prefix_len == 0 || (selection.start.column >= (prefix_len as u32) && snapshot.contains_str_at( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0b10432a9f..e6ad3469ea 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2145,27 +2145,46 @@ impl BufferSnapshot { pub fn language_scope_at(&self, position: D) -> Option { let offset = position.to_offset(self); - let mut range = 0..self.len(); - let mut scope = self.language.clone().map(|language| LanguageScope { - language, - override_id: None, - }); + let mut scope = None; + let mut smallest_range: Option> = None; // Use the layer that has the smallest node intersecting the given point. for layer in self.syntax.layers_for_range(offset..offset, &self.text) { let mut cursor = layer.node().walk(); - while cursor.goto_first_child_for_byte(offset).is_some() {} - let node_range = cursor.node().byte_range(); - if node_range.to_inclusive().contains(&offset) && node_range.len() < range.len() { - range = node_range; - scope = Some(LanguageScope { - language: layer.language.clone(), - override_id: layer.override_id(offset, &self.text), - }); + + let mut range = None; + loop { + let child_range = cursor.node().byte_range(); + if !child_range.to_inclusive().contains(&offset) { + break; + } + + range = Some(child_range); + if cursor.goto_first_child_for_byte(offset).is_none() { + break; + } + } + + if let Some(range) = range { + if smallest_range + .as_ref() + .map_or(true, |smallest_range| range.len() < smallest_range.len()) + { + smallest_range = Some(range); + scope = Some(LanguageScope { + language: layer.language.clone(), + override_id: layer.override_id(offset, &self.text), + }); + } } } - scope + scope.or_else(|| { + self.language.clone().map(|language| LanguageScope { + language, + override_id: None, + }) + }) } pub fn surrounding_word(&self, start: T) -> (Range, Option) { diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 399ca85e56..9d4b9c38fe 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1631,7 +1631,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { } #[gpui::test] -fn test_language_scope_at(cx: &mut AppContext) { +fn test_language_scope_at_with_javascript(cx: &mut AppContext) { init_settings(cx, |_| {}); cx.add_model(|cx| { @@ -1718,6 +1718,73 @@ fn test_language_scope_at(cx: &mut AppContext) { }); } +#[gpui::test] +fn test_language_scope_at_with_rust(cx: &mut AppContext) { + init_settings(cx, |_| {}); + + cx.add_model(|cx| { + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + newline: false, + }, + BracketPair { + start: "'".into(), + end: "'".into(), + close: true, + newline: false, + }, + ], + disabled_scopes_by_bracket_ix: vec![ + Vec::new(), // + vec!["string".into()], + ], + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_override_query( + r#" + (string_literal) @string + "#, + ) + .unwrap(); + + let text = r#" + const S: &'static str = "hello"; + "# + .unindent(); + + let buffer = Buffer::new(0, text.clone(), cx).with_language(Arc::new(language), cx); + let snapshot = buffer.snapshot(); + + // By default, all brackets are enabled + let config = snapshot.language_scope_at(0).unwrap(); + assert_eq!( + config.brackets().map(|e| e.1).collect::>(), + &[true, true] + ); + + // Within a string, the quotation brackets are disabled. + let string_config = snapshot + .language_scope_at(text.find("ello").unwrap()) + .unwrap(); + assert_eq!( + string_config.brackets().map(|e| e.1).collect::>(), + &[true, false] + ); + + buffer + }); +} + #[gpui::test] fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { init_settings(cx, |_| {});