Fix completion labels becoming overly large due to LSP completion items with newlines (#23407)
Reworks https://github.com/zed-industries/zed/pull/23030 and https://github.com/zed-industries/zed/pull/15087 Closes https://github.com/zed-industries/zed/issues/23352 Closes https://github.com/zed-industries/zed/issues/23310 Zed's completion items use `label` from LSP completion items as a base to show in the list:d290da7dac/crates/project/src/lsp_store.rs (L4371-L4374)
Besides that, certain language plugins append `detail` or `label_details.description` as a suffix:d290da7dac/crates/languages/src/vtsls.rs (L178-L188)
Either of these 3 properties may return `\n` (or multiple) in it, spoiling Zed's completion menu, which uses `UniformList` to render those items: a uniform list uses common, minimum possible height for each element, and `\n` bloats that overly. Good approach would be to use something else: https://github.com/zed-industries/zed/issues/21403 but that has its own drawbacks and relatively hard to use instead (?). We could follow VSCode's approach and move away all but `label` from `CodeLabel.text` to the side, where the documentation is, but that does not solve the issue with `details` having newlines. So, for now, sanitize all labels and remove any newlines from them. If newlines are found, also replace whitespace sequences if there's more than 1 in a row. Later, this approach can be improved similarly to how Helix and Zed's inline completions do: rendering a "ghost" text, showing the completion's edit applied to the editor. Release Notes: - Fixed completion labels becoming overly large due to LSP completion items with newlines
This commit is contained in:
parent
94189e1784
commit
75c5344754
5 changed files with 338 additions and 15 deletions
|
@ -4357,7 +4357,7 @@ impl LspStore {
|
|||
|
||||
// NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213
|
||||
// So we have to update the label here anyway...
|
||||
let new_label = match snapshot.language() {
|
||||
let mut new_label = match snapshot.language() {
|
||||
Some(language) => {
|
||||
adapter
|
||||
.labels_for_completions(&[completion_item.clone()], language)
|
||||
|
@ -4373,6 +4373,7 @@ impl LspStore {
|
|||
completion_item.filter_text.as_deref(),
|
||||
)
|
||||
});
|
||||
ensure_uniform_list_compatible_label(&mut new_label);
|
||||
|
||||
let mut completions = completions.borrow_mut();
|
||||
let completion = &mut completions[completion_index];
|
||||
|
@ -8014,15 +8015,18 @@ async fn populate_labels_for_completions(
|
|||
None
|
||||
};
|
||||
|
||||
let mut label = label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(
|
||||
lsp_completion.label.clone(),
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
});
|
||||
ensure_uniform_list_compatible_label(&mut label);
|
||||
|
||||
completions.push(Completion {
|
||||
old_range: completion.old_range,
|
||||
new_text: completion.new_text,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(
|
||||
lsp_completion.label.clone(),
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
label,
|
||||
server_id: completion.server_id,
|
||||
documentation,
|
||||
lsp_completion,
|
||||
|
@ -8733,6 +8737,70 @@ fn include_text(server: &lsp::LanguageServer) -> Option<bool> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Completion items are displayed in a `UniformList`.
|
||||
/// Usually, those items are single-line strings, but in LSP responses,
|
||||
/// completion items `label`, `detail` and `label_details.description` may contain newlines or long spaces.
|
||||
/// Many language plugins construct these items by joining these parts together, and we may fall back to `CodeLabel::plain` that uses `label`.
|
||||
/// All that may lead to a newline being inserted into resulting `CodeLabel.text`, which will force `UniformList` to bloat each entry to occupy more space,
|
||||
/// breaking the completions menu presentation.
|
||||
///
|
||||
/// Sanitize the text to ensure there are no newlines, or, if there are some, remove them and also remove long space sequences if there were newlines.
|
||||
fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) {
|
||||
let mut new_text = String::with_capacity(label.text.len());
|
||||
let mut offset_map = vec![0; label.text.len() + 1];
|
||||
let mut last_char_was_space = false;
|
||||
let mut new_idx = 0;
|
||||
let mut chars = label.text.char_indices().fuse();
|
||||
let mut newlines_removed = false;
|
||||
|
||||
while let Some((idx, c)) = chars.next() {
|
||||
offset_map[idx] = new_idx;
|
||||
|
||||
match c {
|
||||
'\n' if last_char_was_space => {
|
||||
newlines_removed = true;
|
||||
}
|
||||
'\t' | ' ' if last_char_was_space => {}
|
||||
'\n' if !last_char_was_space => {
|
||||
new_text.push(' ');
|
||||
new_idx += 1;
|
||||
last_char_was_space = true;
|
||||
newlines_removed = true;
|
||||
}
|
||||
' ' | '\t' => {
|
||||
new_text.push(' ');
|
||||
new_idx += 1;
|
||||
last_char_was_space = true;
|
||||
}
|
||||
_ => {
|
||||
new_text.push(c);
|
||||
new_idx += 1;
|
||||
last_char_was_space = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset_map[label.text.len()] = new_idx;
|
||||
|
||||
// Only modify the label if newlines were removed.
|
||||
if !newlines_removed {
|
||||
return;
|
||||
}
|
||||
|
||||
for (range, _) in &mut label.runs {
|
||||
range.start = offset_map[range.start];
|
||||
range.end = offset_map[range.end];
|
||||
}
|
||||
|
||||
if label.filter_range == (0..label.text.len()) {
|
||||
label.filter_range = 0..new_text.len();
|
||||
} else {
|
||||
label.filter_range.start = offset_map[label.filter_range.start];
|
||||
label.filter_range.end = offset_map[label.filter_range.end];
|
||||
}
|
||||
|
||||
label.text = new_text;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_glob_literal_prefix() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue