editor: Fix adding extraneous closing tags within TSX (#38534)
This commit is contained in:
+32
-28
@@ -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<String> {
|
||||
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::<DocumentHighlightRead>(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<Completion> = 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<Project> {
|
||||
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<Project> {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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<Editor>,
|
||||
) {
|
||||
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::<lsp::request::Completion, _, _>(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("<divˇ<div></div>");
|
||||
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("<div>ˇ<div></div>");
|
||||
|
||||
// Test typing . do extend linked pair
|
||||
cx.set_state("<Animatedˇ></Animated>");
|
||||
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("<Animated.ˇ></Animated.>");
|
||||
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("<Animated.Vˇ></Animated.V>");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user