From be77682a3fd3ace184a9059cc7b212e3ea4891d3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 20 Sep 2025 04:40:22 +0530 Subject: [PATCH] editor: Fix adding extraneous closing tags within TSX (#38534) --- .../src/session/running/console.rs | 6 +- crates/editor/src/editor.rs | 60 ++++----- crates/editor/src/editor_tests.rs | 116 +++++++++++++++--- crates/editor/src/hover_links.rs | 2 +- crates/editor/src/items.rs | 7 +- crates/language/src/buffer.rs | 39 ++++-- crates/language/src/language.rs | 15 +++ crates/language/src/text_diff.rs | 5 +- crates/languages/src/javascript/config.toml | 3 + crates/languages/src/tsx/config.toml | 3 + crates/multi_buffer/src/multi_buffer.rs | 28 ++--- crates/project/src/lsp_command.rs | 11 +- crates/vim/src/vim.rs | 7 +- 13 files changed, 218 insertions(+), 84 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 92c5ace8f0128e47db08c6b772376679213ffbe1..cf7b59f2fe96bb031fc1ed1a5d7ae4005dd37eb9 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -12,7 +12,7 @@ use gpui::{ Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task, TextStyle, WeakEntity, actions, }; -use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset}; +use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset}; use menu::{Confirm, SelectNext, SelectPrevious}; use project::{ Completion, CompletionDisplayOptions, CompletionResponse, @@ -575,7 +575,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { return false; } - let classifier = snapshot.char_classifier_at(position).for_completion(true); + let classifier = snapshot + .char_classifier_at(position) + .scope_context(Some(CharScopeContext::Completion)); if trigger_in_words && classifier.is_word(char) { return true; } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4084f61bb4a44d591aa544a622fa8888f56a5c57..8b0fc5512731eff70b1e9ac41b6bfe16a65babfa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -121,10 +121,10 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; use itertools::{Either, Itertools}; use language::{ AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, - BufferSnapshot, Capability, CharClassifier, CharKind, CodeLabel, CursorShape, DiagnosticEntry, - DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, - Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, - TransactionId, TreeSitterOptions, WordsQuery, + BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape, + DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, + IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, + TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, language_settings, @@ -3123,7 +3123,8 @@ impl Editor { let position_matches = start_offset == completion_position.to_offset(buffer); let continue_showing = if position_matches { if self.snippet_stack.is_empty() { - buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) + buffer.char_kind_before(start_offset, Some(CharScopeContext::Completion)) + == Some(CharKind::Word) } else { // Snippet choices can be shown even when the cursor is in whitespace. // Dismissing the menu with actions like backspace is handled by @@ -3551,7 +3552,7 @@ impl Editor { let position = display_map .clip_point(position, Bias::Left) .to_offset(&display_map, Bias::Left); - let (range, _) = buffer.surrounding_word(position, false); + let (range, _) = buffer.surrounding_word(position, None); start = buffer.anchor_before(range.start); end = buffer.anchor_before(range.end); mode = SelectMode::Word(start..end); @@ -3711,10 +3712,10 @@ impl Editor { .to_offset(&display_map, Bias::Left); let original_range = original_range.to_offset(buffer); - let head_offset = if buffer.is_inside_word(offset, false) + let head_offset = if buffer.is_inside_word(offset, None) || original_range.contains(&offset) { - let (word_range, _) = buffer.surrounding_word(offset, false); + let (word_range, _) = buffer.surrounding_word(offset, None); if word_range.start < original_range.start { word_range.start } else { @@ -4244,7 +4245,7 @@ impl Editor { let is_word_char = text.chars().next().is_none_or(|char| { let classifier = snapshot .char_classifier_at(start_anchor.to_offset(&snapshot)) - .ignore_punctuation(true); + .scope_context(Some(CharScopeContext::LinkedEdit)); classifier.is_word(char) }); @@ -5101,7 +5102,8 @@ 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, true); + let (word_range, kind) = + buffer.surrounding_word(offset, Some(CharScopeContext::Completion)); if offset > word_range.start && kind == Some(CharKind::Word) { Some( buffer @@ -5571,7 +5573,7 @@ impl Editor { } = buffer_position; let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = - buffer_snapshot.surrounding_word(buffer_position, false) + buffer_snapshot.surrounding_word(buffer_position, None) { let word_to_exclude = buffer_snapshot .text_for_range(word_range.clone()) @@ -6787,8 +6789,8 @@ impl Editor { } let snapshot = cursor_buffer.read(cx).snapshot(); - let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position, false); - let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position, false); + let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position, None); + let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position, None); if start_word_range != end_word_range { self.document_highlights_task.take(); self.clear_background_highlights::(cx); @@ -11440,7 +11442,7 @@ impl Editor { let selection_is_empty = selection.is_empty(); let (start, end) = if selection_is_empty { - let (word_range, _) = buffer.surrounding_word(selection.start, false); + let (word_range, _) = buffer.surrounding_word(selection.start, None); (word_range.start, word_range.end) } else { ( @@ -14206,8 +14208,8 @@ impl Editor { start_offset + query_match.start()..start_offset + query_match.end(); if !select_next_state.wordwise - || (!buffer.is_inside_word(offset_range.start, false) - && !buffer.is_inside_word(offset_range.end, false)) + || (!buffer.is_inside_word(offset_range.start, None) + && !buffer.is_inside_word(offset_range.end, None)) { // TODO: This is n^2, because we might check all the selections if !selections @@ -14271,7 +14273,7 @@ impl Editor { if only_carets { for selection in &mut selections { - let (word_range, _) = buffer.surrounding_word(selection.start, false); + let (word_range, _) = buffer.surrounding_word(selection.start, None); selection.start = word_range.start; selection.end = word_range.end; selection.goal = SelectionGoal::None; @@ -14356,8 +14358,8 @@ impl Editor { }; if !select_next_state.wordwise - || (!buffer.is_inside_word(offset_range.start, false) - && !buffer.is_inside_word(offset_range.end, false)) + || (!buffer.is_inside_word(offset_range.start, None) + && !buffer.is_inside_word(offset_range.end, None)) { new_selections.push(offset_range.start..offset_range.end); } @@ -14431,8 +14433,8 @@ impl Editor { end_offset - query_match.end()..end_offset - query_match.start(); if !select_prev_state.wordwise - || (!buffer.is_inside_word(offset_range.start, false) - && !buffer.is_inside_word(offset_range.end, false)) + || (!buffer.is_inside_word(offset_range.start, None) + && !buffer.is_inside_word(offset_range.end, None)) { next_selected_range = Some(offset_range); break; @@ -14490,7 +14492,7 @@ impl Editor { if only_carets { for selection in &mut selections { - let (word_range, _) = buffer.surrounding_word(selection.start, false); + let (word_range, _) = buffer.surrounding_word(selection.start, None); selection.start = word_range.start; selection.end = word_range.end; selection.goal = SelectionGoal::None; @@ -14968,11 +14970,10 @@ impl Editor { if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) { // manually select word at selection if ["string_content", "inline"].contains(&node.kind()) { - let (word_range, _) = buffer.surrounding_word(old_range.start, false); + let (word_range, _) = buffer.surrounding_word(old_range.start, None); // ignore if word is already selected if !word_range.is_empty() && old_range != word_range { - let (last_word_range, _) = - buffer.surrounding_word(old_range.end, false); + let (last_word_range, _) = buffer.surrounding_word(old_range.end, None); // only select word if start and end point belongs to same word if word_range == last_word_range { selected_larger_node = true; @@ -22545,7 +22546,8 @@ fn snippet_completions( let mut is_incomplete = false; let mut completions: Vec = Vec::new(); for (scope, snippets) in scopes.into_iter() { - let classifier = CharClassifier::new(Some(scope)).for_completion(true); + let classifier = + CharClassifier::new(Some(scope)).scope_context(Some(CharScopeContext::Completion)); let mut last_word = chars .chars() .take_while(|c| classifier.is_word(*c)) @@ -22766,7 +22768,9 @@ impl CompletionProvider for Entity { if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input { return false; } - let classifier = snapshot.char_classifier_at(position).for_completion(true); + let classifier = snapshot + .char_classifier_at(position) + .scope_context(Some(CharScopeContext::Completion)); if trigger_in_words && classifier.is_word(char) { return true; } @@ -22879,7 +22883,7 @@ impl SemanticsProvider for Entity { // Fallback on using TreeSitter info to determine identifier range buffer.read_with(cx, |buffer, _| { let snapshot = buffer.snapshot(); - let (range, kind) = snapshot.surrounding_word(position, false); + let (range, kind) = snapshot.surrounding_word(position, None); if kind != Some(CharKind::Word) { return None; } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f18187d558f1cb90e137d06591ec5b2ecb7b1654..05742cd00bb834550ee20377ff46da6649272f43 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -13,6 +13,7 @@ use crate::{ }, }; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind}; +use collections::HashMap; use futures::StreamExt; use gpui::{ BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal, @@ -23773,6 +23774,28 @@ async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) { }); } +fn set_linked_edit_ranges( + opening: (Point, Point), + closing: (Point, Point), + editor: &mut Editor, + cx: &mut Context, +) { + let Some((buffer, _)) = editor + .buffer + .read(cx) + .text_anchor_for_position(editor.selections.newest_anchor().start, cx) + else { + panic!("Failed to get buffer for selection position"); + }; + let buffer = buffer.read(cx); + let buffer_id = buffer.remote_id(); + let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1); + let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1); + let mut linked_ranges = HashMap::default(); + linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]); + editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges); +} + #[gpui::test] async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -23851,22 +23874,12 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]); }); - let Some((buffer, _)) = editor - .buffer - .read(cx) - .text_anchor_for_position(editor.selections.newest_anchor().start, cx) - else { - panic!("Failed to get buffer for selection position"); - }; - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - let opening_range = - buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3)); - let closing_range = - buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8)); - let mut linked_ranges = HashMap::default(); - linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]); - editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges); + set_linked_edit_ranges( + (Point::new(0, 1), Point::new(0, 3)), + (Point::new(0, 6), Point::new(0, 8)), + editor, + cx, + ); }); let mut completion_handle = fake_server.set_request_handler::(move |_, _| async move { @@ -23910,6 +23923,77 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig { + name: "TSX".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["tsx".to_string()], + ..LanguageMatcher::default() + }, + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "<".into(), + end: ">".into(), + close: true, + ..Default::default() + }], + ..Default::default() + }, + linked_edit_characters: HashSet::from_iter(['.']), + ..Default::default() + }, + Some(tree_sitter_typescript::LANGUAGE_TSX.into()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Test typing > does not extend linked pair + cx.set_state(""); + cx.update_editor(|editor, _, cx| { + set_linked_edit_ranges( + (Point::new(0, 1), Point::new(0, 4)), + (Point::new(0, 11), Point::new(0, 14)), + editor, + cx, + ); + }); + cx.update_editor(|editor, window, cx| { + editor.handle_input(">", window, cx); + }); + cx.assert_editor_state("
ˇ
"); + + // Test typing . do extend linked pair + cx.set_state(""); + cx.update_editor(|editor, _, cx| { + set_linked_edit_ranges( + (Point::new(0, 1), Point::new(0, 9)), + (Point::new(0, 12), Point::new(0, 20)), + editor, + cx, + ); + }); + cx.update_editor(|editor, window, cx| { + editor.handle_input(".", window, cx); + }); + cx.assert_editor_state(""); + cx.update_editor(|editor, _, cx| { + set_linked_edit_ranges( + (Point::new(0, 1), Point::new(0, 10)), + (Point::new(0, 13), Point::new(0, 21)), + editor, + cx, + ); + }); + cx.update_editor(|editor, window, cx| { + editor.handle_input("V", window, cx); + }); + cx.assert_editor_state(""); +} + #[gpui::test] async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index d5a3f17822ff7f0f2324414aeaa9819b8605f53b..2b91f8cb1ca4c515d2f09997f07b42d611b4baaf 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -627,7 +627,7 @@ pub fn show_link_definition( TriggerPoint::Text(trigger_anchor) => { // If no symbol range returned from language server, use the surrounding word. let (offset_range, _) = - snapshot.surrounding_word(*trigger_anchor, false); + snapshot.surrounding_word(*trigger_anchor, None); RangeInEditor::Text( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index bf21d6b461e6fdc082fdd1431f13b8daae730824..a1b311a3ac3b8ed330fee0f015c41d327efe342d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -17,8 +17,8 @@ use gpui::{ ParentElement, Pixels, SharedString, Styled, Task, WeakEntity, Window, point, }; use language::{ - Bias, Buffer, BufferRow, CharKind, DiskState, LocalFile, Point, SelectionGoal, - proto::serialize_anchor as serialize_text_anchor, + Bias, Buffer, BufferRow, CharKind, CharScopeContext, DiskState, LocalFile, Point, + SelectionGoal, proto::serialize_anchor as serialize_text_anchor, }; use lsp::DiagnosticSeverity; use project::{ @@ -1573,7 +1573,8 @@ impl SearchableItem for Editor { } SeedQuerySetting::Selection => String::new(), SeedQuerySetting::Always => { - let (range, kind) = snapshot.surrounding_word(selection.start, true); + let (range, kind) = + snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion)); if kind == Some(CharKind::Word) { let text: String = snapshot.text_for_range(range).collect(); if !text.trim().is_empty() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1a7fca79f64c2c253117a3acde8c4d7519a9c282..d5d83da47bc18a4fd15f59df2ddb2238ceb768d4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -546,6 +546,23 @@ pub enum CharKind { Word, } +/// Context for character classification within a specific scope. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum CharScopeContext { + /// Character classification for completion queries. + /// + /// This context treats certain characters as word constituents that would + /// normally be considered punctuation, such as '-' in Tailwind classes + /// ("bg-yellow-100") or '.' in import paths ("foo.ts"). + Completion, + /// Character classification for linked edits. + /// + /// This context handles characters that should be treated as part of + /// identifiers during linked editing operations, such as '.' in JSX + /// component names like ``. + LinkedEdit, +} + /// A runnable is a set of data about a region that could be resolved into a task pub struct Runnable { pub tags: SmallVec<[RunnableTag; 1]>, @@ -3449,16 +3466,14 @@ impl BufferSnapshot { pub fn surrounding_word( &self, start: T, - for_completion: bool, + scope_context: Option, ) -> (Range, Option) { let mut start = start.to_offset(self); let mut end = start; let mut next_chars = self.chars_at(start).take(128).peekable(); let mut prev_chars = self.reversed_chars_at(start).take(128).peekable(); - let classifier = self - .char_classifier_at(start) - .for_completion(for_completion); + let classifier = self.char_classifier_at(start).scope_context(scope_context); let word_kind = cmp::max( prev_chars.peek().copied().map(|c| classifier.kind(c)), next_chars.peek().copied().map(|c| classifier.kind(c)), @@ -5212,7 +5227,7 @@ pub(crate) fn contiguous_ranges( #[derive(Default, Debug)] pub struct CharClassifier { scope: Option, - for_completion: bool, + scope_context: Option, ignore_punctuation: bool, } @@ -5220,14 +5235,14 @@ impl CharClassifier { pub fn new(scope: Option) -> Self { Self { scope, - for_completion: false, + scope_context: None, ignore_punctuation: false, } } - pub fn for_completion(self, for_completion: bool) -> Self { + pub fn scope_context(self, scope_context: Option) -> Self { Self { - for_completion, + scope_context, ..self } } @@ -5257,10 +5272,10 @@ impl CharClassifier { } if let Some(scope) = &self.scope { - let characters = if self.for_completion { - scope.completion_query_characters() - } else { - scope.word_characters() + let characters = match self.scope_context { + Some(CharScopeContext::Completion) => scope.completion_query_characters(), + Some(CharScopeContext::LinkedEdit) => scope.linked_edit_characters(), + None => scope.word_characters(), }; if let Some(characters) = characters && characters.contains(&c) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2af5657ea776ddd85bf9495d3c1f32c2d0c69ac2..3e9f3bf1bd0cb4719f5442e1b1bd9e357ac9efca 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -780,6 +780,9 @@ pub struct LanguageConfig { /// A list of characters that Zed should treat as word characters for completion queries. #[serde(default)] pub completion_query_characters: HashSet, + /// A list of characters that Zed should treat as word characters for linked edit operations. + #[serde(default)] + pub linked_edit_characters: HashSet, /// A list of preferred debuggers for this language. #[serde(default)] pub debuggers: IndexSet, @@ -916,6 +919,8 @@ pub struct LanguageConfigOverride { #[serde(default)] pub completion_query_characters: Override>, #[serde(default)] + pub linked_edit_characters: Override>, + #[serde(default)] pub opt_into_language_servers: Vec, #[serde(default)] pub prefer_label_for_snippet: Option, @@ -974,6 +979,7 @@ impl Default for LanguageConfig { hidden: false, jsx_tag_auto_close: None, completion_query_characters: Default::default(), + linked_edit_characters: Default::default(), debuggers: Default::default(), } } @@ -2011,6 +2017,15 @@ impl LanguageScope { ) } + /// Returns a list of language-specific characters that are considered part of + /// identifiers during linked editing operations. + pub fn linked_edit_characters(&self) -> Option<&HashSet> { + Override::as_option( + self.config_override().map(|o| &o.linked_edit_characters), + Some(&self.language.config.linked_edit_characters), + ) + } + /// Returns whether to prefer snippet `label` over `new_text` to replace text when /// completion is accepted. /// diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index 11d8a070d213852f0a98078f2ed8c76c9cced47b..5a74362d7d3cb2404cc67ed32595a06efd291ca4 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -1,4 +1,4 @@ -use crate::{CharClassifier, CharKind, LanguageScope}; +use crate::{CharClassifier, CharKind, CharScopeContext, LanguageScope}; use anyhow::{Context, anyhow}; use imara_diff::{ Algorithm, UnifiedDiffBuilder, diff, @@ -181,7 +181,8 @@ fn diff_internal( } fn tokenize(text: &str, language_scope: Option) -> impl Iterator { - let classifier = CharClassifier::new(language_scope).for_completion(true); + let classifier = + CharClassifier::new(language_scope).scope_context(Some(CharScopeContext::Completion)); let mut chars = text.char_indices(); let mut prev = None; let mut start_ix = 0; diff --git a/crates/languages/src/javascript/config.toml b/crates/languages/src/javascript/config.toml index 128eac0e4dda2b5b437c494e862970c23a8df3a1..3bac37aa13ed34c18d1fb8e4f70e0905938e5213 100644 --- a/crates/languages/src/javascript/config.toml +++ b/crates/languages/src/javascript/config.toml @@ -30,6 +30,9 @@ close_tag_node_name = "jsx_closing_element" jsx_element_node_name = "jsx_element" tag_name_node_name = "identifier" +[overrides.default] +linked_edit_characters = ["."] + [overrides.element] line_comments = { remove = true } block_comment = { start = "{/* ", prefix = "", end = "*/}", tab_size = 0 } diff --git a/crates/languages/src/tsx/config.toml b/crates/languages/src/tsx/config.toml index b5ef5bd56df2097bc920f02b87d07e4118d7b0d1..d0a4eb6532db621d741df2fbc99125e1c037ccdf 100644 --- a/crates/languages/src/tsx/config.toml +++ b/crates/languages/src/tsx/config.toml @@ -29,6 +29,9 @@ jsx_element_node_name = "jsx_element" tag_name_node_name = "identifier" tag_name_node_name_alternates = ["member_expression"] +[overrides.default] +linked_edit_characters = ["."] + [overrides.element] line_comments = { remove = true } block_comment = { start = "{/*", prefix = "", end = "*/}", tab_size = 0 } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 2ceeffc89061aa429727142a1659a392d6374b09..c79bc03489be89ad00d10392c520fe13e7748a60 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -17,10 +17,10 @@ use gpui::{App, AppContext as _, Context, Entity, EntityId, EventEmitter, Task}; use itertools::Itertools; use language::{ AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier, - CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentGuideSettings, - IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, - PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, - TreeSitterOptions, Unclipped, + CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntry, DiskState, File, + IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, + OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, + ToPoint as _, TransactionId, TreeSitterOptions, Unclipped, language_settings::{LanguageSettings, language_settings}, }; @@ -4204,11 +4204,15 @@ impl MultiBufferSnapshot { self.diffs.values().any(|diff| !diff.is_empty()) } - pub fn is_inside_word(&self, position: T, for_completion: bool) -> bool { + pub fn is_inside_word( + &self, + position: T, + scope_context: Option, + ) -> bool { let position = position.to_offset(self); let classifier = self .char_classifier_at(position) - .for_completion(for_completion); + .scope_context(scope_context); let next_char_kind = self.chars_at(position).next().map(|c| classifier.kind(c)); let prev_char_kind = self .reversed_chars_at(position) @@ -4220,16 +4224,14 @@ impl MultiBufferSnapshot { pub fn surrounding_word( &self, start: T, - for_completion: bool, + scope_context: Option, ) -> (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 classifier = self - .char_classifier_at(start) - .for_completion(for_completion); + let classifier = self.char_classifier_at(start).scope_context(scope_context); let word_kind = cmp::max( prev_chars.peek().copied().map(|c| classifier.kind(c)), @@ -4258,12 +4260,10 @@ impl MultiBufferSnapshot { pub fn char_kind_before( &self, start: T, - for_completion: bool, + scope_context: Option, ) -> Option { let start = start.to_offset(self); - let classifier = self - .char_classifier_at(start) - .for_completion(for_completion); + let classifier = self.char_classifier_at(start).scope_context(scope_context); self.reversed_chars_at(start) .next() .map(|ch| classifier.kind(ch)) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a960e1183dd46537ef3aee829cd9753b28001480..5ec6e502bd85a25b6755c6994feff7a3062c919c 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -16,8 +16,8 @@ use collections::{HashMap, HashSet}; use futures::future; use gpui::{App, AsyncApp, Entity, Task}; use language::{ - Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16, - ToOffset, ToPointUtf16, Transaction, Unclipped, + Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext, + OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, language_settings::{InlayHintKind, LanguageSettings, language_settings}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, @@ -350,7 +350,7 @@ impl LspCommand for PrepareRename { } Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { let snapshot = buffer.snapshot(); - let (range, _) = snapshot.surrounding_word(self.position, false); + let (range, _) = snapshot.surrounding_word(self.position, None); let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); Ok(PrepareRenameResponse::Success(range)) } @@ -2293,7 +2293,10 @@ impl LspCommand for GetCompletions { range_for_token .get_or_insert_with(|| { let offset = self.position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset, true); + let (range, kind) = snapshot.surrounding_word( + offset, + Some(CharScopeContext::Completion), + ); let range = if kind == Some(CharKind::Word) { range } else { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9e7fb4a564751335db7ba6fe2afe61563ea0f161..c7fb8ffa35ea090296f137b11f08379db968ce3d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -30,7 +30,9 @@ use gpui::{ Render, Subscription, Task, WeakEntity, Window, actions, }; use insert::{NormalBefore, TemporaryNormal}; -use language::{CharKind, CursorShape, Point, Selection, SelectionGoal, TransactionId}; +use language::{ + CharKind, CharScopeContext, CursorShape, Point, Selection, SelectionGoal, TransactionId, +}; pub use mode_indicator::ModeIndicator; use motion::Motion; use normal::search::SearchSubmit; @@ -1347,7 +1349,8 @@ impl Vim { let selection = editor.selections.newest::(cx); let snapshot = &editor.snapshot(window, cx).buffer_snapshot; - let (range, kind) = snapshot.surrounding_word(selection.start, true); + let (range, kind) = + snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion)); if kind == Some(CharKind::Word) { let text: String = snapshot.text_for_range(range).collect(); if !text.trim().is_empty() {