Compare commits
10 Commits
lua-run-cl
...
v0.170.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4b2e556bf | ||
|
|
365398a7f1 | ||
|
|
058bdae413 | ||
|
|
a64b75f214 | ||
|
|
c251987d07 | ||
|
|
8a90fa1ff1 | ||
|
|
eadddc6509 | ||
|
|
ea02a95cf0 | ||
|
|
fba2828877 | ||
|
|
ce3752db98 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -16102,7 +16102,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.170.0"
|
||||
version = "0.170.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
"bindings": {
|
||||
"alt-tab": "editor::NextInlineCompletion",
|
||||
"alt-shift-tab": "editor::PreviousInlineCompletion",
|
||||
"ctrl-right": "editor::AcceptPartialInlineCompletion"
|
||||
"ctrl-cmd-right": "editor::AcceptPartialInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -77,8 +77,8 @@ impl Model {
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
|
||||
Model::Claude3Opus => "claude-3-opus-latest",
|
||||
Model::Claude3Sonnet => "claude-3-sonnet-latest",
|
||||
Model::Claude3Haiku => "claude-3-haiku-latest",
|
||||
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
|
||||
Model::Claude3Haiku => "claude-3-haiku-20240307",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1204,6 +1204,7 @@ impl InlineAssistant {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
|
||||
@@ -1276,6 +1276,7 @@ impl InlineAssistant {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
|
||||
@@ -4720,8 +4720,19 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit(edits) => {
|
||||
if edits.len() == 1 && edits[0].0.start == edits[0].0.end {
|
||||
let text = edits[0].1.as_str();
|
||||
// Find an insertion that starts at the cursor position.
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor_offset = self.selections.newest::<usize>(cx).head();
|
||||
let insertion = edits.iter().find_map(|(range, text)| {
|
||||
let range = range.to_offset(&snapshot);
|
||||
if range.is_empty() && range.start == cursor_offset {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(text) = insertion {
|
||||
let mut partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
@@ -4744,6 +4755,8 @@ impl Editor {
|
||||
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.accept_inline_completion(&Default::default(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8437,6 +8437,247 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.ts": "a",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let typescript_language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..LanguageMatcher::default()
|
||||
},
|
||||
line_comments: vec!["// ".into()],
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
|
||||
));
|
||||
language_registry.add(typescript_language.clone());
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"TypeScript",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
// Emulate vtsls label generation
|
||||
label_for_completion: Some(Box::new(|item, _| {
|
||||
let text = if let Some(description) = item
|
||||
.label_details
|
||||
.as_ref()
|
||||
.and_then(|label_details| label_details.description.as_ref())
|
||||
{
|
||||
format!("{} {}", item.label, description)
|
||||
} else if let Some(detail) = &item.detail {
|
||||
format!("{} {}", item.label, detail)
|
||||
} else {
|
||||
item.label.clone()
|
||||
};
|
||||
let len = text.len();
|
||||
Some(language::CodeLabel {
|
||||
text,
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..len,
|
||||
})
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let worktree_id = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/a/main.ts", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.ts"), None, true, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
|
||||
let multiline_label_2 = "a\nb\nc\n";
|
||||
let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
|
||||
let multiline_description = "d\ne\nf\n";
|
||||
let multiline_detail_2 = "g\nh\ni\n";
|
||||
|
||||
let mut completion_handle =
|
||||
fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: multiline_label.to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
},
|
||||
new_text: "new_text_1".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "single line label 1".to_string(),
|
||||
detail: Some(multiline_detail.to_string()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
},
|
||||
new_text: "new_text_2".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "single line label 2".to_string(),
|
||||
label_details: Some(lsp::CompletionItemLabelDetails {
|
||||
description: Some(multiline_description.to_string()),
|
||||
detail: None,
|
||||
}),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
},
|
||||
new_text: "new_text_2".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: multiline_label_2.to_string(),
|
||||
detail: Some(multiline_detail_2.to_string()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
},
|
||||
new_text: "new_text_3".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "Label with many spaces and \t but without newlines".to_string(),
|
||||
detail: Some(
|
||||
"Details with many spaces and \t but without newlines".to_string(),
|
||||
),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
character: params.text_document_position.position.character,
|
||||
},
|
||||
},
|
||||
new_text: "new_text_4".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.focus(cx);
|
||||
editor.move_to_end(&MoveToEnd, cx);
|
||||
editor.handle_input(".", cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
completion_handle.next().await.unwrap();
|
||||
|
||||
editor.update(cx, |editor, _| {
|
||||
assert!(editor.context_menu_visible());
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
let completion_labels = menu
|
||||
.completions
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|c| c.label.text.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completion_labels,
|
||||
&[
|
||||
"StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
|
||||
"single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
|
||||
"single line label 2 d e f ",
|
||||
"a b c g h i ",
|
||||
"Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
|
||||
],
|
||||
"Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
|
||||
);
|
||||
|
||||
for completion in menu
|
||||
.completions
|
||||
.borrow()
|
||||
.iter() {
|
||||
assert_eq!(
|
||||
completion.label.filter_range,
|
||||
0..completion.label.text.len(),
|
||||
"Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -1333,11 +1333,15 @@ impl EditorElement {
|
||||
total_text_units
|
||||
.horizontal
|
||||
.zip(track_bounds.horizontal)
|
||||
.map(|(total_text_units_x, track_bounds_x)| {
|
||||
.and_then(|(total_text_units_x, track_bounds_x)| {
|
||||
if text_units_per_page.horizontal >= total_text_units_x {
|
||||
return None;
|
||||
}
|
||||
|
||||
let thumb_percent =
|
||||
(text_units_per_page.horizontal / total_text_units_x).min(1.);
|
||||
|
||||
track_bounds_x.size.width * thumb_percent
|
||||
Some(track_bounds_x.size.width * thumb_percent)
|
||||
}),
|
||||
total_text_units.vertical.zip(track_bounds.vertical).map(
|
||||
|(total_text_units_y, track_bounds_y)| {
|
||||
|
||||
@@ -763,6 +763,14 @@ pub struct FakeLspAdapter {
|
||||
|
||||
pub capabilities: lsp::ServerCapabilities,
|
||||
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||
pub label_for_completion: Option<
|
||||
Box<
|
||||
dyn 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(&lsp::CompletionItem, &Arc<Language>) -> Option<CodeLabel>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
/// Configuration of handling bracket pairs for a given language.
|
||||
@@ -1778,6 +1786,7 @@ impl Default for FakeLspAdapter {
|
||||
arguments: vec![],
|
||||
env: Default::default(),
|
||||
},
|
||||
label_for_completion: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1849,6 +1858,15 @@ impl LspAdapter for FakeLspAdapter {
|
||||
) -> Result<Option<Value>> {
|
||||
Ok(self.initialization_options.clone())
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let label_for_completion = self.label_for_completion.as_ref()?;
|
||||
label_for_completion(item, language)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
|
||||
|
||||
@@ -80,7 +80,7 @@ impl CloudModel {
|
||||
| open_ai::Model::FourOmni
|
||||
| open_ai::Model::FourOmniMini
|
||||
| open_ai::Model::O1Mini
|
||||
| open_ai::Model::O1Preview
|
||||
| open_ai::Model::O1
|
||||
| open_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
|
||||
@@ -362,9 +362,7 @@ pub fn count_open_ai_tokens(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match model {
|
||||
open_ai::Model::Custom { .. }
|
||||
| open_ai::Model::O1Mini
|
||||
| open_ai::Model::O1Preview => {
|
||||
open_ai::Model::Custom { .. } | open_ai::Model::O1Mini | open_ai::Model::O1 => {
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
|
||||
}
|
||||
_ => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
|
||||
|
||||
@@ -212,16 +212,14 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let one_line = |s: &str| s.replace(" ", "").replace('\n', " ");
|
||||
|
||||
let text = if let Some(description) = item
|
||||
.label_details
|
||||
.as_ref()
|
||||
.and_then(|label_details| label_details.description.as_ref())
|
||||
{
|
||||
format!("{} {}", item.label, one_line(description))
|
||||
format!("{} {}", item.label, description)
|
||||
} else if let Some(detail) = &item.detail {
|
||||
format!("{} {}", item.label, one_line(detail))
|
||||
format!("{} {}", item.label, detail)
|
||||
} else {
|
||||
item.label.clone()
|
||||
};
|
||||
|
||||
@@ -175,16 +175,14 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let one_line = |s: &str| s.replace(" ", "").replace('\n', " ");
|
||||
|
||||
let text = if let Some(description) = item
|
||||
.label_details
|
||||
.as_ref()
|
||||
.and_then(|label_details| label_details.description.as_ref())
|
||||
{
|
||||
format!("{} {}", item.label, one_line(description))
|
||||
format!("{} {}", item.label, description)
|
||||
} else if let Some(detail) = &item.detail {
|
||||
format!("{} {}", item.label, one_line(detail))
|
||||
format!("{} {}", item.label, detail)
|
||||
} else {
|
||||
item.label.clone()
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ fn get_max_tokens(name: &str) -> usize {
|
||||
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
|
||||
| "dolphin-mixtral" => 32768,
|
||||
"llama3.1" | "phi3" | "phi3.5" | "phi4" | "command-r" | "deepseek-coder-v2"
|
||||
| "yi-coder" | "llama3.2" => 128000,
|
||||
| "deepseek-r1" | "yi-coder" | "llama3.2" => 128000,
|
||||
_ => DEFAULT_TOKENS,
|
||||
}
|
||||
.clamp(1, MAXIMUM_TOKENS)
|
||||
|
||||
@@ -72,8 +72,8 @@ pub enum Model {
|
||||
FourOmni,
|
||||
#[serde(rename = "gpt-4o-mini", alias = "gpt-4o-mini")]
|
||||
FourOmniMini,
|
||||
#[serde(rename = "o1-preview", alias = "o1-preview")]
|
||||
O1Preview,
|
||||
#[serde(rename = "o1", alias = "o1-preview")]
|
||||
O1,
|
||||
#[serde(rename = "o1-mini", alias = "o1-mini")]
|
||||
O1Mini,
|
||||
|
||||
@@ -96,7 +96,7 @@ impl Model {
|
||||
"gpt-4-turbo-preview" => Ok(Self::FourTurbo),
|
||||
"gpt-4o" => Ok(Self::FourOmni),
|
||||
"gpt-4o-mini" => Ok(Self::FourOmniMini),
|
||||
"o1-preview" => Ok(Self::O1Preview),
|
||||
"o1" => Ok(Self::O1),
|
||||
"o1-mini" => Ok(Self::O1Mini),
|
||||
_ => Err(anyhow!("invalid model id")),
|
||||
}
|
||||
@@ -109,7 +109,7 @@ impl Model {
|
||||
Self::FourTurbo => "gpt-4-turbo",
|
||||
Self::FourOmni => "gpt-4o",
|
||||
Self::FourOmniMini => "gpt-4o-mini",
|
||||
Self::O1Preview => "o1-preview",
|
||||
Self::O1 => "o1",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
@@ -122,7 +122,7 @@ impl Model {
|
||||
Self::FourTurbo => "gpt-4-turbo",
|
||||
Self::FourOmni => "gpt-4o",
|
||||
Self::FourOmniMini => "gpt-4o-mini",
|
||||
Self::O1Preview => "o1-preview",
|
||||
Self::O1 => "o1",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
@@ -137,7 +137,7 @@ impl Model {
|
||||
Self::FourTurbo => 128000,
|
||||
Self::FourOmni => 128000,
|
||||
Self::FourOmniMini => 128000,
|
||||
Self::O1Preview => 128000,
|
||||
Self::O1 => 128000,
|
||||
Self::O1Mini => 128000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
@@ -475,7 +475,7 @@ pub async fn stream_completion(
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
) -> Result<BoxStream<'static, Result<ResponseStreamEvent>>> {
|
||||
if request.model == "o1-preview" || request.model == "o1-mini" {
|
||||
if request.model.starts_with("o1") {
|
||||
let response = complete(client, api_url, api_key, request).await;
|
||||
let response_stream_event = response.map(adapt_response_to_stream);
|
||||
return Ok(stream::once(future::ready(response_stream_event)).boxed());
|
||||
|
||||
@@ -4347,7 +4347,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)
|
||||
@@ -4363,6 +4363,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];
|
||||
@@ -7970,15 +7971,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,
|
||||
@@ -8707,6 +8711,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() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.170.0"
|
||||
version = "0.170.1"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
Reference in New Issue
Block a user