From 1dd470ca48fef83e920972f175f232f9ea252b44 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 18 Jul 2025 22:39:00 +0530 Subject: [PATCH] editor: Fix double $ sign on completion accept in PHP (#34726) Closes #33510 https://github.com/zed-extensions/php/issues/29 If certain language servers do not provide an insert/replace range, we use `surrounding_word` as a fallback for that range, which internally uses `word_characters`. It makes sense to use `completion_query_characters` instead of `word_characters` to get that range, because we use `completion_query_characters` to query completions in the first place. That means, for some hypothetical reason (e.g., if the Tailwind server stops providing insert/replace ranges), we would correctly fall back to the range "bg-blue-200^" instead of "200^", because `completion_query_characters` includes "-" in this case. For this particular fix, right now the default PHP language server `phpactor` does not provide an insert/replace range, and hence completion query character is used, which is `$` in this case. Note that `$` isn't in word characters for reasons mentioned here: https://github.com/zed-extensions/php/issues/14 Release Notes: - Fixed an issue where accepting variable completion in PHP would result in a double $ sign in the prefix. --- crates/editor/src/editor.rs | 8 ++++---- crates/language/src/buffer.rs | 10 ++++++++-- crates/project/src/lsp_command.rs | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c3187f6b51..b8dcdd35e0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5451,7 +5451,7 @@ impl Editor { }; let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = - buffer_snapshot.surrounding_word(buffer_position) + buffer_snapshot.surrounding_word(buffer_position, false) { let word_to_exclude = buffer_snapshot .text_for_range(word_range.clone()) @@ -6605,8 +6605,8 @@ impl Editor { } let snapshot = cursor_buffer.read(cx).snapshot(); - let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position); - let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position); + let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position, false); + let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position, false); if start_word_range != end_word_range { self.document_highlights_task.take(); self.clear_background_highlights::(cx); @@ -22137,7 +22137,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); + let (range, kind) = snapshot.surrounding_word(position, false); if kind != Some(CharKind::Word) { return None; } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ae0184b22a..59aa63ff38 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3364,13 +3364,19 @@ impl BufferSnapshot { /// Returns a tuple of the range and character kind of the word /// surrounding the given position. - pub fn surrounding_word(&self, start: T) -> (Range, Option) { + pub fn surrounding_word( + &self, + start: T, + for_completion: bool, + ) -> (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); + let classifier = self + .char_classifier_at(start) + .for_completion(for_completion); let word_kind = cmp::max( prev_chars.peek().copied().map(|c| classifier.kind(c)), next_chars.peek().copied().map(|c| classifier.kind(c)), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4538dc4cda..a2f6de44c9 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -350,7 +350,7 @@ impl LspCommand for PrepareRename { } Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { let snapshot = buffer.snapshot(); - let (range, _) = snapshot.surrounding_word(self.position); + let (range, _) = snapshot.surrounding_word(self.position, false); let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); Ok(PrepareRenameResponse::Success(range)) } @@ -2297,7 +2297,7 @@ 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); + let (range, kind) = snapshot.surrounding_word(offset, true); let range = if kind == Some(CharKind::Word) { range } else {