snippets: Fix snippets for PHP and ERB languages (#27718)
Closes #21541 Closes #22726 This should fix snippets in languages, like PHP, that are based on the HTML syntax layer. To be honest, I don't totally get where HTML comes into it, but the issues outlined in #21541 and #22726 both boil down to "Zed only shows me HTML snippets in PHP/ERB files; I expected to see PHP/ERB snippets". This solution is based on the comments between @mrnugget and @osiewicz in #22726: resolve/combine snippets for all language layers at the given position, whereas current behavior is to resolve snippets only for the `.last()` language layer at the given position. - add `Buffer:languages_at()` (note the plural) - update `snippet_completions()` in `editor.rs` to loop over each language, gathering snippets as it goes - the primary logic for resolving snippets within a single language has not changed ### Verifying this change I couldn't find tests related to snippet and currently active languages (CI may show them to me 😆 ) but I can add some if desired and w/ perhaps a little coaching or prompting about another test to look to for inspiration. I have confirmed that this works for PHP, but I have not checked ERB because I'm not familiar with it or set up for it. To check this manually: 1. install the PHP extension 2. install at least 1 snippet for each of html, php and phpdoc. If you don't have any, these should work: ```sh # BEWARE these will clobber existing snippets! echo '{"dddd":{"body":"hello from phpdoc"}}' > ~/.config/zed/snippets/phpdoc.json echo '{"pppp":{"body":"hello from PHP"}}' > ~/.config/zed/snippets/php.json echo '{"hhhh":{"body":"hello from HTML"}}' > ~/.config/zed/snippets/html.json ``` 3. open any PHP file. If you don't have one, here's one that should work: ```php <?php /** * */ function function_name() { } ``` 4. Place your cursor in a PHPdoc comment (eg after the `/**` on line 3) - you should be able to use the `dddd`, `pppp` and `hhhh` snippets; on `main`, only the `dddd` snippet works here 5. Move your cursor to a non-comment PHP area (eg after the `{` on line 7) - you should be able to use the `pppp` and `hhhh` snippets, but not `dddd`; on `main`, only `hhhh` works here ### Performance This adds 2 separate (not nested) loops to `snippet_completions()`, each of which will iterate over the active language scopes at the given location. I have not looked into the specifics of how many layers most languages have, but I suspect that *most* users will see identical performance as before because there will only be 1 scope active most of the time. In some cases, though (eg PHP, ERB, maybe template strings in JS), the editor will be looping over more layers, possibly many in some deeply injected/embedded cases (I'm thinking of a regex template string in a JS heredoc string in a PHP script in an HTML file). I don't expect this to be an issue – nor has it been in my usage and testing – but performance of snippets could be affected in pathological cases. ### Alternate solutions Instead of resolving snippets for *all* layers, we could just change how we pick which language to resolve. Instead of always using `.last()`, perhaps we could do something more clever. This feels like it could be tricky and potentially error prone, though. Release Notes: - Snippets are now resolved for all languages active at the cursor location. - Fixed snippets in PHP, ERB and other languages whose syntax layers are based on HTML
This commit is contained in:
parent
730f2e7083
commit
97a9a5de10
2 changed files with 141 additions and 116 deletions
|
@ -18853,24 +18853,37 @@ fn snippet_completions(
|
|||
buffer_position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
let language = buffer.read(cx).language_at(buffer_position);
|
||||
let language_name = language.as_ref().map(|language| language.lsp_id());
|
||||
let languages = buffer.read(cx).languages_at(buffer_position);
|
||||
let snippet_store = project.snippets().read(cx);
|
||||
let snippets = snippet_store.snippets_for(language_name, cx);
|
||||
|
||||
let scopes: Vec<_> = languages
|
||||
.iter()
|
||||
.filter_map(|language| {
|
||||
let language_name = language.lsp_id();
|
||||
let snippets = snippet_store.snippets_for(Some(language_name), cx);
|
||||
|
||||
if snippets.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((language.default_scope(), snippets))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if scopes.is_empty() {
|
||||
return Task::ready(Ok(vec![]));
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).text_snapshot();
|
||||
let chars: String = snapshot
|
||||
.reversed_chars_for_range(text::Anchor::MIN..buffer_position)
|
||||
.collect();
|
||||
|
||||
let scope = language.map(|language| language.default_scope());
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let classifier = CharClassifier::new(scope).for_completion(true);
|
||||
let mut all_results: Vec<Completion> = Vec::new();
|
||||
for (scope, snippets) in scopes.into_iter() {
|
||||
let classifier = CharClassifier::new(Some(scope)).for_completion(true);
|
||||
let mut last_word = chars
|
||||
.chars()
|
||||
.take_while(|c| classifier.is_word(*c))
|
||||
|
@ -18905,7 +18918,7 @@ fn snippet_completions(
|
|||
last_word.chars().any(|c| c.is_uppercase()),
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
executor.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -18928,8 +18941,8 @@ fn snippet_completions(
|
|||
.map(|m| m.string)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let result: Vec<Completion> = snippets
|
||||
.into_iter()
|
||||
let mut result: Vec<Completion> = snippets
|
||||
.iter()
|
||||
.filter_map(|snippet| {
|
||||
let matching_prefix = snippet
|
||||
.prefix
|
||||
|
@ -18979,17 +18992,19 @@ fn snippet_completions(
|
|||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: snippet
|
||||
.description
|
||||
.clone()
|
||||
.map(|description| CompletionDocumentation::SingleLine(description.into())),
|
||||
documentation: snippet.description.clone().map(|description| {
|
||||
CompletionDocumentation::SingleLine(description.into())
|
||||
}),
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
all_results.append(&mut result);
|
||||
}
|
||||
|
||||
Ok(all_results)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1373,6 +1373,16 @@ impl Buffer {
|
|||
.or_else(|| self.language.clone())
|
||||
}
|
||||
|
||||
/// Returns each [`Language`] for the active syntax layers at the given location.
|
||||
pub fn languages_at<D: ToOffset>(&self, position: D) -> Vec<Arc<Language>> {
|
||||
let offset = position.to_offset(self);
|
||||
self.syntax_map
|
||||
.lock()
|
||||
.layers_for_range(offset..offset, &self.text, false)
|
||||
.map(|info| info.language.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// An integer version number that accounts for all updates besides
|
||||
/// the buffer's text itself (which is versioned via a version vector).
|
||||
pub fn non_text_state_update_count(&self) -> usize {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue