Skip to content

Commit be77682

Browse files
authored
editor: Fix adding extraneous closing tags within TSX (#38534)
1 parent 8df616e commit be77682

File tree

13 files changed

+218
-84
lines changed

13 files changed

+218
-84
lines changed

crates/debugger_ui/src/session/running/console.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use gpui::{
1212
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
1313
Render, Subscription, Task, TextStyle, WeakEntity, actions,
1414
};
15-
use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset};
15+
use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset};
1616
use menu::{Confirm, SelectNext, SelectPrevious};
1717
use project::{
1818
Completion, CompletionDisplayOptions, CompletionResponse,
@@ -575,7 +575,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
575575
return false;
576576
}
577577

578-
let classifier = snapshot.char_classifier_at(position).for_completion(true);
578+
let classifier = snapshot
579+
.char_classifier_at(position)
580+
.scope_context(Some(CharScopeContext::Completion));
579581
if trigger_in_words && classifier.is_word(char) {
580582
return true;
581583
}

crates/editor/src/editor.rs

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
121121
use itertools::{Either, Itertools};
122122
use language::{
123123
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
124-
BufferSnapshot, Capability, CharClassifier, CharKind, CodeLabel, CursorShape, DiagnosticEntry,
125-
DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize,
126-
Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject,
127-
TransactionId, TreeSitterOptions, WordsQuery,
124+
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
125+
DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
126+
IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal,
127+
TextObject, TransactionId, TreeSitterOptions, WordsQuery,
128128
language_settings::{
129129
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
130130
all_language_settings, language_settings,
@@ -3123,7 +3123,8 @@ impl Editor {
31233123
let position_matches = start_offset == completion_position.to_offset(buffer);
31243124
let continue_showing = if position_matches {
31253125
if self.snippet_stack.is_empty() {
3126-
buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
3126+
buffer.char_kind_before(start_offset, Some(CharScopeContext::Completion))
3127+
== Some(CharKind::Word)
31273128
} else {
31283129
// Snippet choices can be shown even when the cursor is in whitespace.
31293130
// Dismissing the menu with actions like backspace is handled by
@@ -3551,7 +3552,7 @@ impl Editor {
35513552
let position = display_map
35523553
.clip_point(position, Bias::Left)
35533554
.to_offset(&display_map, Bias::Left);
3554-
let (range, _) = buffer.surrounding_word(position, false);
3555+
let (range, _) = buffer.surrounding_word(position, None);
35553556
start = buffer.anchor_before(range.start);
35563557
end = buffer.anchor_before(range.end);
35573558
mode = SelectMode::Word(start..end);
@@ -3711,10 +3712,10 @@ impl Editor {
37113712
.to_offset(&display_map, Bias::Left);
37123713
let original_range = original_range.to_offset(buffer);
37133714

3714-
let head_offset = if buffer.is_inside_word(offset, false)
3715+
let head_offset = if buffer.is_inside_word(offset, None)
37153716
|| original_range.contains(&offset)
37163717
{
3717-
let (word_range, _) = buffer.surrounding_word(offset, false);
3718+
let (word_range, _) = buffer.surrounding_word(offset, None);
37183719
if word_range.start < original_range.start {
37193720
word_range.start
37203721
} else {
@@ -4244,7 +4245,7 @@ impl Editor {
42444245
let is_word_char = text.chars().next().is_none_or(|char| {
42454246
let classifier = snapshot
42464247
.char_classifier_at(start_anchor.to_offset(&snapshot))
4247-
.ignore_punctuation(true);
4248+
.scope_context(Some(CharScopeContext::LinkedEdit));
42484249
classifier.is_word(char)
42494250
});
42504251

@@ -5101,7 +5102,8 @@ impl Editor {
51015102

51025103
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
51035104
let offset = position.to_offset(buffer);
5104-
let (word_range, kind) = buffer.surrounding_word(offset, true);
5105+
let (word_range, kind) =
5106+
buffer.surrounding_word(offset, Some(CharScopeContext::Completion));
51055107
if offset > word_range.start && kind == Some(CharKind::Word) {
51065108
Some(
51075109
buffer
@@ -5571,7 +5573,7 @@ impl Editor {
55715573
} = buffer_position;
55725574

55735575
let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) =
5574-
buffer_snapshot.surrounding_word(buffer_position, false)
5576+
buffer_snapshot.surrounding_word(buffer_position, None)
55755577
{
55765578
let word_to_exclude = buffer_snapshot
55775579
.text_for_range(word_range.clone())
@@ -6787,8 +6789,8 @@ impl Editor {
67876789
}
67886790

67896791
let snapshot = cursor_buffer.read(cx).snapshot();
6790-
let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position, false);
6791-
let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position, false);
6792+
let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position, None);
6793+
let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position, None);
67926794
if start_word_range != end_word_range {
67936795
self.document_highlights_task.take();
67946796
self.clear_background_highlights::<DocumentHighlightRead>(cx);
@@ -11440,7 +11442,7 @@ impl Editor {
1144011442
let selection_is_empty = selection.is_empty();
1144111443

1144211444
let (start, end) = if selection_is_empty {
11443-
let (word_range, _) = buffer.surrounding_word(selection.start, false);
11445+
let (word_range, _) = buffer.surrounding_word(selection.start, None);
1144411446
(word_range.start, word_range.end)
1144511447
} else {
1144611448
(
@@ -14206,8 +14208,8 @@ impl Editor {
1420614208
start_offset + query_match.start()..start_offset + query_match.end();
1420714209

1420814210
if !select_next_state.wordwise
14209-
|| (!buffer.is_inside_word(offset_range.start, false)
14210-
&& !buffer.is_inside_word(offset_range.end, false))
14211+
|| (!buffer.is_inside_word(offset_range.start, None)
14212+
&& !buffer.is_inside_word(offset_range.end, None))
1421114213
{
1421214214
// TODO: This is n^2, because we might check all the selections
1421314215
if !selections
@@ -14271,7 +14273,7 @@ impl Editor {
1427114273

1427214274
if only_carets {
1427314275
for selection in &mut selections {
14274-
let (word_range, _) = buffer.surrounding_word(selection.start, false);
14276+
let (word_range, _) = buffer.surrounding_word(selection.start, None);
1427514277
selection.start = word_range.start;
1427614278
selection.end = word_range.end;
1427714279
selection.goal = SelectionGoal::None;
@@ -14356,8 +14358,8 @@ impl Editor {
1435614358
};
1435714359

1435814360
if !select_next_state.wordwise
14359-
|| (!buffer.is_inside_word(offset_range.start, false)
14360-
&& !buffer.is_inside_word(offset_range.end, false))
14361+
|| (!buffer.is_inside_word(offset_range.start, None)
14362+
&& !buffer.is_inside_word(offset_range.end, None))
1436114363
{
1436214364
new_selections.push(offset_range.start..offset_range.end);
1436314365
}
@@ -14431,8 +14433,8 @@ impl Editor {
1443114433
end_offset - query_match.end()..end_offset - query_match.start();
1443214434

1443314435
if !select_prev_state.wordwise
14434-
|| (!buffer.is_inside_word(offset_range.start, false)
14435-
&& !buffer.is_inside_word(offset_range.end, false))
14436+
|| (!buffer.is_inside_word(offset_range.start, None)
14437+
&& !buffer.is_inside_word(offset_range.end, None))
1443614438
{
1443714439
next_selected_range = Some(offset_range);
1443814440
break;
@@ -14490,7 +14492,7 @@ impl Editor {
1449014492

1449114493
if only_carets {
1449214494
for selection in &mut selections {
14493-
let (word_range, _) = buffer.surrounding_word(selection.start, false);
14495+
let (word_range, _) = buffer.surrounding_word(selection.start, None);
1449414496
selection.start = word_range.start;
1449514497
selection.end = word_range.end;
1449614498
selection.goal = SelectionGoal::None;
@@ -14968,11 +14970,10 @@ impl Editor {
1496814970
if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) {
1496914971
// manually select word at selection
1497014972
if ["string_content", "inline"].contains(&node.kind()) {
14971-
let (word_range, _) = buffer.surrounding_word(old_range.start, false);
14973+
let (word_range, _) = buffer.surrounding_word(old_range.start, None);
1497214974
// ignore if word is already selected
1497314975
if !word_range.is_empty() && old_range != word_range {
14974-
let (last_word_range, _) =
14975-
buffer.surrounding_word(old_range.end, false);
14976+
let (last_word_range, _) = buffer.surrounding_word(old_range.end, None);
1497614977
// only select word if start and end point belongs to same word
1497714978
if word_range == last_word_range {
1497814979
selected_larger_node = true;
@@ -22545,7 +22546,8 @@ fn snippet_completions(
2254522546
let mut is_incomplete = false;
2254622547
let mut completions: Vec<Completion> = Vec::new();
2254722548
for (scope, snippets) in scopes.into_iter() {
22548-
let classifier = CharClassifier::new(Some(scope)).for_completion(true);
22549+
let classifier =
22550+
CharClassifier::new(Some(scope)).scope_context(Some(CharScopeContext::Completion));
2254922551
let mut last_word = chars
2255022552
.chars()
2255122553
.take_while(|c| classifier.is_word(*c))
@@ -22766,7 +22768,9 @@ impl CompletionProvider for Entity<Project> {
2276622768
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
2276722769
return false;
2276822770
}
22769-
let classifier = snapshot.char_classifier_at(position).for_completion(true);
22771+
let classifier = snapshot
22772+
.char_classifier_at(position)
22773+
.scope_context(Some(CharScopeContext::Completion));
2277022774
if trigger_in_words && classifier.is_word(char) {
2277122775
return true;
2277222776
}
@@ -22879,7 +22883,7 @@ impl SemanticsProvider for Entity<Project> {
2287922883
// Fallback on using TreeSitter info to determine identifier range
2288022884
buffer.read_with(cx, |buffer, _| {
2288122885
let snapshot = buffer.snapshot();
22882-
let (range, kind) = snapshot.surrounding_word(position, false);
22886+
let (range, kind) = snapshot.surrounding_word(position, None);
2288322887
if kind != Some(CharKind::Word) {
2288422888
return None;
2288522889
}

crates/editor/src/editor_tests.rs

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::{
1313
},
1414
};
1515
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16+
use collections::HashMap;
1617
use futures::StreamExt;
1718
use gpui::{
1819
BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
@@ -23773,6 +23774,28 @@ async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
2377323774
});
2377423775
}
2377523776

23777+
fn set_linked_edit_ranges(
23778+
opening: (Point, Point),
23779+
closing: (Point, Point),
23780+
editor: &mut Editor,
23781+
cx: &mut Context<Editor>,
23782+
) {
23783+
let Some((buffer, _)) = editor
23784+
.buffer
23785+
.read(cx)
23786+
.text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23787+
else {
23788+
panic!("Failed to get buffer for selection position");
23789+
};
23790+
let buffer = buffer.read(cx);
23791+
let buffer_id = buffer.remote_id();
23792+
let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23793+
let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23794+
let mut linked_ranges = HashMap::default();
23795+
linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23796+
editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23797+
}
23798+
2377623799
#[gpui::test]
2377723800
async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
2377823801
init_test(cx, |_| {});
@@ -23851,22 +23874,12 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
2385123874
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
2385223875
selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
2385323876
});
23854-
let Some((buffer, _)) = editor
23855-
.buffer
23856-
.read(cx)
23857-
.text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23858-
else {
23859-
panic!("Failed to get buffer for selection position");
23860-
};
23861-
let buffer = buffer.read(cx);
23862-
let buffer_id = buffer.remote_id();
23863-
let opening_range =
23864-
buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23865-
let closing_range =
23866-
buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23867-
let mut linked_ranges = HashMap::default();
23868-
linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23869-
editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23877+
set_linked_edit_ranges(
23878+
(Point::new(0, 1), Point::new(0, 3)),
23879+
(Point::new(0, 6), Point::new(0, 8)),
23880+
editor,
23881+
cx,
23882+
);
2387023883
});
2387123884
let mut completion_handle =
2387223885
fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
@@ -23910,6 +23923,77 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
2391023923
});
2391123924
}
2391223925

23926+
#[gpui::test]
23927+
async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23928+
init_test(cx, |_| {});
23929+
23930+
let mut cx = EditorTestContext::new(cx).await;
23931+
let language = Arc::new(Language::new(
23932+
LanguageConfig {
23933+
name: "TSX".into(),
23934+
matcher: LanguageMatcher {
23935+
path_suffixes: vec!["tsx".to_string()],
23936+
..LanguageMatcher::default()
23937+
},
23938+
brackets: BracketPairConfig {
23939+
pairs: vec![BracketPair {
23940+
start: "<".into(),
23941+
end: ">".into(),
23942+
close: true,
23943+
..Default::default()
23944+
}],
23945+
..Default::default()
23946+
},
23947+
linked_edit_characters: HashSet::from_iter(['.']),
23948+
..Default::default()
23949+
},
23950+
Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
23951+
));
23952+
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23953+
23954+
// Test typing > does not extend linked pair
23955+
cx.set_state("<divˇ<div></div>");
23956+
cx.update_editor(|editor, _, cx| {
23957+
set_linked_edit_ranges(
23958+
(Point::new(0, 1), Point::new(0, 4)),
23959+
(Point::new(0, 11), Point::new(0, 14)),
23960+
editor,
23961+
cx,
23962+
);
23963+
});
23964+
cx.update_editor(|editor, window, cx| {
23965+
editor.handle_input(">", window, cx);
23966+
});
23967+
cx.assert_editor_state("<div>ˇ<div></div>");
23968+
23969+
// Test typing . do extend linked pair
23970+
cx.set_state("<Animatedˇ></Animated>");
23971+
cx.update_editor(|editor, _, cx| {
23972+
set_linked_edit_ranges(
23973+
(Point::new(0, 1), Point::new(0, 9)),
23974+
(Point::new(0, 12), Point::new(0, 20)),
23975+
editor,
23976+
cx,
23977+
);
23978+
});
23979+
cx.update_editor(|editor, window, cx| {
23980+
editor.handle_input(".", window, cx);
23981+
});
23982+
cx.assert_editor_state("<Animated.ˇ></Animated.>");
23983+
cx.update_editor(|editor, _, cx| {
23984+
set_linked_edit_ranges(
23985+
(Point::new(0, 1), Point::new(0, 10)),
23986+
(Point::new(0, 13), Point::new(0, 21)),
23987+
editor,
23988+
cx,
23989+
);
23990+
});
23991+
cx.update_editor(|editor, window, cx| {
23992+
editor.handle_input("V", window, cx);
23993+
});
23994+
cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
23995+
}
23996+
2391323997
#[gpui::test]
2391423998
async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
2391523999
init_test(cx, |_| {});

crates/editor/src/hover_links.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ pub fn show_link_definition(
627627
TriggerPoint::Text(trigger_anchor) => {
628628
// If no symbol range returned from language server, use the surrounding word.
629629
let (offset_range, _) =
630-
snapshot.surrounding_word(*trigger_anchor, false);
630+
snapshot.surrounding_word(*trigger_anchor, None);
631631
RangeInEditor::Text(
632632
snapshot.anchor_before(offset_range.start)
633633
..snapshot.anchor_after(offset_range.end),

crates/editor/src/items.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use gpui::{
1717
ParentElement, Pixels, SharedString, Styled, Task, WeakEntity, Window, point,
1818
};
1919
use language::{
20-
Bias, Buffer, BufferRow, CharKind, DiskState, LocalFile, Point, SelectionGoal,
21-
proto::serialize_anchor as serialize_text_anchor,
20+
Bias, Buffer, BufferRow, CharKind, CharScopeContext, DiskState, LocalFile, Point,
21+
SelectionGoal, proto::serialize_anchor as serialize_text_anchor,
2222
};
2323
use lsp::DiagnosticSeverity;
2424
use project::{
@@ -1573,7 +1573,8 @@ impl SearchableItem for Editor {
15731573
}
15741574
SeedQuerySetting::Selection => String::new(),
15751575
SeedQuerySetting::Always => {
1576-
let (range, kind) = snapshot.surrounding_word(selection.start, true);
1576+
let (range, kind) =
1577+
snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion));
15771578
if kind == Some(CharKind::Word) {
15781579
let text: String = snapshot.text_for_range(range).collect();
15791580
if !text.trim().is_empty() {

0 commit comments

Comments
 (0)