Compare commits

...

18 Commits

Author SHA1 Message Date
Zed Bot
8fd8facede Bump to 0.167.2 for @maxdeviant 2025-01-06 19:01:38 +00:00
gcp-cherry-pick-bot[bot]
65954bcdc0 zed: Add timeouts for feature flag resolution in workspace panel initialization (cherry-pick #22715) (#22722)
Cherry-picked zed: Add timeouts for feature flag resolution in workspace
panel initialization (#22715)

This PR adds timeouts when resolving feature flags during workspace
panel initialization so that we don't block indefinitely if Zed is not
connected to the internet.

Right now we wait for 5 seconds, but this value was chosen arbitrarily.

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-06 13:05:18 -05:00
gcp-cherry-pick-bot[bot]
f3774037a2 Return back Rust completion details (cherry-pick #22648) (#22650)
Cherry-picked Return back Rust completion details (#22648)

Closes https://github.com/zed-industries/zed/issues/22642

In Zed, Rust's label generators expected the details to come in ` (use
std.foo.Bar)` form, but recently, r-a started to send these details
without the leading whitespace which broke the code generation.

The PR makes LSP results parsing more lenient to work with both details'
forms.

Release Notes:

- Fixed Rust completion labels not showing the imports

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-01-04 13:49:14 +02:00
gcp-cherry-pick-bot[bot]
e12daa7d66 language_model_selector: Refresh the models when the providers change (cherry-pick #22624) (#22627)
Cherry-picked language_model_selector: Refresh the models when the
providers change (#22624)

This PR fixes an issue introduced in #21939 where the list of models in
the language model selector could be outdated.

Since we're no longer recreating the picker each render, we now need to
make sure we are updating the list of models accordingly when there are
changes to the language model providers.

I noticed it specifically in Assistant1.

Release Notes:

- Fixed a staleness issue with the language model selector.

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-03 15:30:45 -05:00
Kirill Bulatov
3a33fe5a89 Fix tooltips too eager to disappear when there's a gap between the tooltip source and the tooltip itself (#22583)
Follow-up of https://github.com/zed-industries/zed/pull/22548

Release Notes:

- N/A

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-01-03 11:12:40 +02:00
gcp-cherry-pick-bot[bot]
342b41acb8 Remove stuck tooltips (cherry-pick #22548) (#22549)
Cherry-picked Remove stuck tooltips (#22548)

Closes https://github.com/zed-industries/zed/issues/21657

Follow-up of https://github.com/zed-industries/zed/pull/22488
Previous PR broke git blame tooltips, which are expected to be open when
hovered, even if the mouse cursor is moved away from the actual blame
entry that caused the tooltip to appear.

Current version moves the invalidation logic into `prepaint_tooltip`,
where the new data about the tooltip origin is used to ensure we
invalidate only tooltips that have no mouse cursor in either origin
bounds or tooltip bounds (if it's hoverable).


Release Notes:

- Fixed tooltips getting stuck

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-01-01 21:07:42 +02:00
Peter Tripp
a2264c3b75 v0.167.x stable 2025-01-01 12:23:33 -05:00
gcp-cherry-pick-bot[bot]
78e1512c12 Revert "Invalidate tooltips when mouse leaves element's hitbox (#22488)" (cherry-pick #22542) (#22544)
Cherry-picked Revert "Invalidate tooltips when mouse leaves element's
hitbox (#22488)" (#22542)

This reverts commit 344284e013.

That change broke git blame tooltips, as Zed should also show tooltips
which are hovered, even though the mouse had left the origin element's
bounds.

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-01-01 19:13:23 +02:00
gcp-cherry-pick-bot[bot]
3ca88f3a56 Deduplicate edits from WorkspaceEdit LSP responses (cherry-pick #22512) (#22514)
Cherry-picked Deduplicate edits from WorkspaceEdit LSP responses
(#22512)

Closes https://github.com/zed-industries/zed/issues/21515

Release Notes:

- Fixed zls renames applying duplicate edits

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-12-31 03:08:49 +02:00
gcp-cherry-pick-bot[bot]
68fe03ae0e Ensure zls is used for Zig as a primary language server (cherry-pick #22511) (#22513)
Cherry-picked Ensure zls is used for Zig as a primary language server
(#22511)

Part of https://github.com/zed-industries/zed/issues/22415

I've noticed that I cannot work with any Zig projects, as there were no
"go to definition", formatting and inlay hints.

After debugging, I've discovered that `typos` was registered as a first
language server, becoming the "primary" one for Zig.
That one does not have any proper capabilities, hence all corresponding
LSP requests were no-op.

While this solution is not ideal (I wonder, how many other set-ups are
broken due to the same thing?), we'd better fix things for now this way
at least.

Release Notes:

- Fixed `zls` not working properly when `typos` extension is installed

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-12-31 03:01:28 +02:00
gcp-cherry-pick-bot[bot]
b8ad3a56f7 Invalidate tooltips when mouse leaves element's hitbox (cherry-pick #22488) (#22489)
Cherry-picked Invalidate tooltips when mouse leaves element's hitbox
(#22488)

Closes https://github.com/zed-industries/zed/issues/21657

In case of the task rerun button tooltip from



f6dabadaf7/crates/terminal_view/src/terminal_view.rs (L1051-L1070)

, the actual button element is not styled as invisible, only its parent.
Zed won't render such element since it's parent is hidden, but will
consider it "visible" all the time its `paint` is called, spawning a
task with the delay, that will create the tooltip:



f6dabadaf7/crates/gpui/src/elements/div.rs (L1949-L1959)

When the parent is hidden, the child won't be painted anymore, and no
mouse listeners will be able to detect this fact and hide the tooltip.

Hence, check such cases separately, during `prepaint`, and invalidate
the tooltips that are not valid anymore.
We cannot use `hitbox.is_hovered(cx)` as it's not really hovered during
prepaint, so a mouse position check is used instead.

Release Notes:

- Fixed tooltips getting stuck

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-12-29 22:39:19 +02:00
Kirill Bulatov
600923b901 Fix Project strong reference leaks (#22470)
Closes https://github.com/zed-industries/zed/issues/21906

* After https://github.com/zed-industries/zed/pull/21238,
`TerminalPanel` and `Project` strong references were moved into
`Pane`-related closures, creating a cycle, that did not allow
registering project release and shutting down corresponding language
servers

* After https://github.com/zed-industries/zed/pull/22329, a special
`Editor` was created with a strong reference to the `Project` which
seemed to do nothing bad in general, but when a working Zed was running
a Zed Dev build, had the same issue with preventing language servers
from shutting down.

The latter is very odd, and seems quite dangerous, as any arbitrary
`Editor` with `Project` in it may do the same, yet it seems that we did
not store them before the way git panel does.

I have tried creating a test, yet seems that we need to initialize a lot
of Zed for it which I failed — all my attempts resulted in a single
language server being present in the `Project`'s statuses.

Release Notes:

- Fixed language servers not being released between project reopens
2024-12-28 19:46:44 +02:00
Marcel Kersten
36018ff2dc copilot: Update Copilot Chat to o1 GA model version (#22376)
Closes #22375

Release Notes:

- Fixed model version of o1 in GitHub Copilot Chat

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-27 13:31:16 -05:00
Kirill Bulatov
ab02944e48 Resolve completion items once exactly (#22448)
Closes https://github.com/zed-industries/zed/issues/19214
Closes https://github.com/zed-industries/zed/pull/22443

Adds `resolved` property into Zed completion item data, to ensure we
resolve every completion item exactly once.

There are 2 paths for singplayer Zed, and corresponding 2 analogues for
multi player code, where resolve may happen:
* completions menu display & selection, that ends up using
`resolve_completions` in `lsp_store.rs`
* applying a completion menu entry, that ends up using
`apply_additional_edits_for_completion` in `lsp_store.rs`

Now, all local counterparts check `enabled` field before resolving and
set it to true afterwards, and reuse the same `resolve_completion_local`
method for resolving the items.

A logic for re-generating docs and item labels was moved out from the
`resolve_completion_local` method into a separate method, as
`apply_additional_edits_for_completion` does not need that, but needs
the rest of the logic for resolving.
During the extraction, I've noted that multiplayer clients are not
getting the item labels, regenerated after the resolve — as the Zed
protocol-based flow is not the exact copy of the local resolving.
To improve that, `resolve_completion_remote` needs to be adjusted, but
this change is omitted to avoid bloating the PR.

Release Notes:

- Fixed autocomplete inserting multiple imports
2024-12-27 18:57:05 +02:00
Piotr Osiewicz
b9d1431e17 terminal: Clear output after venv is activated (#22256)
The command used to activate the venv can still be accessed/scrolled to
if needed.

Release Notes:

- The Python virtual environment activation command is no longer shown
in the terminal output by default.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-19 13:17:08 -05:00
Peter Tripp
d60635b84a zed 0.167.1 2024-12-19 13:05:34 -05:00
Thorsten Ball
0e288bf358 Fix project environment not working correctly with multiple worktrees (#22246)
Fixes https://github.com/zed-industries/zed/issues/21972

This fixes two bugs:

**Bug 1**: this bug caused us to only ever load a single environment in
a multi-worktree project, thanks to this line:

```rust
if let Some(task) = self.get_environment_task.as_ref()
```

We'd only ever run a single task per project, which is wrong.

What does code does is to cache the tasks per `worktree_id`, which means
we don't even need to cache the environments again, since we can just
cache the `Shared<Task<...>>`.

**Bug 2**: we assumed that every `worktree_abs_path` is a directory,
which lead to `Failed to run direnv` log messages when opening a project
that had a worktree with a single file open (easy to reproduce: open a
normal project, open your settings, close Zed, reopen it — the settings
faile caused environments to not load)

It's fixed by checking whether the `worktree_abs_path` is an absolute
directory. Since this is always running locally, it's fine to use
`smol::fs` here instead of using our `Fs`.

Release Notes:

- Fixed shell environments not being loaded properly to be used by
language servers and terminals in case a project had multiple worktrees.
- Fixed `Failed to run direnv` messages showing up in case Zed restored
a window that contained a worktree with a single file.
https://github.com/zed-industries/zed/issues/21972
2024-12-19 13:05:03 -05:00
Peter Tripp
a8776f8a5c v0.167.x preview 2024-12-18 11:41:34 -05:00
25 changed files with 710 additions and 366 deletions

2
Cargo.lock generated
View File

@@ -16000,7 +16000,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.167.0"
version = "0.167.2"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -1101,6 +1101,9 @@
"prettier": {
"allowed": true
}
},
"Zig": {
"language_servers": ["zls", "..."]
}
},
// Different settings for specific language models.

View File

@@ -149,6 +149,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
resolved: true,
})
})
.collect()
@@ -242,6 +243,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
resolved: true,
}
})
.collect())
@@ -330,16 +332,6 @@ impl CompletionProvider for SlashCommandCompletionProvider {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,

View File

@@ -79,16 +79,6 @@ impl CompletionProvider for MessageEditorCompletionProvider {
Task::ready(Ok(false))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Model<Buffer>,
_completion: Completion,
_push_to_history: bool,
_cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
_buffer: &Model<Buffer>,
@@ -319,6 +309,7 @@ impl MessageEditor {
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
confirm: None,
resolved: true,
}
})
.collect()

View File

@@ -34,9 +34,9 @@ pub enum Model {
Gpt4,
#[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")]
Gpt3_5Turbo,
#[serde(alias = "o1-preview", rename = "o1-preview-2024-09-12")]
#[serde(alias = "o1-preview", rename = "o1")]
O1Preview,
#[serde(alias = "o1-mini", rename = "o1-mini-2024-09-12")]
#[serde(alias = "o1-mini", rename = "o1-mini")]
O1Mini,
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
Claude3_5Sonnet,

View File

@@ -217,6 +217,7 @@ impl CompletionsMenu {
documentation: None,
lsp_completion: Default::default(),
confirm: None,
resolved: true,
})
.collect();

View File

@@ -3828,8 +3828,11 @@ impl Editor {
};
let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.borrow_mut();
let completion = completions.get(mat.candidate_id)?;
let completion = completions_menu
.completions
.borrow()
.get(mat.candidate_id)?
.clone();
cx.stop_propagation();
let snippet;
@@ -3973,9 +3976,11 @@ impl Editor {
}
let provider = self.completion_provider.as_ref()?;
drop(completion);
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
completion.clone(),
completions_menu.completions.clone(),
mat.candidate_id,
true,
cx,
);
@@ -5098,7 +5103,7 @@ impl Editor {
}))
}
#[cfg(feature = "test-support")]
#[cfg(any(feature = "test-support", test))]
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.borrow()
@@ -13433,11 +13438,14 @@ pub trait CompletionProvider {
fn apply_additional_edits_for_completion(
&self,
buffer: Model<Buffer>,
completion: Completion,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>>;
_buffer: Model<Buffer>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_completion_index: usize,
_push_to_history: bool,
_cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
@@ -13596,6 +13604,7 @@ fn snippet_completions(
Some(Completion {
old_range: range,
new_text: snippet.body.clone(),
resolved: false,
label: CodeLabel {
text: matching_prefix.clone(),
runs: vec![],
@@ -13661,19 +13670,30 @@ impl CompletionProvider for Model<Project> {
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
self.update(cx, |project, cx| {
project.resolve_completions(buffer, completion_indices, completions, cx)
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.resolve_completions(buffer, completion_indices, completions, cx)
})
})
}
fn apply_additional_edits_for_completion(
&self,
buffer: Model<Buffer>,
completion: Completion,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
self.update(cx, |project, cx| {
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.apply_additional_edits_for_completion(
buffer,
completions,
completion_index,
push_to_history,
cx,
)
})
})
}

View File

@@ -8398,7 +8398,6 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit
"});
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
update_test_language_settings(&mut cx, |settings| {
@@ -10694,10 +10693,14 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
..lsp::CompletionItem::default()
};
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let item1 = item1.clone();
cx.handle_request::<lsp::request::Completion, _, _>({
let item1 = item1.clone();
let item2 = item2.clone();
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
move |_, _, _| {
let item1 = item1.clone();
let item2 = item2.clone();
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
}
})
.next()
.await;
@@ -10724,43 +10727,41 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
}
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem {
label: "method id()".to_string(),
filter_text: Some("id".to_string()),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
})
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
let item1 = item1.clone();
move |_, item_to_resolve, _| {
let item1 = item1.clone();
async move {
if item1 == item_to_resolve {
Ok(lsp::CompletionItem {
label: "method id()".to_string(),
filter_text: Some("id".to_string()),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(
lsp::Position::new(0, 22),
lsp::Position::new(0, 22),
),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
})
} else {
Ok(item_to_resolve)
}
}
}
})
.next()
.await;
.await
.unwrap();
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), cx);
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem {
label: "invalid changed label".to_string(),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
})
})
.next()
.await;
cx.run_until_parked();
cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut();
let context_menu = context_menu
@@ -10783,6 +10784,172 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
});
}
#[gpui::test]
async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let unresolved_item_1 = lsp::CompletionItem {
label: "id".to_string(),
filter_text: Some("id".to_string()),
detail: None,
documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
};
let resolved_item_1 = lsp::CompletionItem {
additional_text_edits: Some(vec![lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
new_text: "!!".to_string(),
}]),
..unresolved_item_1.clone()
};
let unresolved_item_2 = lsp::CompletionItem {
label: "other".to_string(),
filter_text: Some("other".to_string()),
detail: None,
documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".other".to_string(),
})),
..lsp::CompletionItem::default()
};
let resolved_item_2 = lsp::CompletionItem {
additional_text_edits: Some(vec![lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
new_text: "??".to_string(),
}]),
..unresolved_item_2.clone()
};
let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let unresolved_item_1 = unresolved_item_1.clone();
let resolved_item_1 = resolved_item_1.clone();
let unresolved_item_2 = unresolved_item_2.clone();
let resolved_item_2 = resolved_item_2.clone();
let resolve_requests_1 = resolve_requests_1.clone();
let resolve_requests_2 = resolve_requests_2.clone();
move |unresolved_request, _| {
let unresolved_item_1 = unresolved_item_1.clone();
let resolved_item_1 = resolved_item_1.clone();
let unresolved_item_2 = unresolved_item_2.clone();
let resolved_item_2 = resolved_item_2.clone();
let resolve_requests_1 = resolve_requests_1.clone();
let resolve_requests_2 = resolve_requests_2.clone();
async move {
if unresolved_request == unresolved_item_1 {
resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
Ok(resolved_item_1.clone())
} else if unresolved_request == unresolved_item_2 {
resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
Ok(resolved_item_2.clone())
} else {
panic!("Unexpected completion item {unresolved_request:?}")
}
}
}
})
.detach();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let unresolved_item_1 = unresolved_item_1.clone();
let unresolved_item_2 = unresolved_item_2.clone();
async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
unresolved_item_1,
unresolved_item_2,
])))
}
})
.next()
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut();
let context_menu = context_menu
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.borrow_mut();
assert_eq!(
completions
.iter()
.map(|completion| &completion.label.text)
.collect::<Vec<_>>(),
vec!["id", "other"]
)
}
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
}
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&ContextMenuNext, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_prev(&ContextMenuPrev, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&ContextMenuNext, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor
.compose_completion(&ComposeCompletion::default(), cx)
.expect("No task returned")
})
.await
.expect("Completion failed");
cx.run_until_parked();
cx.update_editor(|editor, cx| {
assert_eq!(
resolve_requests_1.load(atomic::Ordering::Acquire),
1,
"Should always resolve once despite multiple selections"
);
assert_eq!(
resolve_requests_2.load(atomic::Ordering::Acquire),
1,
"Should always resolve once after multiple selections and applying the completion"
);
assert_eq!(
editor.text(cx),
"fn main() { let a = ??.other; }",
"Should use resolved data when applying the completion"
);
});
}
#[gpui::test]
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -10978,8 +11145,8 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
1,
"After re-selecting the first item, no new resolve requests should be sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
@@ -10989,7 +11156,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
2,
"After selecting the other item, another resolve request should have been sent"
);
}
@@ -11129,7 +11296,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
)));
update_test_language_settings(cx, |settings| {
settings.defaults.prettier = Some(PrettierSettings {
@@ -14601,6 +14768,62 @@ fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.set_state(indoc! {"
struct Fˇoo {}
"});
cx.update_editor(|editor, cx| {
let highlight_range = Point::new(0, 7)..Point::new(0, 10);
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|c| c.editor_document_highlight_read_background,
cx,
);
});
cx.update_editor(|e, cx| e.rename(&Rename, cx))
.expect("Rename was not started")
.await
.expect("Rename failed");
let mut rename_handler =
cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
let edit = lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 7,
},
end: lsp::Position {
line: 0,
character: 10,
},
},
new_text: "FooRenamed".to_string(),
};
Ok(Some(lsp::WorkspaceEdit::new(
// Specify the same edit twice
std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
)))
});
cx.update_editor(|e, cx| e.confirm_rename(&ConfirmRename, cx))
.expect("Confirm rename was not started")
.await
.expect("Confirm rename failed");
rename_handler.next().await.unwrap();
cx.run_until_parked();
// Despite two edits, only one is actually applied as those are identical
cx.assert_editor_state(indoc! {"
struct FooRenamedˇ {}
"});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -33,9 +33,9 @@ use util::ResultExt;
use crate::{
current_platform, hash, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
Asset, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
Entity, EventEmitter, FocusHandle, FocusId, ForegroundExecutor, Global, KeyBinding, Keymap,
Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, Context, DispatchPhase,
DisplayId, Entity, EventEmitter, FocusHandle, FocusId, ForegroundExecutor, Global, KeyBinding,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
Subscription, SvgRenderer, Task, TextSystem, View, ViewContext, Window, WindowAppearance,
@@ -1612,6 +1612,12 @@ pub struct AnyTooltip {
/// The absolute position of the mouse when the tooltip was deployed.
pub mouse_position: Point<Pixels>,
/// Whether the tooltitp can be hovered or not.
pub hoverable: bool,
/// Bounds of the element that triggered the tooltip appearance.
pub origin_bounds: Bounds<Pixels>,
}
/// A keystroke event, and potentially the associated action

View File

@@ -1923,6 +1923,7 @@ impl Interactivity {
cx.on_mouse_event({
let active_tooltip = active_tooltip.clone();
let hitbox = hitbox.clone();
let source_bounds = hitbox.bounds;
let tooltip_id = self.tooltip_id;
move |_: &MouseMoveEvent, phase, cx| {
let is_hovered =
@@ -1952,6 +1953,8 @@ impl Interactivity {
tooltip: Some(AnyTooltip {
view: build_tooltip(cx),
mouse_position: cx.mouse_position(),
hoverable: tooltip_is_hoverable,
origin_bounds: source_bounds,
}),
_task: None,
});

View File

@@ -675,6 +675,7 @@ impl Element for InteractiveText {
if let Some(tooltip_builder) = self.tooltip_builder.clone() {
let hitbox = hitbox.clone();
let source_bounds = hitbox.bounds;
let active_tooltip = interactive_state.active_tooltip.clone();
let pending_mouse_down = interactive_state.mouse_down_index.clone();
let text_layout = text_layout.clone();
@@ -708,6 +709,8 @@ impl Element for InteractiveText {
tooltip: Some(AnyTooltip {
view: tooltip,
mouse_position: cx.mouse_position(),
hoverable: true,
origin_bounds: source_bounds,
}),
_task: None,
}

View File

@@ -1581,6 +1581,19 @@ impl<'a> WindowContext<'a> {
}
}
// Element's parent can get hidden (e.g. via the `visible_on_hover` method),
// and element's `paint` won't be called (ergo, mouse listeners also won't be active) to detect that the tooltip has to be removed.
// Ensure it's not stuck around in such cases.
let invalidate_tooltip = !tooltip_request
.tooltip
.origin_bounds
.contains(&self.mouse_position())
&& (!tooltip_request.tooltip.hoverable
|| !tooltip_bounds.contains(&self.mouse_position()));
if invalidate_tooltip {
return None;
}
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx));
self.window.tooltip_bounds = Some(TooltipBounds {

View File

@@ -2,8 +2,8 @@ use std::sync::Arc;
use feature_flags::ZedPro;
use gpui::{
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Task,
View, WeakView,
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
Subscription, Task, View, WeakView,
};
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use picker::{Picker, PickerDelegate};
@@ -17,6 +17,10 @@ type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>
pub struct LanguageModelSelector {
picker: View<Picker<LanguageModelPickerDelegate>>,
/// The task used to update the picker's matches when there is a change to
/// the language model registry.
update_matches_task: Option<Task<()>>,
_subscriptions: Vec<Subscription>,
}
impl LanguageModelSelector {
@@ -26,7 +30,51 @@ impl LanguageModelSelector {
) -> Self {
let on_model_changed = Arc::new(on_model_changed);
let all_models = LanguageModelRegistry::global(cx)
let all_models = Self::all_models(cx);
let delegate = LanguageModelPickerDelegate {
language_model_selector: cx.view().downgrade(),
on_model_changed: on_model_changed.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
LanguageModelSelector {
picker,
update_matches_task: None,
_subscriptions: vec![cx.subscribe(
&LanguageModelRegistry::global(cx),
Self::handle_language_model_registry_event,
)],
}
}
fn handle_language_model_registry_event(
&mut self,
_registry: Model<LanguageModelRegistry>,
event: &language_model::Event,
cx: &mut ViewContext<Self>,
) {
match event {
language_model::Event::ProviderStateChanged
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
let task = self.picker.update(cx, |this, cx| {
let query = this.query(cx);
this.delegate.all_models = Self::all_models(cx);
this.delegate.update_matches(query, cx)
});
self.update_matches_task = Some(task);
}
_ => {}
}
}
fn all_models(cx: &AppContext) -> Vec<ModelInfo> {
LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
@@ -44,20 +92,7 @@ impl LanguageModelSelector {
}
})
})
.collect::<Vec<_>>();
let delegate = LanguageModelPickerDelegate {
language_model_selector: cx.view().downgrade(),
on_model_changed: on_model_changed.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
LanguageModelSelector { picker }
.collect::<Vec<_>>()
}
}
@@ -152,25 +187,25 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let llm_registry = LanguageModelRegistry::global(cx);
let configured_models: Vec<_> = llm_registry
let configured_providers = llm_registry
.read(cx)
.providers()
.iter()
.filter(|provider| provider.is_authenticated(cx))
.map(|provider| provider.id())
.collect();
.collect::<Vec<_>>();
cx.spawn(|this, mut cx| async move {
let filtered_models = cx
.background_executor()
.spawn(async move {
let displayed_models = if configured_models.is_empty() {
let displayed_models = if configured_providers.is_empty() {
all_models
} else {
all_models
.into_iter()
.filter(|model_info| {
configured_models.contains(&model_info.model.provider_id())
configured_providers.contains(&model_info.model.provider_id())
})
.collect::<Vec<_>>()
};

View File

@@ -253,49 +253,51 @@ impl LspAdapter for RustLspAdapter {
.as_ref()
.and_then(|detail| detail.detail.as_ref())
.or(completion.detail.as_ref())
.map(ToOwned::to_owned);
.map(|detail| detail.trim());
let function_signature = completion
.label_details
.as_ref()
.and_then(|detail| detail.description.as_ref())
.or(completion.detail.as_ref())
.map(ToOwned::to_owned);
match completion.kind {
Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => {
.and_then(|detail| detail.description.as_deref())
.or(completion.detail.as_deref());
match (detail, completion.kind) {
(Some(detail), Some(lsp::CompletionItemKind::FIELD)) => {
let name = &completion.label;
let text = format!("{}: {}", name, detail.unwrap());
let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
let runs = language.highlight_text(&source, 11..11 + text.len());
let text = format!("{name}: {detail}");
let prefix = "struct S { ";
let source = Rope::from(format!("{prefix}{text} }}"));
let runs =
language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..name.len(),
});
}
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
if detail.is_some()
&& completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
{
(
Some(detail),
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
let name = &completion.label;
let text = format!(
"{}: {}",
name,
completion.detail.as_ref().or(detail.as_ref()).unwrap()
completion.detail.as_deref().unwrap_or(detail)
);
let source = Rope::from(format!("let {} = ();", text).as_str());
let runs = language.highlight_text(&source, 4..4 + text.len());
let prefix = "let ";
let source = Rope::from(format!("{prefix}{text} = ();"));
let runs =
language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..name.len(),
});
}
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
if detail.is_some() =>
{
(
Some(detail),
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
) => {
static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
let detail = detail.unwrap();
const FUNCTION_PREFIXES: [&str; 6] = [
"async fn",
"async unsafe fn",
@@ -315,10 +317,11 @@ impl LspAdapter for RustLspAdapter {
// fn keyword should be followed by opening parenthesis.
if let Some((prefix, suffix)) = fn_keyword {
let mut text = REGEX.replace(&completion.label, suffix).to_string();
let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
let source = Rope::from(format!("{prefix} {text} {{}}"));
let run_start = prefix.len() + 1;
let runs = language.highlight_text(&source, run_start..run_start + text.len());
if detail.starts_with(" (") {
if detail.starts_with("(") {
text.push(' ');
text.push_str(&detail);
}
@@ -342,7 +345,7 @@ impl LspAdapter for RustLspAdapter {
});
}
}
Some(kind) => {
(_, Some(kind)) => {
let highlight_name = match kind {
lsp::CompletionItemKind::STRUCT
| lsp::CompletionItemKind::INTERFACE
@@ -356,9 +359,9 @@ impl LspAdapter for RustLspAdapter {
};
let mut label = completion.label.clone();
if let Some(detail) = detail.filter(|detail| detail.starts_with(" (")) {
use std::fmt::Write;
write!(label, "{detail}").ok()?;
if let Some(detail) = detail.filter(|detail| detail.starts_with("(")) {
label.push(' ');
label.push_str(detail);
}
let mut label = CodeLabel::plain(label, None);
if let Some(highlight_name) = highlight_name {
@@ -883,7 +886,7 @@ mod tests {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello(…)".to_string(),
label_details: Some(CompletionItemLabelDetails {
detail: Some(" (use crate::foo)".into()),
detail: Some("(use crate::foo)".into()),
description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
}),
..Default::default()

View File

@@ -284,6 +284,7 @@ impl<F: Future> LspRequestFuture<F::Output> for LspRequest<F> {
}
/// Combined capabilities of the server and the adapter.
#[derive(Debug)]
pub struct AdapterServerCapabilities {
// Reported capabilities by the server
pub server_capabilities: ServerCapabilities,

View File

@@ -14,8 +14,7 @@ use crate::{
pub struct ProjectEnvironment {
cli_environment: Option<HashMap<String, String>>,
get_environment_task: Option<Shared<Task<Option<HashMap<String, String>>>>>,
cached_shell_environments: HashMap<WorktreeId, HashMap<String, String>>,
environments: HashMap<WorktreeId, Shared<Task<Option<HashMap<String, String>>>>>,
environment_error_messages: HashMap<WorktreeId, EnvironmentErrorMessage>,
}
@@ -35,27 +34,15 @@ impl ProjectEnvironment {
Self {
cli_environment,
get_environment_task: None,
cached_shell_environments: Default::default(),
environments: Default::default(),
environment_error_messages: Default::default(),
}
})
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn set_cached(
&mut self,
shell_environments: &[(WorktreeId, HashMap<String, String>)],
) {
self.cached_shell_environments = shell_environments
.iter()
.cloned()
.collect::<HashMap<_, _>>();
}
pub(crate) fn remove_worktree_environment(&mut self, worktree_id: WorktreeId) {
self.cached_shell_environments.remove(&worktree_id);
self.environment_error_messages.remove(&worktree_id);
self.environments.remove(&worktree_id);
}
/// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
@@ -91,96 +78,83 @@ impl ProjectEnvironment {
worktree_abs_path: Option<Arc<Path>>,
cx: &ModelContext<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if let Some(task) = self.get_environment_task.as_ref() {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
}
if let Some(cli_environment) = self.get_cli_environment() {
return cx
.spawn(|_, _| async move {
let path = cli_environment
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables from CLI. PATH={:?}",
path
);
Some(cli_environment)
})
.shared();
}
let Some((worktree_id, worktree_abs_path)) = worktree_id.zip(worktree_abs_path) else {
return Task::ready(None).shared();
};
if let Some(task) = self.environments.get(&worktree_id) {
task.clone()
} else {
let task = self
.build_environment_task(worktree_id, worktree_abs_path, cx)
.get_worktree_env(worktree_id, worktree_abs_path, cx)
.shared();
self.get_environment_task = Some(task.clone());
self.environments.insert(worktree_id, task.clone());
task
}
}
fn build_environment_task(
&mut self,
worktree_id: Option<WorktreeId>,
worktree_abs_path: Option<Arc<Path>>,
cx: &ModelContext<Self>,
) -> Task<Option<HashMap<String, String>>> {
let worktree = worktree_id.zip(worktree_abs_path);
let cli_environment = self.get_cli_environment();
if let Some(environment) = cli_environment {
cx.spawn(|_, _| async move {
let path = environment
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables from CLI. PATH={:?}",
path
);
Some(environment)
})
} else if let Some((worktree_id, worktree_abs_path)) = worktree {
self.get_worktree_env(worktree_id, worktree_abs_path, cx)
} else {
Task::ready(None)
}
}
fn get_worktree_env(
&mut self,
worktree_id: WorktreeId,
worktree_abs_path: Arc<Path>,
cx: &ModelContext<Self>,
) -> Task<Option<HashMap<String, String>>> {
let cached_env = self.cached_shell_environments.get(&worktree_id).cloned();
if let Some(env) = cached_env {
Task::ready(Some(env))
} else {
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
cx.spawn(|this, mut cx| async move {
let (mut shell_env, error_message) = cx
.background_executor()
.spawn({
let cwd = worktree_abs_path.clone();
async move { load_shell_environment(&cwd, &load_direnv).await }
})
.await;
cx.spawn(|this, mut cx| async move {
let (mut shell_env, error_message) = cx
.background_executor()
.spawn({
let worktree_abs_path = worktree_abs_path.clone();
async move {
load_worktree_shell_environment(&worktree_abs_path, &load_direnv).await
}
})
.await;
if let Some(shell_env) = shell_env.as_mut() {
let path = shell_env
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables shell launched in {:?}. PATH={:?}",
worktree_abs_path,
path
);
this.update(&mut cx, |this, _| {
this.cached_shell_environments
.insert(worktree_id, shell_env.clone());
})
.log_err();
if let Some(shell_env) = shell_env.as_mut() {
let path = shell_env
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables shell launched in {:?}. PATH={:?}",
worktree_abs_path,
path
);
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
}
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
}
if let Some(error) = error_message {
this.update(&mut cx, |this, _| {
this.environment_error_messages.insert(worktree_id, error);
})
.log_err();
}
if let Some(error) = error_message {
this.update(&mut cx, |this, _| {
this.environment_error_messages.insert(worktree_id, error);
})
.log_err();
}
shell_env
})
}
shell_env
})
}
}
@@ -213,6 +187,42 @@ impl EnvironmentErrorMessage {
}
}
async fn load_worktree_shell_environment(
worktree_abs_path: &Path,
load_direnv: &DirenvSettings,
) -> (
Option<HashMap<String, String>>,
Option<EnvironmentErrorMessage>,
) {
match smol::fs::metadata(worktree_abs_path).await {
Ok(meta) => {
let dir = if meta.is_dir() {
worktree_abs_path
} else if let Some(parent) = worktree_abs_path.parent() {
parent
} else {
return (
None,
Some(EnvironmentErrorMessage(format!(
"Failed to load shell environment in {}: not a directory",
worktree_abs_path.display()
))),
);
};
load_shell_environment(&dir, load_direnv).await
}
Err(err) => (
None,
Some(EnvironmentErrorMessage(format!(
"Failed to load shell environment in {}: {}",
worktree_abs_path.display(),
err
))),
),
}
}
#[cfg(any(test, feature = "test-support"))]
async fn load_shell_environment(
_dir: &Path,

View File

@@ -1918,6 +1918,7 @@ impl LspCommand for GetCompletions {
new_text,
server_id,
lsp_completion,
resolved: false,
}
})
.collect())

View File

@@ -2353,8 +2353,16 @@ impl LocalLspStore {
let (mut edits, mut snippet_edits) = (vec![], vec![]);
for edit in op.edits {
match edit {
Edit::Plain(edit) => edits.push(edit),
Edit::Annotated(edit) => edits.push(edit.text_edit),
Edit::Plain(edit) => {
if !edits.contains(&edit) {
edits.push(edit)
}
}
Edit::Annotated(edit) => {
if !edits.contains(&edit.text_edit) {
edits.push(edit.text_edit)
}
}
Edit::Snippet(edit) => {
let Ok(snippet) = Snippet::parse(&edit.snippet.value)
else {
@@ -2365,10 +2373,13 @@ impl LocalLspStore {
snippet_edits.push((edit.range, snippet));
} else {
// Since this buffer is not focused, apply a normal edit.
edits.push(TextEdit {
let new_edit = TextEdit {
range: edit.range,
new_text: snippet.text,
});
};
if !edits.contains(&new_edit) {
edits.push(new_edit);
}
}
}
}
@@ -4152,38 +4163,27 @@ impl LspStore {
let mut did_resolve = false;
if let Some((client, project_id)) = client {
for completion_index in completion_indices {
let (server_id, completion) = {
let completions = completions.borrow_mut();
let completion = &completions[completion_index];
did_resolve = true;
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
let server_id = completions.borrow()[completion_index].server_id;
(server_id, completion)
};
Self::resolve_completion_remote(
if Self::resolve_completion_remote(
project_id,
server_id,
buffer_id,
completions.clone(),
completion_index,
completion,
client.clone(),
language_registry.clone(),
)
.await;
.await
.log_err()
.is_some()
{
did_resolve = true;
}
}
} else {
for completion_index in completion_indices {
let (server_id, completion) = {
let completions = completions.borrow_mut();
let completion = &completions[completion_index];
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
(server_id, completion)
};
let server_id = completions.borrow()[completion_index].server_id;
let server_and_adapter = this
.read_with(&cx, |lsp_store, _| {
@@ -4198,17 +4198,27 @@ impl LspStore {
continue;
};
did_resolve = true;
Self::resolve_completion_local(
let resolved = Self::resolve_completion_local(
server,
adapter,
&buffer_snapshot,
completions.clone(),
completion_index,
completion,
language_registry.clone(),
)
.await;
.await
.log_err()
.is_some();
if resolved {
Self::regenerate_completion_labels(
adapter,
&buffer_snapshot,
completions.clone(),
completion_index,
language_registry.clone(),
)
.await
.log_err();
did_resolve = true;
}
}
}
@@ -4218,13 +4228,10 @@ impl LspStore {
async fn resolve_completion_local(
server: Arc<lsp::LanguageServer>,
adapter: Arc<CachedLspAdapter>,
snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
language_registry: Arc<LanguageRegistry>,
) {
) -> Result<()> {
let can_resolve = server
.capabilities()
.completion_provider
@@ -4232,30 +4239,17 @@ impl LspStore {
.and_then(|options| options.resolve_provider)
.unwrap_or(false);
if !can_resolve {
return;
return Ok(());
}
let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
let Some(completion_item) = request.await.log_err() else {
return;
let request = {
let completion = &completions.borrow()[completion_index];
if completion.resolved {
return Ok(());
}
server.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion.clone())
};
if let Some(lsp_documentation) = completion_item.documentation.as_ref() {
let documentation = language::prepare_completion_documentation(
lsp_documentation,
&language_registry,
snapshot.language().cloned(),
)
.await;
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
let completion_item = request.await?;
if let Some(text_edit) = completion_item.text_edit.as_ref() {
// Technically we don't have to parse the whole `text_edit`, since the only
@@ -4283,28 +4277,61 @@ impl LspStore {
}
}
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.lsp_completion = completion_item;
completion.resolved = true;
Ok(())
}
async fn regenerate_completion_labels(
adapter: Arc<CachedLspAdapter>,
snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
language_registry: Arc<LanguageRegistry>,
) -> Result<()> {
let completion_item = completions.borrow()[completion_index]
.lsp_completion
.clone();
if let Some(lsp_documentation) = completion_item.documentation.as_ref() {
let documentation = language::prepare_completion_documentation(
lsp_documentation,
&language_registry,
snapshot.language().cloned(),
)
.await;
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
// 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() {
Some(language) => adapter
.labels_for_completions(&[completion_item.clone()], language)
.await
.log_err()
.unwrap_or_default(),
Some(language) => {
adapter
.labels_for_completions(&[completion_item.clone()], language)
.await?
}
None => Vec::new(),
}
.pop()
.flatten()
.unwrap_or_else(|| {
CodeLabel::plain(
completion_item.label.clone(),
completion_item.label,
completion_item.filter_text.as_deref(),
)
});
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.lsp_completion = completion_item;
if completion.label.filter_text() == new_label.filter_text() {
completion.label = new_label;
} else {
@@ -4317,6 +4344,8 @@ impl LspStore {
new_label.filter_text()
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
@@ -4326,29 +4355,30 @@ impl LspStore {
buffer_id: BufferId,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
client: AnyProtoClient,
language_registry: Arc<LanguageRegistry>,
) {
) -> Result<()> {
let lsp_completion = {
let completion = &completions.borrow()[completion_index];
if completion.resolved {
return Ok(());
}
serde_json::to_string(&completion.lsp_completion)
.unwrap()
.into_bytes()
};
let request = proto::ResolveCompletionDocumentation {
project_id,
language_server_id: server_id.0 as u64,
lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
lsp_completion,
buffer_id: buffer_id.into(),
};
let Some(response) = client
let response = client
.request(request)
.await
.context("completion documentation resolve proto request")
.log_err()
else {
return;
};
let Some(lsp_completion) = serde_json::from_slice(&response.lsp_completion).log_err()
else {
return;
};
.context("completion documentation resolve proto request")?;
let lsp_completion = serde_json::from_slice(&response.lsp_completion)?;
let documentation = if response.documentation.is_empty() {
Documentation::Undocumented
@@ -4366,6 +4396,7 @@ impl LspStore {
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
completion.lsp_completion = lsp_completion;
completion.resolved = true;
let old_range = response
.old_start
@@ -4377,12 +4408,15 @@ impl LspStore {
completion.old_range = old_start..old_end;
}
}
Ok(())
}
pub fn apply_additional_edits_for_completion(
&self,
buffer_handle: Model<Buffer>,
completion: Completion,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
@@ -4391,8 +4425,9 @@ impl LspStore {
if let Some((client, project_id)) = self.upstream_client() {
cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::ApplyCompletionAdditionalEdits {
let request = {
let completion = completions.borrow()[completion_index].clone();
proto::ApplyCompletionAdditionalEdits {
project_id,
buffer_id: buffer_id.into(),
completion: Some(Self::serialize_completion(&CoreCompletion {
@@ -4400,9 +4435,13 @@ impl LspStore {
new_text: completion.new_text,
server_id: completion.server_id,
lsp_completion: completion.lsp_completion,
resolved: completion.resolved,
})),
})
.await?;
}
};
let response = client.request(request).await?;
completions.borrow_mut()[completion_index].resolved = true;
if let Some(transaction) = response.transaction {
let transaction = language::proto::deserialize_transaction(transaction)?;
@@ -4422,34 +4461,31 @@ impl LspStore {
}
})
} else {
let server_id = completion.server_id;
let lang_server = match self.language_server_for_local_buffer(buffer, server_id, cx) {
let server_id = completions.borrow()[completion_index].server_id;
let server = match self.language_server_for_local_buffer(buffer, server_id, cx) {
Some((_, server)) => server.clone(),
_ => return Task::ready(Ok(Default::default())),
_ => return Task::ready(Ok(None)),
};
let snapshot = buffer_handle.read(&cx).snapshot();
cx.spawn(move |this, mut cx| async move {
let can_resolve = lang_server
.capabilities()
.completion_provider
.as_ref()
.and_then(|options| options.resolve_provider)
.unwrap_or(false);
let additional_text_edits = if can_resolve {
lang_server
.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
.await?
.additional_text_edits
} else {
completion.lsp_completion.additional_text_edits
};
Self::resolve_completion_local(
server.clone(),
&snapshot,
completions.clone(),
completion_index,
)
.await
.context("resolving completion")?;
let completion = completions.borrow()[completion_index].clone();
let additional_text_edits = completion.lsp_completion.additional_text_edits;
if let Some(edits) = additional_text_edits {
let edits = this
.update(&mut cx, |this, cx| {
this.as_local_mut().unwrap().edits_from_lsp(
&buffer_handle,
edits,
lang_server.server_id(),
server.server_id(),
None,
cx,
)
@@ -6803,7 +6839,7 @@ impl LspStore {
let apply_additional_edits = this.update(&mut cx, |this, cx| {
this.apply_additional_edits_for_completion(
buffer,
Completion {
Rc::new(RefCell::new(Box::new([Completion {
old_range: completion.old_range,
new_text: completion.new_text,
lsp_completion: completion.lsp_completion,
@@ -6815,7 +6851,9 @@ impl LspStore {
filter_range: Default::default(),
},
confirm: None,
},
resolved: completion.resolved,
}]))),
0,
false,
cx,
)
@@ -7780,6 +7818,7 @@ impl LspStore {
new_text: completion.new_text.clone(),
server_id: completion.server_id.0 as u64,
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
resolved: completion.resolved,
}
}
@@ -7799,6 +7838,7 @@ impl LspStore {
new_text: completion.new_text,
server_id: LanguageServerId(completion.server_id as usize),
lsp_completion,
resolved: completion.resolved,
})
}
@@ -7900,6 +7940,7 @@ async fn populate_labels_for_completions(
documentation,
lsp_completion,
confirm: None,
resolved: false,
})
}
}

View File

@@ -73,10 +73,8 @@ use snippet::Snippet;
use snippet_provider::SnippetProvider;
use std::{
borrow::Cow,
cell::RefCell,
ops::Range,
path::{Component, Path, PathBuf},
rc::Rc,
str,
sync::Arc,
time::Duration,
@@ -353,6 +351,8 @@ pub struct Completion {
pub documentation: Option<Documentation>,
/// The raw completion provided by the language server.
pub lsp_completion: lsp::CompletionItem,
/// Whether this completion has been resolved, to ensure it happens once per completion.
pub resolved: bool,
/// An optional callback to invoke when this completion is confirmed.
/// Returns, whether new completions should be retriggered after the current one.
/// If `true` is returned, the editor will show a new completion menu after this completion is confirmed.
@@ -380,6 +380,7 @@ pub(crate) struct CoreCompletion {
new_text: String,
server_id: LanguageServerId,
lsp_completion: lsp::CompletionItem,
resolved: bool,
}
/// A code action provided by a language server.
@@ -1207,13 +1208,6 @@ impl Project {
.await
.unwrap();
project.update(cx, |project, cx| {
let tree_id = tree.read(cx).id();
project.environment.update(cx, |environment, _| {
environment.set_cached(&[(tree_id, HashMap::default())])
});
});
tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
}
@@ -2870,35 +2864,6 @@ impl Project {
})
}
pub fn resolve_completions(
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.resolve_completions(buffer, completion_indices, completions, cx)
})
}
pub fn apply_additional_edits_for_completion(
&self,
buffer_handle: Model<Buffer>,
completion: Completion,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.apply_additional_edits_for_completion(
buffer_handle,
completion,
push_to_history,
cx,
)
})
}
pub fn code_actions<T: Clone + ToOffset>(
&mut self,
buffer_handle: &Model<Buffer>,

View File

@@ -433,7 +433,10 @@ impl Project {
"windows" => "\r",
_ => "\n",
};
Some(format!("{} {}{}", activate_keyword, quoted, line_ending))
Some(format!(
"{} {} ; clear{}",
activate_keyword, quoted, line_ending
))
}
fn activate_python_virtual_environment(
@@ -450,7 +453,7 @@ impl Project {
}
}
pub fn wrap_for_ssh(
fn wrap_for_ssh(
ssh_command: &SshCommand,
command: Option<(&String, &Vec<String>)>,
path: Option<&Path>,

View File

@@ -927,6 +927,7 @@ message Completion {
string new_text = 3;
uint64 server_id = 4;
bytes lsp_completion = 5;
bool resolved = 6;
}
message GetCodeActions {

View File

@@ -964,19 +964,23 @@ pub fn new_terminal_pane(
pane.set_should_display_tab_bar(|_| true);
pane.set_zoom_out_on_close(false);
let terminal_panel_for_split_check = terminal_panel.clone();
let split_closure_terminal_panel = terminal_panel.downgrade();
pane.set_can_split(Some(Arc::new(move |pane, dragged_item, cx| {
if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
let current_pane = cx.view().clone();
let can_drag_away =
terminal_panel_for_split_check.update(cx, |terminal_panel, _| {
let is_current_pane = &tab.pane == cx.view();
let Some(can_drag_away) = split_closure_terminal_panel
.update(cx, |terminal_panel, _| {
let current_panes = terminal_panel.center.panes();
!current_panes.contains(&&tab.pane)
|| current_panes.len() > 1
|| (tab.pane != current_pane || pane.items_len() > 1)
});
|| (!is_current_pane || pane.items_len() > 1)
})
.ok()
else {
return false;
};
if can_drag_away {
let item = if tab.pane == current_pane {
let item = if is_current_pane {
pane.item_for_index(tab.ix)
} else {
tab.pane.read(cx).item_for_index(tab.ix)
@@ -996,7 +1000,12 @@ pub fn new_terminal_pane(
toolbar.add_item(breadcrumbs, cx);
});
let drop_closure_project = project.downgrade();
let drop_closure_terminal_panel = terminal_panel.downgrade();
pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
let Some(project) = drop_closure_project.upgrade() else {
return ControlFlow::Break(());
};
if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
let this_pane = cx.view().clone();
let item = if tab.pane == this_pane {
@@ -1009,10 +1018,10 @@ pub fn new_terminal_pane(
let source = tab.pane.clone();
let item_id_to_move = item.item_id();
let new_split_pane = pane
let Ok(new_split_pane) = pane
.drag_split_direction()
.map(|split_direction| {
terminal_panel.update(cx, |terminal_panel, cx| {
drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
let is_zoomed = if terminal_panel.active_pane == this_pane {
pane.is_zoomed()
} else {
@@ -1033,9 +1042,12 @@ pub fn new_terminal_pane(
anyhow::Ok(new_pane)
})
})
.transpose();
.transpose()
else {
return ControlFlow::Break(());
};
match new_split_pane {
match new_split_pane.transpose() {
// Source pane may be the one currently updated, so defer the move.
Ok(Some(new_pane)) => cx
.spawn(|_, mut cx| async move {

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.167.0"
version = "0.167.2"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
stable

View File

@@ -20,6 +20,7 @@ use command_palette_hooks::CommandPaletteFilter;
use editor::ProposedChangesEditorToolbar;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use feature_flags::FeatureFlagAppExt;
use futures::FutureExt;
use futures::{channel::mpsc, select_biased, StreamExt};
use gpui::{
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem,
@@ -349,7 +350,16 @@ fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Wo
workspace.add_panel(assistant_panel, cx);
})?;
let git_ui_enabled = git_ui_feature_flag.await;
let git_ui_enabled = {
let mut git_ui_feature_flag = git_ui_feature_flag.fuse();
let mut timeout =
FutureExt::fuse(smol::Timer::after(std::time::Duration::from_secs(5)));
select_biased! {
is_git_ui_enabled = git_ui_feature_flag => is_git_ui_enabled,
_ = timeout => false,
}
};
let git_panel = if git_ui_enabled {
Some(git_ui::git_panel::GitPanel::load(workspace_handle.clone(), cx.clone()).await?)
} else {
@@ -364,7 +374,14 @@ fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Wo
let is_assistant2_enabled = if cfg!(test) || release_channel != ReleaseChannel::Dev {
false
} else {
assistant2_feature_flag.await
let mut assistant2_feature_flag = assistant2_feature_flag.fuse();
let mut timeout =
FutureExt::fuse(smol::Timer::after(std::time::Duration::from_secs(5)));
select_biased! {
is_assistant2_enabled = assistant2_feature_flag => is_assistant2_enabled,
_ = timeout => false,
}
};
let assistant2_panel = if is_assistant2_enabled {
Some(assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?)