Compare commits

..

22 Commits

Author SHA1 Message Date
Bennet Bo Fenner
100ad7601d WIP 2025-04-24 15:15:26 +02:00
Kirill Bulatov
fcfeea4825 Allow creating entries when nothing is selected in the project panel (#29336)
Closes https://github.com/zed-industries/zed/issues/29249

Release Notes:

- Allowed creating entries when nothing is selected in the project panel
2025-04-24 08:46:35 +00:00
Conrad Irwin
c0f8e0f605 Fix context_stack race in KeyContextView (#29324)
cc @notpeter

Before this change we used our own copy of `cx.key_context()` when
matching.
This led to races where the context queried could be either before (or
after) the
context used in dispatching.

To avoid the race, gpui now passes out the context stack actually used
instead.

Release Notes:

- Fixed a bug where the Key Context View could show the incorrect
context,
  causing confusing results.
2025-04-23 23:34:39 -06:00
Conrad Irwin
9d10489607 Show diagnostic codes (#29296)
Closes #28135
Closes #4388
Closes #28136

Release Notes:

- diagnostics: Show the diagnostic code if available

---------

Co-authored-by: Neo Nie <nihgwu@live.com>
Co-authored-by: Zed AI <ai+claude-3.7@zed.dev>
2025-04-23 20:51:01 -06:00
Nathan Sobo
8836c6fb42 Introduce LanguageModelToolUse::raw_input (#29322)
This is to enable alternative streaming solutions at the application
layer. I'm not sure we really should have performed parsing of the input
at this layer. Either way I want to experiment with streaming approaches
in a separate crate on a branch, and this will help.

/cc @maxdeviant @bennetbo @rtfeldman

Closes #ISSUE

Release Notes:

- N/A
2025-04-24 02:30:48 +00:00
Max Brunsfeld
f125353b6f Add tree-sitter example to the eval (#29321)
Interesting things about this example:
* It's a useful, non-trivial change I made with the agent in Tree-sitter
* It runs fast
* It frequently showcases edit file errors
* It occasionally completely errors out due to errors parsing tool call
input JSON

Release Notes:

- N/A
2025-04-23 18:46:38 -07:00
Marshall Bowers
fef2681cfa language_models: Count Google AI tokens through LLM service (#29319)
This PR wires the counting of Google AI tokens back up.

It now goes through the LLM service instead of collab's RPC.

Still only available for Zed staff.

Release Notes:

- N/A
2025-04-24 01:21:53 +00:00
Agus Zubiaga
8b5835de17 agent: Improve initial file search quality (#29317)
This PR significantly improves the quality of the initial file search
that occurs when the model doesn't yet know the full path to a file it
needs to read/edit.

Previously, the assertions in file_search often failed on main as the
model attempted to guess full file paths. On this branch, it reliably
calls `find_path` (previously `path_search`) before reading files.

After getting the model to find paths first, I noticed it would try
using `grep` instead of `path_search`. This motivated renaming
`path_search` to `find_path` (continuing the analogy to unix commands)
and adding system prompt instructions about proper tool selection.

Note: I know the command is just called `find`, but that seemed too
general.

In my eval runs, the `file_search` example improved from 40% ± 10% to
98% ± 2%. The only assertion I'm seeing occasionally fail is "glob
starts with `**` or project". We can probably add some instructions in
that regard.

Release Notes:

- N/A
2025-04-23 21:24:41 -03:00
Agus Zubiaga
2124b7ea99 agent: Encourage model to include displayed fields first (#29308)
Instructs the model to include the fields that we display first in the
input object, so that e.g the user can see the path of a file while the
model generates the content.

Release Notes:

- N/A
2025-04-23 20:16:15 -03:00
Marshall Bowers
74442b68ea collab: Remove CountLanguageModelTokens RPC message (#29314)
This PR removes the `CountLanguageModelTokens` RPC message from collab.

We were only using this for Google AI models through the Zed provider
(which is only available to Zed staff).

For now we're returning `0`, but will bring back soon.

Release Notes:

- N/A
2025-04-23 23:10:47 +00:00
Danilo Leal
ba3d82629e ui: Add inline_code method to label (#29306)
This makes it easy to have a label that looks like Markdown inline code
via the `inline_code(cx)` method.

Release Notes:

- N/A
2025-04-23 19:27:56 -03:00
Marshall Bowers
ecc600a68f collab: Remove code for embeddings (#29310)
This PR removes the embeddings-related code from collab and the
protocol, as we weren't using it anywhere.

Release Notes:

- N/A
2025-04-23 18:27:46 -04:00
Remco Smits
218496744c debugger: Add support for inline value hints (#28656)
This PR uses Tree Sitter to show inline values while a user is in a
debug session.

We went with Tree Sitter over the LSP Inline Values request because the
LSP request isn't widely supported. Tree Sitter is easy for
languages/extensions to add support to. Tree Sitter can compute the
inline values locally, so there's no need to add extra RPC messages for
Collab. Tree Sitter also gives Zed more control over how we want to show
variables.

There's still more work to be done after this PR, namely differentiating
between global/local scoped variables, but it's a great starting point
to start iteratively improving it.

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Kirill <kirill@zed.dev>
2025-04-23 22:27:27 +00:00
Marshall Bowers
d095bab8ad agent: Read the user's plan from the UserStore (#29305)
This PR updates the Agent panel to read the user's plan from the
`UserStore` instead of hard-coding it.

Release Notes:

- N/A
2025-04-23 22:27:11 +00:00
Finn Evers
f8c3fe7871 editor: Fix broken mouse scrolling on main (#29307)
This PR is a quick follow-up to #29234 , which unfortunately broke
scrolling with the mouse in editors on main.

The linked PR introduced the possiblilty to completely disable scrolling
for editors. Unfortunately, it also disabled scrolling for editors by
default. This PR fixes this by re-enabling it by default.

This change also needs to be backported to v0.184.x. Otherwise, mouse
scrolling in the next preview release will not work!

Release Notes:

- N/A
2025-04-23 22:26:19 +00:00
Oleksiy Syvokon
aa161078fb agent: Add "copy to clipboard" button to error message popups (#29299)
This change makes agent errors copy-able to clipboard:


![image](https://github.com/user-attachments/assets/bd34a3f2-ecd4-4092-9b3b-960953ed1879)



Release Notes:

- N/A
2025-04-24 01:12:27 +03:00
Julia Ryan
f11c749353 VSCode Settings import (#29018)
Things this doesn't currently handle:

- [x] ~testing~
- ~we really need an snapshot test that takes a vscode settings file
with all options that we support, and verifies the zed settings file you
get from importing it, both from an empty starting file or one with lots
of conflicts. that way we can open said vscode settings file in vscode
to ensure that those options all still exist in the future.~
- Discussed this, we don't think this will meaningfully protect us from
future failures, and we will just do this as a manual validation step
before merging this PR. Any imports that have meaningfully complex
translation steps should still be tested.
- [x] confirmation (right now it just clobbers your settings file
silently)
- it'd be really cool if we could show a diff multibuffer of your
current settings with the result of the vscode import and let you pick
"hunks" to keep, but that's probably too much effort for this feature,
especially given that we expect most of the people using it to have an
empty/barebones zed config when they run the import.
- [x] ~UI in the "welcome" page~
- we're planning on redoing our welcome/walkthrough experience anyways,
but in the meantime it'd be nice to conditionally show a button there if
we see a user level vscode config
- we'll add it to the UI when we land the new walkthrough experience,
for now it'll be accessible through the action
- [ ] project-specific settings
- handling translation of `.vscode/settings.json` or `.code-workspace`
settings to `.zed/settings.json` will come in a future PR, along with UI
to prompt the user for those actions when opening a project with local
vscode settings for the first time
- [ ] extension settings
- we probably want to do a best-effort pass of popular extensions like
vim and git lens
- it's also possible to look for installed/enabled extensions with `code
--list-extensions`, but we'd have to maintain some sort of mapping of
those to our settings and/or extensions
- [ ] LSP settings
- these are tricky without access to the json schemas for various
language server extensions. we could probably manage to do translations
for a couple popular languages and avoid solving it in the general case.
- [ ] platform specific settings (`[macos].blah`)
  - this is blocked on #16392 which I'm hoping to address soon
- [ ] language specific settings (`[rust].foo`)
  - totally doable, just haven't gotten to it yet
 
~We may want to put this behind some kind of flag and/or not land it
until some of the above issues are addressed, given that we expect
people to only run this importer once there's an incentive to get it
right the first time. Maybe we land it alongside a keymap importer so
you don't have to go through separate imports for those?~

We are gonna land this as-is, all these unchecked items at the bottom
will be addressed in followup PRs, so maybe don't run the importer for
now if you have a large and complex VsCode settings file you'd like to
import.

Release Notes:

- Added a VSCode settings importer, available via a
`zed::ImportVsCodeSettings` action

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-04-23 20:54:09 +00:00
Danilo Leal
40b5a1b028 agent: Improve feedback text and buttons wrapping (#29302)
Just a little UI improvement here.

Release Notes:

- N/A
2025-04-23 17:12:52 -03:00
Peter Tripp
2d43818c04 rust-analyzer: Fix for deserialization error of CargoRunnableArgs (#29291)
Fix for error:

```log
2025-04-23T13:02:14-04:00 INFO  [lsp] starting language server process. binary path: "/Users/peter/Library/Application Support/Zed/languages/rust-analyzer/rust-analyzer-2025-04-21", working directory: "/Users/peter/zcode/zed", args: []
2025-04-23T13:02:16-04:00 ERROR [lsp] failed to deserialize response from language server: data did not match any variant of untagged enum RunnableArgs at line 1 column 199. response from language server: "[{\"label\":\"cargo check --workspace\",\"kind\":\"cargo\",\"args\":{\"cwd\":\"/Users/peter/zcode/zed/crates/gpui/src/platform/linux\",\"overrideCargo\":null,\"cargoArgs\":[\"check\",\"--workspace\"],\"executableArgs\":[]}}]"
2025-04-23T13:02:16-04:00 WARN  [project::lsp_store] LSP Runnables via rust-analyzer failed: failed to deserialize response
2025-04-23T13:02:16-04:00 ERROR [*unknown*] LSP Runnables via rust-analyzer failed: failed to deserialize response
```

Object is missing `environment`:
```json
[
  {
    "label": "cargo check --workspace",
    "kind": "cargo",
    "args": {
      "cwd": "/Users/peter/zcode/zed/crates/gpui/src/platform/linux",
      "overrideCargo": null,
      "cargoArgs": ["check", "--workspace"],
      "executableArgs": []
    }
  }
]
```

Follow-up to: https://github.com/zed-industries/zed/pull/28359

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-04-23 20:11:01 +00:00
Smit Barmase
636c6e7f2d editor: Make SelectNext and SelectPrevious preserve cursor direction (#29293)
Closes #27652

Now, if the last selection is reversed, subsequent `SelectNext` or
`SelectPrevious` selection will also be reversed.


https://github.com/user-attachments/assets/dff31abf-ac9e-4d35-bd2c-34e7b0f3ca23

Release Notes:

- Fixed an issue where `SelectNext` and `SelectPrevious` did not
preserve the last selection's cursor direction.
2025-04-24 00:17:17 +05:30
Agus Zubiaga
45d3f5168a eval: New add_arg_to_trait_method example (#29297)
Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-04-23 18:46:39 +00:00
Danilo Leal
8366cd0b52 agent: Render diffs for the edit file tool (#29234)
This PR implements the `ToolCard` for the edit file tool, which allow us
to display an editor with a diff in the thread view with the changes
performed by the model.

- [x] Fix buffer sometimes displaying empty
- [x] Stop buffer from scrolling together with the thread
- [x] Fix multibuffer header sometimes appearing
- [x] Fix buffer height issue
- [x] Implement "full height" expand button
- [x] Add "Jump To File" functionality
- [x] Polish and refine styles

Release Notes:

- agent: Added diff preview cards in the thread view for edits performed
by the agent.

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-04-23 15:43:33 -03:00
182 changed files with 4692 additions and 1740 deletions

418
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -296,6 +296,7 @@ livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
media = { path = "crates/media" }
@@ -605,7 +606,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.7.0"
zed_llm_client = "0.7.1"
zstd = "0.11"
metal = "0.29"

View File

@@ -31,6 +31,9 @@ If appropriate, use tool calls to explore the current project, which contains th
- When looking for symbols in the project, prefer the `grep` tool.
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
- Bias towards not asking the user for help if you can find the answer yourself.
{{! TODO: Only mention tools if they are enabled }}
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
## Fixing Diagnostics

View File

@@ -646,7 +646,7 @@
"fetch": true,
"list_directory": false,
"now": true,
"path_search": true,
"find_path": true,
"read_file": true,
"grep": true,
"thinking": true,
@@ -670,7 +670,7 @@
"list_directory": true,
"move_path": false,
"now": false,
"path_search": true,
"find_path": true,
"read_file": true,
"grep": true,
"rename": false,

View File

@@ -433,47 +433,39 @@ fn render_markdown_code_block(
workspace
.update(cx, {
|workspace, cx| {
if let Some(project_path) = workspace
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
{
let target = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
});
let open_task = workspace.open_path(
project_path,
None,
true,
window,
cx,
);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(target) = target {
if let Some(active_editor) =
item.downcast::<Editor>()
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor
.go_to_singleton_buffer_point(
target, window, cx,
);
})
.log_err();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
else {
return;
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task =
workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target, window, cx,
);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
@@ -807,10 +799,11 @@ impl ActiveThread {
self.thread.read(cx).summary_or_default()
}
pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.last_error.take();
self.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx))
self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
})
}
pub fn last_error(&self) -> Option<ThreadError> {
@@ -1314,7 +1307,7 @@ impl ActiveThread {
fn confirm_editing_message(
&mut self,
_: &menu::Confirm,
_: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some((message_id, state)) = self.editing_message.take() else {
@@ -1344,7 +1337,7 @@ impl ActiveThread {
self.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model.model, cx)
thread.send_to_model(model.model, Some(window.window_handle()), cx);
});
cx.notify();
}
@@ -1545,13 +1538,14 @@ impl ActiveThread {
window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
});
// For all items that should be aligned with the Assistant's response.
// For all items that should be aligned with the LLM's response.
const RESPONSE_PADDING_X: Pixels = px(18.);
let feedback_container = h_flex()
.py_2()
.px(RESPONSE_PADDING_X)
.gap_1()
.flex_wrap()
.justify_between();
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
Some(feedback) => feedback_container
@@ -1563,7 +1557,8 @@ impl ActiveThread {
}
})
.color(Color::Muted)
.size(LabelSize::XSmall),
.size(LabelSize::XSmall)
.truncate(),
)
.child(
h_flex()
@@ -1614,7 +1609,8 @@ impl ActiveThread {
"Rating the thread sends all of your current conversation to the Zed team.",
)
.color(Color::Muted)
.size(LabelSize::XSmall),
.size(LabelSize::XSmall)
.truncate(),
)
.child(
h_flex()
@@ -1850,11 +1846,9 @@ impl ActiveThread {
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, window, cx)),
)
parent.children(tool_uses.into_iter().map(|tool_use| {
self.render_tool_use(tool_use, window, workspace.clone(), cx)
}))
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
@@ -2447,10 +2441,11 @@ impl ActiveThread {
&self,
tool_use: ToolUse,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
return card.render(&tool_use.status, window, cx);
return card.render(&tool_use.status, window, workspace, cx);
}
let is_open = self
@@ -3047,7 +3042,7 @@ impl ActiveThread {
&mut self,
tool_use_id: LanguageModelToolUseId,
_: &ClickEvent,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
@@ -3063,6 +3058,7 @@ impl ActiveThread {
c.input.clone(),
&c.messages,
c.tool.clone(),
Some(window.window_handle()),
cx,
);
});
@@ -3074,11 +3070,12 @@ impl ActiveThread {
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
_: &ClickEvent,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let window_handle = window.window_handle();
self.thread.update(cx, |thread, cx| {
thread.deny_tool_use(tool_use_id, tool_name, cx);
thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx);
});
}

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use assistant_settings::{
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
ContextServerPresetContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
@@ -201,10 +201,10 @@ impl PickerDelegate for ToolPickerDelegate {
let profile_id = self.profile_id.clone();
let default_profile = self.profile.clone();
let tool = tool.clone();
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(boxed) => {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
let profiles = settings.profiles.get_or_insert_default();
move |settings: &mut AssistantSettingsContent, _cx| {
settings
.v2_setting(|v2_settings| {
let profiles = v2_settings.profiles.get_or_insert_default();
let profile =
profiles
.entry(profile_id)
@@ -240,9 +240,10 @@ impl PickerDelegate for ToolPickerDelegate {
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
}
}
}
}
_ => {}
Ok(())
})
.ok();
}
});
}

View File

@@ -12,13 +12,13 @@ use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
@@ -180,6 +180,7 @@ impl ActiveView {
pub struct AssistantPanel {
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
project: Entity<Project>,
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
@@ -243,6 +244,7 @@ impl AssistantPanel {
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let fs = workspace.app_state().fs.clone();
let user_store = workspace.app_state().user_store.clone();
let project = workspace.project();
let language_registry = project.read(cx).languages().clone();
let workspace = workspace.weak_handle();
@@ -307,6 +309,7 @@ impl AssistantPanel {
Self {
active_view,
workspace,
user_store,
project: project.clone(),
fs: fs.clone(),
language_registry,
@@ -356,14 +359,9 @@ impl AssistantPanel {
&self.thread_store
}
fn cancel(
&mut self,
_: &editor::actions::Cancel,
_window: &mut Window,
cx: &mut Context<Self>,
) {
fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
}
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
@@ -1548,9 +1546,19 @@ impl AssistantPanel {
}
fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let plan = self
.user_store
.read(cx)
.current_plan()
.map(|plan| match plan {
Plan::Free => zed_llm_client::Plan::Free,
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
})
.unwrap_or(zed_llm_client::Plan::Free);
let usage = self.thread.read(cx).last_usage()?;
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage).into_any_element())
Some(UsageBanner::new(plan, usage).into_any_element())
}
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
@@ -1605,6 +1613,8 @@ impl AssistantPanel {
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(ERROR_MESSAGE))
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
@@ -1651,6 +1661,8 @@ impl AssistantPanel {
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(ERROR_MESSAGE))
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, _, cx| {
@@ -1716,6 +1728,8 @@ impl AssistantPanel {
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(error_message))
.child(
Button::new("subscribe", call_to_action).on_click(cx.listener(
|this, _, _, cx| {
@@ -1747,6 +1761,7 @@ impl AssistantPanel {
message: SharedString,
cx: &mut Context<Self>,
) -> AnyElement {
let message_with_header = format!("{}\n{}", header, message);
v_flex()
.gap_0p5()
.child(
@@ -1761,12 +1776,14 @@ impl AssistantPanel {
.id("error-message")
.max_h_32()
.overflow_y_scroll()
.child(Label::new(message)),
.child(Label::new(message.clone())),
)
.child(
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(message_with_header))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
@@ -1780,6 +1797,15 @@ impl AssistantPanel {
.into_any()
}
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
let message = message.into();
IconButton::new("copy", IconName::Copy)
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
})
.tooltip(Tooltip::text("Copy Error Message"))
}
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");

View File

@@ -195,6 +195,7 @@ impl MessageEditor {
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
})
} else {
editor.set_mode(EditorMode::AutoHeight {
@@ -277,6 +278,7 @@ impl MessageEditor {
let context_store = self.context_store.clone();
let git_store = self.project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let window_handle = window.window_handle();
cx.spawn(async move |this, cx| {
let checkpoint = checkpoint.await.ok();
@@ -333,7 +335,7 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, cx);
thread.send_to_model(model, Some(window_handle), cx);
})
.log_err();
})
@@ -341,9 +343,9 @@ impl MessageEditor {
}
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let cancelled = self
.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
let cancelled = self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
});
if cancelled {
self.set_editor_is_expanded(false, cx);

View File

@@ -13,7 +13,9 @@ use feature_flags::{self, FeatureFlagAppExt};
use futures::future::Shared;
use futures::{FutureExt, StreamExt as _};
use git::repository::DiffType;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelImage, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
@@ -951,7 +953,12 @@ impl Thread {
self.remaining_turns = remaining_turns;
}
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
if self.remaining_turns == 0 {
return;
}
@@ -982,7 +989,7 @@ impl Thread {
};
}
self.stream_completion(request, model, cx);
self.stream_completion(request, model, window, cx);
}
pub fn used_tools_since_last_user_message(&self) -> bool {
@@ -1096,7 +1103,10 @@ impl Thread {
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
// Skip empty messages to avoid sending them to the LLM model
// if !request_message.content.is_empty() {
request.messages.push(request_message);
// }
}
// https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
@@ -1201,6 +1211,7 @@ impl Thread {
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
@@ -1382,7 +1393,7 @@ impl Thread {
match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
let tool_uses = thread.use_pending_tools(cx);
let tool_uses = thread.use_pending_tools(window, cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
}
StopReason::EndTurn => {}
@@ -1427,7 +1438,7 @@ impl Thread {
}));
}
thread.cancel_last_completion(cx);
thread.cancel_last_completion(window, cx);
}
}
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
@@ -1596,7 +1607,11 @@ impl Thread {
)
}
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
pub fn use_pending_tools(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = self.to_completion_request(cx);
let messages = Arc::new(request.messages);
@@ -1628,6 +1643,7 @@ impl Thread {
tool_use.input.clone(),
&messages,
tool,
window,
cx,
);
}
@@ -1644,9 +1660,10 @@ impl Thread {
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
tool: Arc<dyn Tool>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) {
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx);
self.tool_use
.run_pending_tool(tool_use_id, ui_text.into(), task);
}
@@ -1657,6 +1674,7 @@ impl Thread {
messages: &[LanguageModelRequestMessage],
input: serde_json::Value,
tool: Arc<dyn Tool>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) -> Task<()> {
let tool_name: Arc<str> = tool.name().into();
@@ -1669,6 +1687,7 @@ impl Thread {
messages,
self.project.clone(),
self.action_log.clone(),
window,
cx,
)
};
@@ -1691,7 +1710,7 @@ impl Thread {
output,
cx,
);
thread.tool_finished(tool_use_id, pending_tool_use, false, cx);
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
})
.ok();
}
@@ -1703,18 +1722,22 @@ impl Thread {
tool_use_id: LanguageModelToolUseId,
pending_tool_use: Option<PendingToolUse>,
canceled: bool,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
if self.all_tools_finished() {
dbg!("All tools finished");
let model_registry = LanguageModelRegistry::read_global(cx);
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
dbg!("Attach tool results");
self.attach_tool_results(cx);
if !canceled {
self.send_to_model(model, cx);
self.send_to_model(model, window, cx);
}
}
}
println!("ToolFinished {}", tool_use_id);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
@@ -1732,7 +1755,11 @@ impl Thread {
/// Cancels the last pending completion, if there are any pending.
///
/// Returns whether a completion was canceled.
pub fn cancel_last_completion(&mut self, cx: &mut Context<Self>) -> bool {
pub fn cancel_last_completion(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> bool {
let canceled = if self.pending_completions.pop().is_some() {
true
} else {
@@ -1743,6 +1770,7 @@ impl Thread {
pending_tool_use.id.clone(),
Some(pending_tool_use),
true,
window,
cx,
);
}
@@ -2199,6 +2227,7 @@ impl Thread {
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
let err = Err(anyhow::anyhow!(
@@ -2207,7 +2236,7 @@ impl Thread {
self.tool_use
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
self.tool_finished(tool_use_id.clone(), None, true, cx);
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
}
}

View File

@@ -72,6 +72,7 @@ impl ToolUseState {
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
raw_input: tool_use.input.to_string(),
input: tool_use.input.clone(),
is_input_complete: true,
})
@@ -449,6 +450,7 @@ impl ToolUseState {
message_id: MessageId,
request_message: &mut LanguageModelRequestMessage,
) {
dbg!(&self.tool_uses_by_assistant_message, &message_id);
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
for tool_use in tool_uses {
if self.tool_results.contains_key(&tool_use.id) {
@@ -471,6 +473,7 @@ impl ToolUseState {
message_id: MessageId,
request_message: &mut LanguageModelRequestMessage,
) {
dbg!(&self.tool_uses_by_user_message, &message_id);
if let Some(tool_uses) = self.tool_uses_by_user_message.get(&message_id) {
for tool_use_id in tool_uses {
if let Some(tool_result) = self.tool_results.get(tool_use_id) {

View File

@@ -73,6 +73,11 @@ impl LabelCommon for AnimatedLabel {
self.base = self.base.buffer_font(cx);
self
}
fn inline_code(mut self, cx: &App) -> Self {
self.base = self.base.inline_code(cx);
self
}
}
impl RenderOnce for AnimatedLabel {

View File

@@ -44,4 +44,6 @@ impl Settings for SlashCommandSettings {
.chain(sources.server),
)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -112,13 +112,27 @@ impl AssistantSettings {
}
/// Assistant panel settings
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct AssistantSettingsContent {
#[serde(flatten)]
pub inner: Option<AssistantSettingsContentInner>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum AssistantSettingsContent {
pub enum AssistantSettingsContentInner {
Versioned(Box<VersionedAssistantSettingsContent>),
Legacy(LegacyAssistantSettingsContent),
}
impl AssistantSettingsContentInner {
fn for_v2(content: AssistantSettingsContentV2) -> Self {
AssistantSettingsContentInner::Versioned(Box::new(VersionedAssistantSettingsContent::V2(
content,
)))
}
}
impl JsonSchema for AssistantSettingsContent {
fn schema_name() -> String {
VersionedAssistantSettingsContent::schema_name()
@@ -133,26 +147,21 @@ impl JsonSchema for AssistantSettingsContent {
}
}
impl Default for AssistantSettingsContent {
fn default() -> Self {
Self::Versioned(Box::new(VersionedAssistantSettingsContent::default()))
}
}
impl AssistantSettingsContent {
pub fn is_version_outdated(&self) -> bool {
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
match &self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAssistantSettingsContent::V1(_) => true,
VersionedAssistantSettingsContent::V2(_) => false,
},
AssistantSettingsContent::Legacy(_) => true,
Some(AssistantSettingsContentInner::Legacy(_)) => true,
None => false,
}
}
fn upgrade(&self) -> AssistantSettingsContentV2 {
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
match &self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
button: settings.button,
@@ -212,7 +221,7 @@ impl AssistantSettingsContent {
},
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
},
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
Some(AssistantSettingsContentInner::Legacy(settings)) => AssistantSettingsContentV2 {
enabled: None,
button: settings.button,
dock: settings.dock,
@@ -237,12 +246,13 @@ impl AssistantSettingsContent {
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
},
None => AssistantSettingsContentV2::default(),
}
}
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
match &mut self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAssistantSettingsContent::V1(ref mut settings) => {
settings.dock = Some(dock);
}
@@ -250,9 +260,17 @@ impl AssistantSettingsContent {
settings.dock = Some(dock);
}
},
AssistantSettingsContent::Legacy(settings) => {
Some(AssistantSettingsContentInner::Legacy(settings)) => {
settings.dock = Some(dock);
}
None => {
self.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
dock: Some(dock),
..Default::default()
},
))
}
}
}
@@ -260,8 +278,8 @@ impl AssistantSettingsContent {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
match &mut self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAssistantSettingsContent::V1(ref mut settings) => {
match provider.as_ref() {
"zed.dev" => {
@@ -337,56 +355,80 @@ impl AssistantSettingsContent {
settings.default_model = Some(LanguageModelSelection { provider, model });
}
},
AssistantSettingsContent::Legacy(settings) => {
Some(AssistantSettingsContentInner::Legacy(settings)) => {
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
settings.default_open_ai_model = Some(model);
}
}
None => {
self.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection { provider, model }),
..Default::default()
},
));
}
}
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
if let AssistantSettingsContent::Versioned(boxed) = self {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.inline_assistant_model = Some(LanguageModelSelection { provider, model });
}
}
self.v2_setting(|setting| {
setting.inline_assistant_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
}
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
if let AssistantSettingsContent::Versioned(boxed) = self {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.commit_message_model = Some(LanguageModelSelection { provider, model });
self.v2_setting(|setting| {
setting.commit_message_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
}
pub fn v2_setting(
&mut self,
f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
match self.inner.get_or_insert_with(|| {
AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 {
..Default::default()
})
}) {
AssistantSettingsContentInner::Versioned(boxed) => {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
f(settings)
} else {
Ok(())
}
}
_ => Ok(()),
}
}
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
if let AssistantSettingsContent::Versioned(boxed) = self {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.thread_summary_model = Some(LanguageModelSelection { provider, model });
}
}
self.v2_setting(|setting| {
setting.thread_summary_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
}
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
let AssistantSettingsContent::Versioned(boxed) = self else {
return;
};
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.always_allow_tool_actions = Some(allow);
}
self.v2_setting(|setting| {
setting.always_allow_tool_actions = Some(allow);
Ok(())
})
.ok();
}
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
let AssistantSettingsContent::Versioned(boxed) = self else {
return;
};
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.default_profile = Some(profile_id);
}
self.v2_setting(|setting| {
setting.default_profile = Some(profile_id);
Ok(())
})
.ok();
}
pub fn create_profile(
@@ -394,11 +436,7 @@ impl AssistantSettingsContent {
profile_id: AgentProfileId,
profile: AgentProfile,
) -> Result<()> {
let AssistantSettingsContent::Versioned(boxed) = self else {
return Ok(());
};
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
self.v2_setting(|settings| {
let profiles = settings.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
bail!("profile with ID '{profile_id}' already exists");
@@ -424,9 +462,9 @@ impl AssistantSettingsContent {
.collect(),
},
);
}
Ok(())
Ok(())
})
}
}
@@ -461,7 +499,7 @@ impl Default for VersionedAssistantSettingsContent {
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
@@ -708,6 +746,39 @@ impl Settings for AssistantSettings {
Ok(settings)
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
if let Some(b) = vscode
.read_value("chat.agent.enabled")
.and_then(|b| b.as_bool())
{
match &mut current.inner {
Some(AssistantSettingsContentInner::Versioned(versioned)) => {
match versioned.as_mut() {
VersionedAssistantSettingsContent::V1(setting) => {
setting.enabled = Some(b);
setting.button = Some(b);
}
VersionedAssistantSettingsContent::V2(setting) => {
setting.enabled = Some(b);
setting.button = Some(b);
}
}
}
Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
None => {
current.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
enabled: Some(b),
button: Some(b),
..Default::default()
},
));
}
}
}
}
}
fn merge<T>(target: &mut T, value: Option<T>) {
@@ -751,28 +822,30 @@ mod tests {
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
fs.clone(),
|settings, _| {
*settings = AssistantSettingsContent::Versioned(Box::new(
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
}),
))
*settings = AssistantSettingsContent {
inner: Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
},
)),
}
},
);
});

View File

@@ -28,6 +28,7 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -10,14 +10,16 @@ use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::AnyWindowHandle;
use gpui::Context;
use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use workspace::Workspace;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
@@ -65,6 +67,7 @@ pub trait ToolCard: 'static + Sized {
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement;
}
@@ -76,6 +79,7 @@ pub struct AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement,
}
@@ -86,11 +90,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
let entity = entity.downcast::<T>().unwrap();
entity.update(cx, |entity, cx| {
entity.render(status, window, cx).into_any_element()
entity
.render(status, window, workspace, cx)
.into_any_element()
})
}
@@ -102,8 +109,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
}
impl AnyToolCard {
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
(self.render)(self.entity.clone(), status, window, cx)
pub fn render(
&self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
(self.render)(self.entity.clone(), status, window, workspace, cx)
}
}
@@ -163,6 +176,7 @@ pub trait Tool: 'static + Send + Sync {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult;
}

View File

@@ -14,9 +14,11 @@ path = "src/assistant_tools.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -35,6 +37,7 @@ serde_json.workspace = true
ui.workspace = true
util.workspace = true
web_search.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
worktree.workspace = true
zed_llm_client.workspace = true

View File

@@ -9,12 +9,12 @@ mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod path_search_tool;
mod read_file_tool;
mod rename_tool;
mod replace;
@@ -45,18 +45,22 @@ use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_file_tool::EditFileTool;
use crate::fetch_tool::FetchTool;
use crate::find_path_tool::FindPathTool;
use crate::grep_tool::GrepTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::open_tool::OpenTool;
use crate::path_search_tool::PathSearchTool;
use crate::read_file_tool::ReadFileTool;
use crate::rename_tool::RenameTool;
use crate::symbol_info_tool::SymbolInfoTool;
use crate::terminal_tool::TerminalTool;
use crate::thinking_tool::ThinkingTool;
pub use path_search_tool::PathSearchToolInput;
pub use create_file_tool::CreateFileToolInput;
pub use edit_file_tool::EditFileToolInput;
pub use find_path_tool::FindPathToolInput;
pub use list_directory_tool::ListDirectoryToolInput;
pub use read_file_tool::ReadFileToolInput;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
@@ -78,7 +82,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(OpenTool);
registry.register_tool(CodeSymbolsTool);
registry.register_tool(ContentsTool);
registry.register_tool(PathSearchTool);
registry.register_tool(FindPathTool);
registry.register_tool(ReadFileTool);
registry.register_tool(GrepTool);
registry.register_tool(RenameTool);

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -97,7 +97,7 @@ pub struct BatchToolInput {
/// }
/// },
/// {
/// "name": "path_search",
/// "name": "find_path",
/// "input": {
/// "glob": "**/*test*.rs"
/// }
@@ -218,6 +218,7 @@ impl Tool for BatchTool {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<BatchToolInput>(input) {
@@ -258,7 +259,9 @@ impl Tool for BatchTool {
let action_log = action_log.clone();
let messages = messages.clone();
let tool_result = cx
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
.update(|cx| {
tool.run(invocation.input, &messages, project, action_log, window, cx)
})
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
tasks.push(tool_result.output);

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{self, LspAction, Project};
@@ -140,6 +140,7 @@ impl Tool for CodeActionTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeActionToolInput>(input) {

View File

@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, Symbol};
@@ -128,6 +128,7 @@ impl Tool for CodeSymbolsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -102,6 +102,7 @@ impl Tool for ContentsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ContentsToolInput>(input) {

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -76,6 +77,7 @@ impl Tool for CopyPathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -67,6 +68,7 @@ impl Tool for CreateDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -23,6 +24,9 @@ pub struct CreateFileToolInput {
///
/// You can create a new file by providing a path of "directory1/new_file.txt"
/// </example>
///
/// Make sure to include this field before the `contents` field in the input object
/// so that we can display it immediately.
pub path: String,
/// The text contents of the file to create.
@@ -89,6 +93,7 @@ impl Tool for CreateFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
@@ -62,6 +62,7 @@ impl Tool for DeletePathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -82,6 +82,7 @@ impl Tool for DiagnosticsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
match serde_json::from_value::<DiagnosticsToolInput>(input)

View File

@@ -1,18 +1,40 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use crate::{
replace::{replace_exact, replace_with_flexible_indent},
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
use gpui::{
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity,
};
use language::{
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
use crate::replace::replace_exact;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use ui::{Disclosure, Tooltip, Window, prelude::*};
use util::ResultExt;
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
///
/// Make sure to include this field before all the others in the input object
/// so that we can display it immediately.
pub display_description: String,
/// The full path of the file to modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST
@@ -34,12 +56,6 @@ pub struct EditFileToolInput {
/// </example>
pub path: PathBuf,
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
pub display_description: String,
/// The text to replace.
pub old_string: String,
@@ -113,6 +129,7 @@ impl Tool for EditFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<EditFileToolInput>(input) {
@@ -120,7 +137,18 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.spawn(async move |cx: &mut AsyncApp| {
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
cx.new(|cx| {
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
})
})
.ok()
});
let card_clone = card.clone();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
@@ -128,26 +156,38 @@ impl Tool for EditFileTool {
})??;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.old_string.is_empty() {
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
return Err(anyhow!(
"`old_string` can't be empty, use another tool if you want to create a file."
));
}
if input.old_string == input.new_string {
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
return Err(anyhow!(
"The `old_string` and `new_string` are identical, so no changes would be made."
));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
.await
// If that fails, try being flexible about indentation
.or_else(|| {
replace_with_flexible_indent(
&input.old_string,
&input.new_string,
&snapshot,
)
})?;
if diff.edits.is_empty() {
return None;
@@ -177,41 +217,409 @@ impl Tool for EditFileTool {
}
})?;
return Err(err)
return Err(err);
};
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| {
log.track_buffer(buffer.clone(), cx)
});
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
snapshot
})?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
})?.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await?;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}).await;
let new_text = snapshot.text();
let diff_str = cx
.background_spawn({
let old_text = old_text.clone();
let new_text = new_text.clone();
async move { language::unified_diff(&old_text, &new_text) }
})
.await;
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
})
.log_err();
}
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
Ok(format!(
"Edited {}:\n\n```diff\n{}\n```",
input.path.display(),
diff_str
))
});
}).into()
ToolResult {
output: task,
card: card.map(AnyToolCard::from),
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
multibuffer: Entity<MultiBuffer>,
project: Entity<Project>,
diff_task: Option<Task<Result<()>>>,
preview_expanded: bool,
full_height_expanded: bool,
editor_unique_id: EntityId,
}
impl EditFileToolCard {
fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
let editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: true,
},
multibuffer.clone(),
Some(project.clone()),
window,
cx,
);
editor.set_show_scrollbars(false, cx);
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_scrolling(cx);
editor.disable_expand_excerpt_buttons(cx);
editor.set_show_breakpoints(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_expand_all_diff_hunks(cx);
editor
});
Self {
editor_unique_id: editor.entity_id(),
path,
project,
editor,
multibuffer,
diff_task: None,
preview_expanded: true,
full_height_expanded: false,
}
}
fn set_diff(
&mut self,
path: Arc<Path>,
old_text: String,
new_text: String,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
self.diff_task = Some(cx.spawn(async move |this, cx| {
let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
let snapshot = buffer.read(cx).snapshot();
let diff = buffer_diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.collect::<Vec<_>>();
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
debug_assert!(is_newly_added);
multibuffer.add_diff(buffer_diff, cx);
});
cx.notify();
})
}));
}
}
impl ToolCard for EditFileToolCard {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let failed = matches!(status, ToolUseStatus::Error(_));
let path_label_button = h_flex()
.id(("edit-tool-path-label-button", self.editor_unique_id))
.w_full()
.max_w_full()
.px_1()
.gap_0p5()
.cursor_pointer()
.rounded_sm()
.opacity(0.8)
.hover(|label| {
label
.opacity(1.)
.bg(cx.theme().colors().element_hover.opacity(0.5))
})
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.child(
Icon::new(IconName::Pencil)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
div()
.text_size(rems(0.8125))
.child(self.path.display().to_string())
.ml_1p5()
.mr_0p5(),
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Ignored),
),
)
.on_click({
let path = self.path.clone();
let workspace = workspace.clone();
move |_, window, cx| {
workspace
.update(cx, {
|workspace, cx| {
let Some(project_path) =
workspace.project().read(cx).find_project_path(&path, cx)
else {
return;
};
let open_task =
workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
language::Point::new(0, 0),
window,
cx,
);
})
.log_err();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
}
})
.into_any_element();
let codeblock_header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let codeblock_header = h_flex()
.flex_none()
.p_1()
.gap_1()
.justify_between()
.rounded_t_md()
.when(!failed, |header| header.bg(codeblock_header_bg))
.child(path_label_button)
.map(|container| {
if failed {
container.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
} else {
container.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
self.preview_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.preview_expanded = !this.preview_expanded;
},
)),
)
}
});
let editor = self.editor.update(cx, |editor, cx| {
editor.render(window, cx).into_any_element()
});
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
(IconName::ChevronUp, "Collapse Code Block")
} else {
(IconName::ChevronDown, "Expand Code Block")
};
let gradient_overlay = div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let border_color = cx.theme().colors().border.opacity(0.6);
v_flex()
.mb_2()
.border_1()
.when(failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_lg()
.overflow_hidden()
.child(codeblock_header)
.when(!failed && self.preview_expanded, |card| {
card.child(
v_flex()
.relative()
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.map(|editor_container| {
if self.full_height_expanded {
editor_container.h_full()
} else {
editor_container.max_h_64()
}
})
.child(div().pl_1().child(editor))
.when(!self.full_height_expanded, |editor_container| {
editor_container.child(gradient_overlay)
}),
)
})
.when(!failed && self.preview_expanded, |card| {
card.child(
h_flex()
.id(("edit-tool-card-inner-hflex", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.rounded_b_md()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
}
}
async fn build_buffer(
mut text: String,
path: Arc<Path>,
language_registry: &Arc<language::LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<Buffer>> {
let line_ending = LineEnding::detect(&text);
LineEnding::normalize(&mut text);
let text = Rope::from(text);
let language = cx
.update(|_cx| language_registry.language_for_file_path(&path))?
.await
.ok();
let buffer = cx.new(|cx| {
let buffer = TextBuffer::new_normalized(
0,
cx.entity_id().as_non_zero_u64().into(),
line_ending,
text,
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
buffer
})?;
Ok(buffer)
}
async fn build_buffer_diff(
mut old_text: String,
buffer: &Entity<Buffer>,
language_registry: &Arc<LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
LineEnding::normalize(&mut old_text);
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
let base_buffer = cx
.update(|cx| {
Buffer::build_snapshot(
old_text.clone().into(),
buffer.language().cloned(),
Some(language_registry.clone()),
cx,
)
})?
.await;
let diff_snapshot = cx
.update(|cx| {
BufferDiffSnapshot::new_with_base_buffer(
buffer.text.clone(),
Some(old_text.into()),
base_buffer,
cx,
)
})?
.await;
cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer.text, cx);
diff.set_snapshot(diff_snapshot, &buffer.text, cx);
diff
})
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -145,6 +145,7 @@ impl Tool for FetchTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<FetchToolInput>(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -12,7 +12,7 @@ use util::paths::PathMatcher;
use worktree::Snapshot;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct PathSearchToolInput {
pub struct FindPathToolInput {
/// The glob to match against every path in the project.
///
/// <example>
@@ -34,11 +34,11 @@ pub struct PathSearchToolInput {
const RESULTS_PER_PAGE: usize = 50;
pub struct PathSearchTool;
pub struct FindPathTool;
impl Tool for PathSearchTool {
impl Tool for FindPathTool {
fn name(&self) -> String {
"path_search".into()
"find_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
@@ -46,7 +46,7 @@ impl Tool for PathSearchTool {
}
fn description(&self) -> String {
include_str!("./path_search_tool/description.md").into()
include_str!("./find_path_tool/description.md").into()
}
fn icon(&self) -> IconName {
@@ -54,11 +54,11 @@ impl Tool for PathSearchTool {
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<PathSearchToolInput>(format)
json_schema_for::<FindPathToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<PathSearchToolInput>(input.clone()) {
match serde_json::from_value::<FindPathToolInput>(input.clone()) {
Ok(input) => format!("Find paths matching “`{}`”", input.glob),
Err(_) => "Search paths".to_string(),
}
@@ -70,9 +70,10 @@ impl Tool for PathSearchTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
let (offset, glob) = match serde_json::from_value::<FindPathToolInput>(input) {
Ok(input) => (input.offset, input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
@@ -143,7 +144,7 @@ mod test {
use util::path;
#[gpui::test]
async fn test_path_search_tool(cx: &mut TestAppContext) {
async fn test_find_path_tool(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());

View File

@@ -1,4 +1,4 @@
Fast file pattern matching tool that works with any codebase size
Fast file path pattern matching tool that works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted alphabetically

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::OffsetRangeExt;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
@@ -20,6 +20,8 @@ use util::paths::PathMatcher;
pub struct GrepToolInput {
/// A regex pattern to search for in the entire project. Note that the regex
/// will be parsed by the Rust `regex` crate.
///
/// Do NOT specify a path here! This will only be matched against the code **content**.
pub regex: String,
/// A glob pattern for the paths of files to include in the search.
@@ -96,6 +98,7 @@ impl Tool for GrepTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
@@ -405,7 +408,7 @@ mod tests {
) -> String {
let tool = Arc::new(GrepTool);
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
match task.output.await {
Ok(result) => result,

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -76,6 +76,7 @@ impl Tool for ListDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {

View File

@@ -1 +1 @@
Lists files and directories in a given path. Prefer the `grep` or `path_search` tools when searching the codebase.
Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -89,6 +89,7 @@ impl Tool for MovePathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<MovePathToolInput>(input) {

View File

@@ -4,7 +4,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use chrono::{Local, Utc};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -59,6 +59,7 @@ impl Tool for NowTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
let input: NowToolInput = match serde_json::from_value(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -52,6 +52,7 @@ impl Tool for OpenTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input: OpenToolInput = match serde_json::from_value(input) {

View File

@@ -1,7 +1,8 @@
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use indoc::formatdoc;
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -87,6 +88,7 @@ impl Tool for ReadFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
@@ -193,7 +195,7 @@ mod test {
"path": "root/nonexistent_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -223,7 +225,7 @@ mod test {
"path": "root/small_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -253,7 +255,7 @@ mod test {
"path": "root/large_file.rs"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log.clone(), cx)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.output
})
.await;
@@ -277,7 +279,7 @@ mod test {
"offset": 1
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -323,7 +325,7 @@ mod test {
"end_line": 4
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{self, Buffer, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -87,6 +87,7 @@ impl Tool for RenameTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<RenameToolInput>(input) {

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AsyncApp, Entity, Task};
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -121,6 +121,7 @@ impl Tool for SymbolInfoTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {

View File

@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -78,6 +78,7 @@ impl Tool for TerminalTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input: TerminalToolInput = match serde_json::from_value(input) {

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -50,6 +50,7 @@ impl Tool for ThinkingTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
// This tool just "thinks out loud" and doesn't perform any actions.

View File

@@ -5,13 +5,16 @@ use crate::ui::ToolCallCardHeader;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{App, AppContext, Context, Entity, IntoElement, Task, Window};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry;
use workspace::Workspace;
use zed_llm_client::{WebSearchCitation, WebSearchResponse};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -54,6 +57,7 @@ impl Tool for WebSearchTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
@@ -111,6 +115,7 @@ impl ToolCard for WebSearchToolCard {
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
_workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let header = match self.response.as_ref() {
@@ -220,8 +225,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(in_progress_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Pending, window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Pending,
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
@@ -230,8 +240,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(successful_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Finished("".into()), window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Finished("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
@@ -240,8 +255,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(error_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Error("".into()), window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Error("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),

View File

@@ -118,6 +118,13 @@ impl Settings for AutoUpdateSetting {
Ok(Self(auto_update.0))
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.enum_setting("update.mode", current, |s| match s {
"none" | "manual" => Some(AutoUpdateSettingContent(false)),
_ => Some(AutoUpdateSettingContent(true)),
});
}
}
#[derive(Default)]

View File

@@ -32,4 +32,6 @@ impl Settings for CallSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -104,6 +104,8 @@ impl Settings for ClientSettings {
}
Ok(result)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
@@ -130,6 +132,10 @@ impl Settings for ProxySettings {
.or(sources.default.proxy.clone()),
})
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.string_setting("http.proxy", &mut current.proxy);
}
}
pub fn init_settings(cx: &mut App) {
@@ -518,6 +524,18 @@ impl settings::Settings for TelemetrySettings {
.unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
})
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.enum_setting("telemetry.telemetryLevel", &mut current.metrics, |s| {
Some(s == "all")
});
vscode.enum_setting("telemetry.telemetryLevel", &mut current.diagnostics, |s| {
Some(matches!(s, "all" | "error" | "crash"))
});
// we could translate telemetry.telemetryLevel, but just because users didn't want
// to send microsoft telemetry doesn't mean they don't want to send it to zed. their
// all/error/crash/off correspond to combinations of our "diagnostics" and "metrics".
}
}
impl Client {

View File

@@ -34,14 +34,12 @@ dashmap.workspace = true
derive_more.workspace = true
envy = "0.4.2"
futures.workspace = true
google_ai.workspace = true
hex.workspace = true
http_client.workspace = true
jsonwebtoken.workspace = true
livekit_api.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
parking_lot.workspace = true
prometheus = "0.14"
prost.workspace = true

View File

@@ -3,7 +3,7 @@ mod connection_pool;
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
use crate::llm::LlmTokenClaims;
use crate::{
AppState, Config, Error, RateLimit, Result, auth,
AppState, Error, Result, auth,
db::{
self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
@@ -33,11 +33,8 @@ use chrono::Utc;
use collections::{HashMap, HashSet};
pub use connection_pool::{ConnectionPool, ZedVersion};
use core::fmt::{self, Debug, Formatter};
use http_client::HttpClient;
use open_ai::{OPEN_AI_API_URL, OpenAiEmbeddingModel};
use reqwest_client::ReqwestClient;
use rpc::proto::split_repository_update;
use sha2::Digest;
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
use futures::{
@@ -134,7 +131,6 @@ struct Session {
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
app_state: Arc<AppState>,
supermaven_client: Option<Arc<SupermavenAdminApi>>,
http_client: Arc<dyn HttpClient>,
/// The GeoIP country code for the user.
#[allow(unused)]
geoip_country_code: Option<String>,
@@ -427,29 +423,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context)
.add_request_handler({
let app_state = app_state.clone();
move |request, response, session| {
let app_state = app_state.clone();
async move {
count_language_model_tokens(request, response, session, &app_state.config)
.await
}
}
})
.add_request_handler(get_cached_embeddings)
.add_request_handler({
let app_state = app_state.clone();
move |request, response, session| {
compute_embeddings(
request,
response,
session,
app_state.config.openai_api_key.clone(),
)
}
});
.add_message_handler(update_context);
Arc::new(server)
}
@@ -778,7 +752,6 @@ impl Server {
peer: this.peer.clone(),
connection_pool: this.connection_pool.clone(),
app_state: this.app_state.clone(),
http_client,
geoip_country_code,
system_id,
_executor: executor.clone(),
@@ -3697,223 +3670,6 @@ async fn acknowledge_buffer_version(
Ok(())
}
async fn count_language_model_tokens(
request: proto::CountLanguageModelTokens,
response: Response<proto::CountLanguageModelTokens>,
session: Session,
config: &Config,
) -> Result<()> {
authorize_access_to_legacy_llm_endpoints(&session).await?;
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProCountLanguageModelTokensRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => {
Box::new(FreeCountLanguageModelTokensRateLimit)
}
};
session
.app_state
.rate_limiter
.check(&*rate_limit, session.user_id())
.await?;
let result = match proto::LanguageModelProvider::from_i32(request.provider) {
Some(proto::LanguageModelProvider::Google) => {
let api_key = config
.google_ai_api_key
.as_ref()
.context("no Google AI API key configured on the server")?;
google_ai::count_tokens(
session.http_client.as_ref(),
google_ai::API_URL,
api_key,
serde_json::from_str(&request.request)?,
)
.await?
}
_ => return Err(anyhow!("unsupported provider"))?,
};
response.send(proto::CountLanguageModelTokensResponse {
token_count: result.total_tokens as u32,
})?;
Ok(())
}
struct ZedProCountLanguageModelTokensRateLimit;
impl RateLimit for ZedProCountLanguageModelTokensRateLimit {
fn capacity(&self) -> usize {
std::env::var("COUNT_LANGUAGE_MODEL_TOKENS_RATE_LIMIT_PER_HOUR")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(600) // Picked arbitrarily
}
fn refill_duration(&self) -> chrono::Duration {
chrono::Duration::hours(1)
}
fn db_name(&self) -> &'static str {
"zed-pro:count-language-model-tokens"
}
}
struct FreeCountLanguageModelTokensRateLimit;
impl RateLimit for FreeCountLanguageModelTokensRateLimit {
fn capacity(&self) -> usize {
std::env::var("COUNT_LANGUAGE_MODEL_TOKENS_RATE_LIMIT_PER_HOUR_FREE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(600 / 10) // Picked arbitrarily
}
fn refill_duration(&self) -> chrono::Duration {
chrono::Duration::hours(1)
}
fn db_name(&self) -> &'static str {
"free:count-language-model-tokens"
}
}
struct ZedProComputeEmbeddingsRateLimit;
impl RateLimit for ZedProComputeEmbeddingsRateLimit {
fn capacity(&self) -> usize {
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5000) // Picked arbitrarily
}
fn refill_duration(&self) -> chrono::Duration {
chrono::Duration::hours(1)
}
fn db_name(&self) -> &'static str {
"zed-pro:compute-embeddings"
}
}
struct FreeComputeEmbeddingsRateLimit;
impl RateLimit for FreeComputeEmbeddingsRateLimit {
fn capacity(&self) -> usize {
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR_FREE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5000 / 10) // Picked arbitrarily
}
fn refill_duration(&self) -> chrono::Duration {
chrono::Duration::hours(1)
}
fn db_name(&self) -> &'static str {
"free:compute-embeddings"
}
}
async fn compute_embeddings(
request: proto::ComputeEmbeddings,
response: Response<proto::ComputeEmbeddings>,
session: Session,
api_key: Option<Arc<str>>,
) -> Result<()> {
let api_key = api_key.context("no OpenAI API key configured on the server")?;
authorize_access_to_legacy_llm_endpoints(&session).await?;
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => Box::new(FreeComputeEmbeddingsRateLimit),
};
session
.app_state
.rate_limiter
.check(&*rate_limit, session.user_id())
.await?;
let embeddings = match request.model.as_str() {
"openai/text-embedding-3-small" => {
open_ai::embed(
session.http_client.as_ref(),
OPEN_AI_API_URL,
&api_key,
OpenAiEmbeddingModel::TextEmbedding3Small,
request.texts.iter().map(|text| text.as_str()),
)
.await?
}
provider => return Err(anyhow!("unsupported embedding provider {:?}", provider))?,
};
let embeddings = request
.texts
.iter()
.map(|text| {
let mut hasher = sha2::Sha256::new();
hasher.update(text.as_bytes());
let result = hasher.finalize();
result.to_vec()
})
.zip(
embeddings
.data
.into_iter()
.map(|embedding| embedding.embedding),
)
.collect::<HashMap<_, _>>();
let db = session.db().await;
db.save_embeddings(&request.model, &embeddings)
.await
.context("failed to save embeddings")
.trace_err();
response.send(proto::ComputeEmbeddingsResponse {
embeddings: embeddings
.into_iter()
.map(|(digest, dimensions)| proto::Embedding { digest, dimensions })
.collect(),
})?;
Ok(())
}
async fn get_cached_embeddings(
request: proto::GetCachedEmbeddings,
response: Response<proto::GetCachedEmbeddings>,
session: Session,
) -> Result<()> {
authorize_access_to_legacy_llm_endpoints(&session).await?;
let db = session.db().await;
let embeddings = db.get_embeddings(&request.model, &request.digests).await?;
response.send(proto::GetCachedEmbeddingsResponse {
embeddings: embeddings
.into_iter()
.map(|(digest, dimensions)| proto::Embedding { digest, dimensions })
.collect(),
})?;
Ok(())
}
/// This is leftover from before the LLM service.
///
/// The endpoints protected by this check will be moved there eventually.
async fn authorize_access_to_legacy_llm_endpoints(session: &Session) -> Result<(), Error> {
if session.is_staff() {
Ok(())
} else {
Err(anyhow!("permission denied"))?
}
}
/// Get a Supermaven API key for the user
async fn get_supermaven_api_key(
_request: proto::GetSupermavenApiKey,

View File

@@ -1544,6 +1544,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_value_hints: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
show_type_hints: true,
@@ -1559,6 +1560,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -1778,6 +1780,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: false,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -1794,6 +1797,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,

View File

@@ -86,6 +86,8 @@ impl Settings for CollaborationPanelSettings {
) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
impl Settings for ChatPanelSettings {
@@ -99,6 +101,8 @@ impl Settings for ChatPanelSettings {
) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
impl Settings for NotificationPanelSettings {
@@ -112,6 +116,8 @@ impl Settings for NotificationPanelSettings {
) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
impl Settings for MessageEditorSettings {
@@ -125,4 +131,6 @@ impl Settings for MessageEditorSettings {
) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use icons::IconName;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -77,6 +77,7 @@ impl Tool for ContextServerTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {

View File

@@ -58,4 +58,42 @@ impl Settings for ContextServerSettings {
) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
// we don't handle "inputs" replacement strings, see perplexity-key in this example:
// https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_configuration-example
#[derive(Deserialize)]
struct VsCodeServerCommand {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
// note: we don't support envFile and type
}
impl From<VsCodeServerCommand> for ServerCommand {
fn from(cmd: VsCodeServerCommand) -> Self {
Self {
path: cmd.command,
args: cmd.args.unwrap_or_default(),
env: cmd.env,
}
}
}
if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
current
.context_servers
.extend(mcp.iter().filter_map(|(k, v)| {
Some((
k.clone().into(),
ServerConfig {
command: Some(
serde_json::from_value::<VsCodeServerCommand>(v.clone())
.ok()?
.into(),
),
settings: None,
},
))
}));
}
}
}

View File

@@ -36,6 +36,7 @@ gpui.workspace = true
http_client.workspace = true
language.workspace = true
log.workspace = true
lsp-types.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
paths.workspace = true

View File

@@ -284,6 +284,10 @@ pub async fn fetch_latest_adapter_version_from_github(
})
}
pub trait InlineValueProvider {
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue>;
}
#[async_trait(?Send)]
pub trait DebugAdapter: 'static + Send + Sync {
fn name(&self) -> DebugAdapterName;
@@ -373,7 +377,12 @@ pub trait DebugAdapter: 'static + Send + Sync {
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary>;
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
None
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeAdapter {}

View File

@@ -54,6 +54,8 @@ impl Settings for DebuggerSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
impl Global for DebuggerSettings {}

View File

@@ -26,6 +26,7 @@ async-trait.workspace = true
dap.workspace = true
gpui.workspace = true
language.workspace = true
lsp-types.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Result, bail};
use async_trait::async_trait;
use dap::adapters::latest_github_release;
use dap::adapters::{InlineValueProvider, latest_github_release};
use gpui::AsyncApp;
use task::{DebugRequest, DebugTaskDefinition};
@@ -150,4 +150,25 @@ impl DebugAdapter for CodeLldbDebugAdapter {
connection: None,
})
}
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
Some(Box::new(CodeLldbInlineValueProvider))
}
}
struct CodeLldbInlineValueProvider;
impl InlineValueProvider for CodeLldbInlineValueProvider {
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue> {
variables
.into_iter()
.map(|(variable, range)| {
lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup {
range,
variable_name: Some(variable),
case_sensitive_lookup: true,
})
})
.collect()
}
}

View File

@@ -1,5 +1,5 @@
use crate::*;
use dap::{DebugRequest, StartDebuggingRequestArguments};
use dap::{StartDebuggingRequestArguments, adapters::InlineValueProvider};
use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
@@ -160,4 +160,34 @@ impl DebugAdapter for PythonDebugAdapter {
request_args: self.request_args(config),
})
}
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
Some(Box::new(PythonInlineValueProvider))
}
}
struct PythonInlineValueProvider;
impl InlineValueProvider for PythonInlineValueProvider {
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue> {
variables
.into_iter()
.map(|(variable, range)| {
if variable.contains(".") || variable.contains("[") {
lsp_types::InlineValue::EvaluatableExpression(
lsp_types::InlineValueEvaluatableExpression {
range,
expression: Some(variable),
},
)
} else {
lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup {
range,
variable_name: Some(variable),
case_sensitive_lookup: true,
})
}
})
.collect()
}
}

View File

@@ -247,7 +247,7 @@ pub fn init(cx: &mut App) {
let stack_id = state.selected_stack_frame_id(cx);
state.session().update(cx, |session, cx| {
session.evaluate(text, None, stack_id, None, cx);
session.evaluate(text, None, stack_id, None, cx).detach();
});
});
Some(())

View File

@@ -141,14 +141,16 @@ impl Console {
expression
});
self.session.update(cx, |state, cx| {
state.evaluate(
expression,
Some(dap::EvaluateArgumentsContext::Variables),
self.stack_frame_list.read(cx).selected_stack_frame_id(),
None,
cx,
);
self.session.update(cx, |session, cx| {
session
.evaluate(
expression,
Some(dap::EvaluateArgumentsContext::Variables),
self.stack_frame_list.read(cx).selected_stack_frame_id(),
None,
cx,
)
.detach();
});
}

View File

@@ -10,6 +10,7 @@ use gpui::{
};
use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
@@ -265,6 +266,7 @@ impl StackFrameList {
return Task::ready(Err(anyhow!("Project path not found")));
};
let stack_frame_id = stack_frame.id;
cx.spawn_in(window, async move |this, cx| {
let (worktree, relative_path) = this
.update(cx, |this, cx| {
@@ -313,12 +315,22 @@ impl StackFrameList {
.await?;
this.update(cx, |this, cx| {
let Some(thread_id) = this.state.read_with(cx, |state, _| state.thread_id)? else {
return Err(anyhow!("No selected thread ID found"));
};
this.workspace.update(cx, |workspace, cx| {
let breakpoint_store = workspace.project().read(cx).breakpoint_store();
breakpoint_store.update(cx, |store, cx| {
store.set_active_position(
(this.session.read(cx).session_id(), abs_path, position),
ActiveStackFrame {
session_id: this.session.read(cx).session_id(),
thread_id,
stack_frame_id,
path: abs_path,
position,
},
cx,
);
})

View File

@@ -28,6 +28,7 @@ impl DiagnosticRenderer {
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
merge_same_row: bool,
cx: &mut App,
) -> Vec<DiagnosticBlock> {
let Some(primary_ix) = diagnostic_group
@@ -45,7 +46,7 @@ impl DiagnosticRenderer {
if entry.diagnostic.is_primary {
continue;
}
if entry.range.start.row == primary.range.start.row {
if entry.range.start.row == primary.range.start.row && merge_same_row {
same_row.push(entry)
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
close.push(entry)
@@ -54,28 +55,48 @@ impl DiagnosticRenderer {
}
}
let mut markdown =
Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() {
format!("{}: {}", source, primary.diagnostic.message)
} else {
primary.diagnostic.message
})
.to_string();
let mut markdown = String::new();
let diagnostic = &primary.diagnostic;
markdown.push_str(&Markdown::escape(&diagnostic.message));
for entry in same_row {
markdown.push_str("\n- hint: ");
markdown.push_str(&Markdown::escape(&entry.diagnostic.message))
}
if diagnostic.source.is_some() || diagnostic.code.is_some() {
markdown.push_str(" (");
}
if let Some(source) = diagnostic.source.as_ref() {
markdown.push_str(&Markdown::escape(&source));
}
if diagnostic.source.is_some() && diagnostic.code.is_some() {
markdown.push(' ');
}
if let Some(code) = diagnostic.code.as_ref() {
if let Some(description) = diagnostic.code_description.as_ref() {
markdown.push('[');
markdown.push_str(&Markdown::escape(&code.to_string()));
markdown.push_str("](");
markdown.push_str(&Markdown::escape(description.as_ref()));
markdown.push(')');
} else {
markdown.push_str(&Markdown::escape(&code.to_string()));
}
}
if diagnostic.source.is_some() || diagnostic.code.is_some() {
markdown.push(')');
}
for (ix, entry) in &distant {
markdown.push_str("\n- hint: [");
markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",))
markdown.push_str(&format!(
"](file://#diagnostic-{buffer_id}-{group_id}-{ix})\n",
))
}
let mut results = vec![DiagnosticBlock {
initial_range: primary.range,
severity: primary.diagnostic.severity,
buffer_id,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
}];
@@ -91,7 +112,6 @@ impl DiagnosticRenderer {
results.push(DiagnosticBlock {
initial_range: entry.range,
severity: entry.diagnostic.severity,
buffer_id,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
});
@@ -105,15 +125,12 @@ impl DiagnosticRenderer {
};
let mut markdown = Markdown::escape(&markdown).to_string();
markdown.push_str(&format!(
" ([back](file://#diagnostic-{group_id}-{primary_ix}))"
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
));
// problem: group-id changes...
// - only an issue in diagnostics because caching
results.push(DiagnosticBlock {
initial_range: entry.range,
severity: entry.diagnostic.severity,
buffer_id,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
});
@@ -132,7 +149,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
editor: WeakEntity<Editor>,
cx: &mut App,
) -> Vec<BlockProperties<Anchor>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, true, cx);
blocks
.into_iter()
.map(|block| {
@@ -151,13 +168,40 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
})
.collect()
}
fn render_hover(
&self,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
range: Range<Point>,
buffer_id: BufferId,
cx: &mut App,
) -> Option<Entity<Markdown>> {
let blocks =
Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, false, cx);
blocks.into_iter().find_map(|block| {
if block.initial_range == range {
Some(block.markdown)
} else {
None
}
})
}
fn open_link(
&self,
editor: &mut Editor,
link: SharedString,
window: &mut Window,
cx: &mut Context<Editor>,
) {
DiagnosticBlock::open_link(editor, &None, link, window, cx);
}
}
#[derive(Clone)]
pub(crate) struct DiagnosticBlock {
pub(crate) initial_range: Range<Point>,
pub(crate) severity: DiagnosticSeverity,
pub(crate) buffer_id: BufferId,
pub(crate) markdown: Entity<Markdown>,
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
}
@@ -181,7 +225,6 @@ impl DiagnosticBlock {
let settings = ThemeSettings::get_global(cx);
let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
let line_height = editor_line_height;
let buffer_id = self.buffer_id;
let diagnostics_editor = self.diagnostics_editor.clone();
div()
@@ -195,14 +238,11 @@ impl DiagnosticBlock {
MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
.on_url_click({
move |link, window, cx| {
Self::open_link(
editor.clone(),
&diagnostics_editor,
link,
window,
buffer_id,
cx,
)
editor
.update(cx, |editor, cx| {
Self::open_link(editor, &diagnostics_editor, link, window, cx)
})
.ok();
}
}),
)
@@ -210,79 +250,71 @@ impl DiagnosticBlock {
}
pub fn open_link(
editor: WeakEntity<Editor>,
editor: &mut Editor,
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
link: SharedString,
window: &mut Window,
buffer_id: BufferId,
cx: &mut App,
cx: &mut Context<Editor>,
) {
editor
.update(cx, |editor, cx| {
let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
editor::hover_popover::open_markdown_url(link, window, cx);
return;
};
let Some((group_id, ix)) = maybe!({
let (group_id, ix) = diagnostic_link.split_once('-')?;
let group_id: usize = group_id.parse().ok()?;
let ix: usize = ix.parse().ok()?;
Some((group_id, ix))
}) else {
return;
};
let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
editor::hover_popover::open_markdown_url(link, window, cx);
return;
};
let Some((buffer_id, group_id, ix)) = maybe!({
let mut parts = diagnostic_link.split('-');
let buffer_id: u64 = parts.next()?.parse().ok()?;
let group_id: usize = parts.next()?.parse().ok()?;
let ix: usize = parts.next()?.parse().ok()?;
Some((BufferId::new(buffer_id).ok()?, group_id, ix))
}) else {
return;
};
if let Some(diagnostics_editor) = diagnostics_editor {
if let Some(diagnostic) = diagnostics_editor
.update(cx, |diagnostics, _| {
diagnostics
.diagnostics
.get(&buffer_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|d| d.diagnostic.group_id == group_id)
.nth(ix)
})
.ok()
.flatten()
{
let multibuffer = editor.buffer().read(cx);
let Some(snapshot) = multibuffer
.buffer(buffer_id)
.map(|entity| entity.read(cx).snapshot())
else {
return;
};
for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
if range.context.overlaps(&diagnostic.range, &snapshot) {
Self::jump_to(
editor,
Anchor::range_in_buffer(
excerpt_id,
buffer_id,
diagnostic.range,
),
window,
cx,
);
return;
}
}
}
} else {
if let Some(diagnostic) = editor
.snapshot(window, cx)
.buffer_snapshot
.diagnostic_group(buffer_id, group_id)
if let Some(diagnostics_editor) = diagnostics_editor {
if let Some(diagnostic) = diagnostics_editor
.update(cx, |diagnostics, _| {
diagnostics
.diagnostics
.get(&buffer_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|d| d.diagnostic.group_id == group_id)
.nth(ix)
{
Self::jump_to(editor, diagnostic.range, window, cx)
}
})
.ok()
.flatten()
{
let multibuffer = editor.buffer().read(cx);
let Some(snapshot) = multibuffer
.buffer(buffer_id)
.map(|entity| entity.read(cx).snapshot())
else {
return;
};
})
.ok();
for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
if range.context.overlaps(&diagnostic.range, &snapshot) {
Self::jump_to(
editor,
Anchor::range_in_buffer(excerpt_id, buffer_id, diagnostic.range),
window,
cx,
);
return;
}
}
}
} else {
if let Some(diagnostic) = editor
.snapshot(window, cx)
.buffer_snapshot
.diagnostic_group(buffer_id, group_id)
.nth(ix)
{
Self::jump_to(editor, diagnostic.range, window, cx)
}
};
}
fn jump_to<T: ToOffset>(

View File

@@ -416,6 +416,7 @@ impl ProjectDiagnosticsEditor {
group,
buffer_snapshot.remote_id(),
Some(this.clone()),
true,
cx,
)
})?;

View File

@@ -1,10 +1,13 @@
use super::*;
use collections::{HashMap, HashSet};
use editor::{
DisplayPoint, InlayId,
actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
DisplayPoint, EditorSettings, InlayId,
actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
display_map::{DisplayRow, Inlay},
test::{editor_content_with_blocks, editor_test_context::EditorTestContext},
test::{
editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext,
},
};
use gpui::{TestAppContext, VisualTestContext};
use indoc::indoc;
@@ -134,11 +137,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// comment 1
// comment 2
c(y);
§ use of moved value value used here after move
§ use of moved value
§ value used here after move
§ hint: move occurs because `y` has type `Vec<char>`, which does not
§ implement the `Copy` trait
d(x);
§ use of moved value value used here after move
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
§ hint: value moved here
@@ -168,7 +173,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "mismatched types\nexpected `usize`, found `char`".to_string(),
message: "mismatched types expected `usize`, found `char`".to_string(),
..Default::default()
}],
version: None,
@@ -206,11 +211,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// comment 1
// comment 2
c(y);
§ use of moved value value used here after move
§ use of moved value
§ value used here after move
§ hint: move occurs because `y` has type `Vec<char>`, which does not
§ implement the `Copy` trait
d(x);
§ use of moved value value used here after move
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
§ hint: value moved here
@@ -241,7 +248,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "mismatched types\nexpected `usize`, found `char`".to_string(),
message: "mismatched types expected `usize`, found `char`".to_string(),
..Default::default()
},
lsp::Diagnostic {
@@ -289,11 +296,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// comment 1
// comment 2
c(y);
§ use of moved value value used here after move
§ use of moved value
§ value used here after move
§ hint: move occurs because `y` has type `Vec<char>`, which does not
§ implement the `Copy` trait
d(x);
§ use of moved value value used here after move
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
§ hint: value moved here
@@ -1192,8 +1201,219 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
init_test(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
fn func(abˇc def: i32) -> u32 {
}
"});
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
..Default::default()
}],
},
&[],
cx,
)
})
}).unwrap();
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor::hover_popover::hover(editor, &Default::default(), window, cx)
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
}
#[gpui::test]
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
// Hover with just diagnostic, pops DiagnosticPopover immediately and then
// info popover once request completes
cx.set_state(indoc! {"
fn teˇst() { println!(); }
"});
// Send diagnostic to client
let range = cx.lsp_range(indoc! {"
fn «test»() { println!(); }
"});
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "A test diagnostic message.".to_string(),
..Default::default()
}],
},
&[],
cx,
)
})
})
.unwrap();
cx.run_until_parked();
// Hover pops diagnostic immediately
cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _, _| {
assert!(hover_state.diagnostic_popover.is_some());
assert!(hover_state.info_popovers.is_empty());
});
// Info Popover shows after request responded to
let range = cx.lsp_range(indoc! {"
fn «test»() { println!(); }
"});
cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "some new docs".to_string(),
}),
range: Some(range),
}))
});
let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay + 1);
cx.background_executor
.advance_clock(Duration::from_millis(delay));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _, _| {
hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
});
}
#[gpui::test]
async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"main.js": "
function test() {
const x = 10;
const y = 20;
return 1;
}
test();
"
.unindent(),
}),
)
.await;
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let uri = lsp::Url::from_file_path(path!("/root/main.js")).unwrap();
// Create diagnostics with code fields
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(1, 4),
lsp::Position::new(1, 14),
),
severity: Some(lsp::DiagnosticSeverity::WARNING),
code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
source: Some("eslint".to_string()),
message: "'x' is assigned a value but never used".to_string(),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(2, 4),
lsp::Position::new(2, 14),
),
severity: Some(lsp::DiagnosticSeverity::WARNING),
code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
source: Some("eslint".to_string()),
message: "'y' is assigned a value but never used".to_string(),
..Default::default()
},
],
version: None,
},
&[],
cx,
)
.unwrap();
});
// Open the project diagnostics view
let diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
// Verify that the diagnostic codes are displayed correctly
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
function test() {
const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars)
const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars)
return 1;
}"
}
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
env_logger::try_init().ok();
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);

View File

@@ -419,6 +419,7 @@ actions!(
OpenGitBlameCommit,
ToggleIndentGuides,
ToggleInlayHints,
ToggleInlineValues,
ToggleInlineDiagnostics,
ToggleEditPrediction,
ToggleLineNumbers,

View File

@@ -64,6 +64,14 @@ impl Inlay {
text: text.into(),
}
}
pub fn debugger_hint<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::DebuggerValue(id),
position,
text: text.into(),
}
}
}
impl sum_tree::Item for Transform {
@@ -287,6 +295,7 @@ impl<'a> Iterator for InlayChunks<'a> {
})
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
};
let next_inlay_highlight_endpoint;
let offset_in_inlay = self.output_offset - self.transforms.start().0;

View File

@@ -121,8 +121,11 @@ use mouse_context_menu::MouseContextMenu;
use persistence::DB;
use project::{
ProjectPath,
debugger::breakpoint_store::{
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
debugger::{
breakpoint_store::{
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
},
session::{Session, SessionEvent},
},
};
@@ -248,10 +251,27 @@ const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers {
function: false,
};
struct InlineValueCache {
enabled: bool,
inlays: Vec<InlayId>,
refresh_task: Task<Option<()>>,
}
impl InlineValueCache {
fn new(enabled: bool) -> Self {
Self {
enabled,
inlays: Vec::new(),
refresh_task: Task::ready(None),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId {
InlineCompletion(usize),
Hint(usize),
DebuggerValue(usize),
}
impl InlayId {
@@ -259,6 +279,7 @@ impl InlayId {
match self {
Self::InlineCompletion(id) => *id,
Self::Hint(id) => *id,
Self::DebuggerValue(id) => *id,
}
}
}
@@ -373,10 +394,32 @@ pub trait DiagnosticRenderer {
editor: WeakEntity<Editor>,
cx: &mut App,
) -> Vec<BlockProperties<Anchor>>;
fn render_hover(
&self,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
range: Range<Point>,
buffer_id: BufferId,
cx: &mut App,
) -> Option<Entity<markdown::Markdown>>;
fn open_link(
&self,
editor: &mut Editor,
link: SharedString,
window: &mut Window,
cx: &mut Context<Editor>,
);
}
pub(crate) struct GlobalDiagnosticRenderer(pub Arc<dyn DiagnosticRenderer>);
impl GlobalDiagnosticRenderer {
fn global(cx: &App) -> Option<Arc<dyn DiagnosticRenderer>> {
cx.try_global::<Self>().map(|g| g.0.clone())
}
}
impl gpui::Global for GlobalDiagnosticRenderer {}
pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) {
cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer)));
@@ -433,6 +476,8 @@ pub enum EditorMode {
scale_ui_elements_with_buffer_font_size: bool,
/// When set to `true`, the editor will render a background for the active line.
show_active_line_background: bool,
/// When set to `true`, the editor's height will be determined by its content.
sized_by_content: bool,
},
}
@@ -441,6 +486,7 @@ impl EditorMode {
Self::Full {
scale_ui_elements_with_buffer_font_size: true,
show_active_line_background: true,
sized_by_content: false,
}
}
@@ -798,6 +844,8 @@ pub struct Editor {
show_breadcrumbs: bool,
show_gutter: bool,
show_scrollbars: bool,
disable_scrolling: bool,
disable_expand_excerpt_buttons: bool,
show_line_numbers: Option<bool>,
use_relative_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
@@ -841,7 +889,7 @@ pub struct Editor {
read_only: bool,
leader_peer_id: Option<PeerId>,
remote_id: Option<ViewId>,
hover_state: HoverState,
pub hover_state: HoverState,
pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
gutter_hovered: bool,
hovered_link_state: Option<HoveredLinkState>,
@@ -918,6 +966,7 @@ pub struct Editor {
mouse_cursor_hidden: bool,
hide_mouse_mode: HideMouseMode,
pub change_list: ChangeList,
inline_value_cache: InlineValueCache,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1512,6 +1561,8 @@ impl Editor {
if editor.go_to_active_debug_line(window, cx) {
cx.stop_propagation();
}
editor.refresh_inline_values(cx);
}
_ => {}
},
@@ -1589,11 +1640,13 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: true,
disable_scrolling: false,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode.is_full(),
show_line_numbers: None,
use_relative_line_numbers: None,
disable_expand_excerpt_buttons: false,
show_git_diff_gutter: None,
show_code_actions: None,
show_runnables: None,
@@ -1652,6 +1705,7 @@ impl Editor {
released_too_fast: false,
},
inline_diagnostics_enabled: mode.is_full(),
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@@ -1781,6 +1835,33 @@ impl Editor {
},
));
if let Some(dap_store) = this
.project
.as_ref()
.map(|project| project.read(cx).dap_store())
{
let weak_editor = cx.weak_entity();
this._subscriptions
.push(
cx.observe_new::<project::debugger::session::Session>(move |_, _, cx| {
let session_entity = cx.entity();
weak_editor
.update(cx, |editor, cx| {
editor._subscriptions.push(
cx.subscribe(&session_entity, Self::on_debug_session_event),
);
})
.ok();
}),
);
for session in dap_store.read(cx).sessions().cloned().collect::<Vec<_>>() {
this._subscriptions
.push(cx.subscribe(&session, Self::on_debug_session_event));
}
}
this.end_selection(window, cx);
this.scroll_manager.show_scrollbars(window, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
@@ -4188,6 +4269,17 @@ impl Editor {
}
}
pub fn toggle_inline_values(
&mut self,
_: &ToggleInlineValues,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
self.refresh_inline_values(cx);
}
pub fn toggle_inlay_hints(
&mut self,
_: &ToggleInlayHints,
@@ -4204,6 +4296,10 @@ impl Editor {
self.inlay_hint_cache.enabled
}
pub fn inline_values_enabled(&self) -> bool {
self.inline_value_cache.enabled
}
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
if self.semantics_provider.is_none() || !self.mode.is_full() {
return;
@@ -11827,6 +11923,7 @@ impl Editor {
fn select_next_match_ranges(
this: &mut Editor,
range: Range<usize>,
reversed: bool,
replace_newest: bool,
auto_scroll: Option<Autoscroll>,
window: &mut Window,
@@ -11837,7 +11934,11 @@ impl Editor {
if replace_newest {
s.delete(s.newest_anchor().id);
}
s.insert_range(range.clone());
if reversed {
s.insert_range(range.end..range.start);
} else {
s.insert_range(range);
}
});
}
@@ -11888,6 +11989,7 @@ impl Editor {
select_next_match_ranges(
self,
next_selected_range,
last_selection.reversed,
replace_newest,
autoscroll,
window,
@@ -11946,6 +12048,7 @@ impl Editor {
select_next_match_ranges(
self,
selection.start..selection.end,
selection.reversed,
replace_newest,
autoscroll,
window,
@@ -12113,7 +12216,11 @@ impl Editor {
if action.replace_newest {
s.delete(s.newest_anchor().id);
}
s.insert_range(next_selected_range);
if last_selection.reversed {
s.insert_range(next_selected_range.end..next_selected_range.start);
} else {
s.insert_range(next_selected_range);
}
});
} else {
select_prev_state.done = true;
@@ -14703,25 +14810,17 @@ impl Editor {
}
self.dismiss_diagnostics(cx);
let snapshot = self.snapshot(window, cx);
let Some(diagnostic_renderer) = cx
.try_global::<GlobalDiagnosticRenderer>()
.map(|g| g.0.clone())
else {
let buffer = self.buffer.read(cx).snapshot(cx);
let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else {
return;
};
let buffer = self.buffer.read(cx).snapshot(cx);
let diagnostic_group = buffer
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
.collect::<Vec<_>>();
let blocks = diagnostic_renderer.render_group(
diagnostic_group,
buffer_id,
snapshot,
cx.weak_entity(),
cx,
);
let blocks =
renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx);
let blocks = self.display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(blocks, cx).into_iter().collect()
@@ -16175,11 +16274,21 @@ impl Editor {
cx.notify();
}
pub fn disable_scrolling(&mut self, cx: &mut Context<Self>) {
self.disable_scrolling = true;
cx.notify();
}
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context<Self>) {
self.show_line_numbers = Some(show_line_numbers);
cx.notify();
}
pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context<Self>) {
self.disable_expand_excerpt_buttons = true;
cx.notify();
}
pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
self.show_git_diff_gutter = Some(show_git_diff_gutter);
cx.notify();
@@ -16315,34 +16424,33 @@ impl Editor {
maybe!({
let breakpoint_store = self.breakpoint_store.as_ref()?;
let Some((_, _, active_position)) =
breakpoint_store.read(cx).active_position().cloned()
let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned()
else {
self.clear_row_highlights::<DebugCurrentRowHighlight>();
return None;
};
let position = active_stack_frame.position;
let buffer_id = position.buffer_id?;
let snapshot = self
.project
.as_ref()?
.read(cx)
.buffer_for_id(active_position.buffer_id?, cx)?
.buffer_for_id(buffer_id, cx)?
.read(cx)
.snapshot();
let mut handled = false;
for (id, ExcerptRange { context, .. }) in self
.buffer
.read(cx)
.excerpts_for_buffer(active_position.buffer_id?, cx)
for (id, ExcerptRange { context, .. }) in
self.buffer.read(cx).excerpts_for_buffer(buffer_id, cx)
{
if context.start.cmp(&active_position, &snapshot).is_ge()
|| context.end.cmp(&active_position, &snapshot).is_lt()
if context.start.cmp(&position, &snapshot).is_ge()
|| context.end.cmp(&position, &snapshot).is_lt()
{
continue;
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let multibuffer_anchor = snapshot.anchor_in_excerpt(id, active_position)?;
let multibuffer_anchor = snapshot.anchor_in_excerpt(id, position)?;
handled = true;
self.clear_row_highlights::<DebugCurrentRowHighlight>();
@@ -16355,6 +16463,7 @@ impl Editor {
cx.notify();
}
handled.then_some(())
})
.is_some()
@@ -17346,6 +17455,87 @@ impl Editor {
cx.notify();
}
fn on_debug_session_event(
&mut self,
_session: Entity<Session>,
event: &SessionEvent,
cx: &mut Context<Self>,
) {
match event {
SessionEvent::InvalidateInlineValue => {
self.refresh_inline_values(cx);
}
_ => {}
}
}
fn refresh_inline_values(&mut self, cx: &mut Context<Self>) {
let Some(project) = self.project.clone() else {
return;
};
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
return;
};
if !self.inline_value_cache.enabled {
let inlays = std::mem::take(&mut self.inline_value_cache.inlays);
self.splice_inlays(&inlays, Vec::new(), cx);
return;
}
let current_execution_position = self
.highlighted_rows
.get(&TypeId::of::<DebugCurrentRowHighlight>())
.and_then(|lines| lines.last().map(|line| line.range.start));
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
let snapshot = editor
.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
.ok()?;
let inline_values = editor
.update(cx, |_, cx| {
let Some(current_execution_position) = current_execution_position else {
return Some(Task::ready(Ok(Vec::new())));
};
// todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text
// anchor is in the same buffer
let range =
buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor;
project.inline_values(buffer, range, cx)
})
.ok()
.flatten()?
.await
.context("refreshing debugger inlays")
.log_err()?;
let (excerpt_id, buffer_id) = snapshot
.excerpts()
.next()
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
editor
.update(cx, |editor, cx| {
let new_inlays = inline_values
.into_iter()
.map(|debugger_value| {
Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position),
debugger_value.text(),
)
})
.collect::<Vec<_>>();
let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);
editor.splice_inlays(&inlay_ids, new_inlays, cx);
})
.ok()?;
Some(())
});
}
fn on_buffer_event(
&mut self,
multibuffer: &Entity<MultiBuffer>,
@@ -18881,6 +19071,13 @@ pub trait SemanticsProvider {
cx: &mut App,
) -> Option<Task<Vec<project::Hover>>>;
fn inline_values(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
fn inlay_hints(
&self,
buffer_handle: Entity<Buffer>,
@@ -19338,13 +19535,33 @@ impl SemanticsProvider for Entity<Project> {
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
// TODO: make this work for remote projects
self.update(cx, |this, cx| {
self.update(cx, |project, cx| {
if project
.active_debug_session(cx)
.is_some_and(|(session, _)| session.read(cx).any_stopped_thread())
{
return true;
}
buffer.update(cx, |buffer, cx| {
this.any_language_server_supports_inlay_hints(buffer, cx)
project.any_language_server_supports_inlay_hints(buffer, cx)
})
})
}
fn inline_values(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {
self.update(cx, |project, cx| {
let (session, active_stack_frame) = project.active_debug_session(cx)?;
Some(project.inline_values(session, active_stack_frame, buffer_handle, range, cx))
})
}
fn inlay_hints(
&self,
buffer_handle: Entity<Buffer>,

View File

@@ -2,7 +2,7 @@ use gpui::App;
use language::CursorShape;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, VsCodeSettings};
#[derive(Deserialize, Clone)]
pub struct EditorSettings {
@@ -388,7 +388,7 @@ pub struct ToolbarContent {
}
/// Scrollbar related settings
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
pub struct ScrollbarContent {
/// When to show the scrollbar in the editor.
///
@@ -423,7 +423,7 @@ pub struct ScrollbarContent {
}
/// Forcefully enable or disable the scrollbar for each axis
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
pub struct ScrollbarAxesContent {
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
///
@@ -475,4 +475,164 @@ impl Settings for EditorSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) {
vscode.enum_setting(
"editor.cursorBlinking",
&mut current.cursor_blink,
|s| match s {
"blink" | "phase" | "expand" | "smooth" => Some(true),
"solid" => Some(false),
_ => None,
},
);
vscode.enum_setting(
"editor.cursorStyle",
&mut current.cursor_shape,
|s| match s {
"block" => Some(CursorShape::Block),
"block-outline" => Some(CursorShape::Hollow),
"line" | "line-thin" => Some(CursorShape::Bar),
"underline" | "underline-thin" => Some(CursorShape::Underline),
_ => None,
},
);
vscode.enum_setting(
"editor.renderLineHighlight",
&mut current.current_line_highlight,
|s| match s {
"gutter" => Some(CurrentLineHighlight::Gutter),
"line" => Some(CurrentLineHighlight::Line),
"all" => Some(CurrentLineHighlight::All),
_ => None,
},
);
vscode.bool_setting(
"editor.selectionHighlight",
&mut current.selection_highlight,
);
vscode.bool_setting("editor.hover.enabled", &mut current.hover_popover_enabled);
vscode.u64_setting("editor.hover.delay", &mut current.hover_popover_delay);
let mut gutter = GutterContent::default();
vscode.enum_setting(
"editor.showFoldingControls",
&mut gutter.folds,
|s| match s {
"always" | "mouseover" => Some(true),
"never" => Some(false),
_ => None,
},
);
vscode.enum_setting(
"editor.lineNumbers",
&mut gutter.line_numbers,
|s| match s {
"on" | "relative" => Some(true),
"off" => Some(false),
_ => None,
},
);
if let Some(old_gutter) = current.gutter.as_mut() {
if gutter.folds.is_some() {
old_gutter.folds = gutter.folds
}
if gutter.line_numbers.is_some() {
old_gutter.line_numbers = gutter.line_numbers
}
} else {
if gutter != GutterContent::default() {
current.gutter = Some(gutter)
}
}
if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") {
current.scroll_beyond_last_line = Some(if b {
ScrollBeyondLastLine::OnePage
} else {
ScrollBeyondLastLine::Off
})
}
let mut scrollbar_axes = ScrollbarAxesContent::default();
vscode.enum_setting(
"editor.scrollbar.horizontal",
&mut scrollbar_axes.horizontal,
|s| match s {
"auto" | "visible" => Some(true),
"hidden" => Some(false),
_ => None,
},
);
vscode.enum_setting(
"editor.scrollbar.vertical",
&mut scrollbar_axes.horizontal,
|s| match s {
"auto" | "visible" => Some(true),
"hidden" => Some(false),
_ => None,
},
);
if scrollbar_axes != ScrollbarAxesContent::default() {
let scrollbar_settings = current.scrollbar.get_or_insert_default();
let axes_settings = scrollbar_settings.axes.get_or_insert_default();
if let Some(vertical) = scrollbar_axes.vertical {
axes_settings.vertical = Some(vertical);
}
if let Some(horizontal) = scrollbar_axes.horizontal {
axes_settings.horizontal = Some(horizontal);
}
}
// TODO: check if this does the int->float conversion?
vscode.f32_setting(
"editor.cursorSurroundingLines",
&mut current.vertical_scroll_margin,
);
vscode.f32_setting(
"editor.mouseWheelScrollSensitivity",
&mut current.scroll_sensitivity,
);
if Some("relative") == vscode.read_string("editor.lineNumbers") {
current.relative_line_numbers = Some(true);
}
vscode.enum_setting(
"editor.find.seedSearchStringFromSelection",
&mut current.seed_search_query_from_cursor,
|s| match s {
"always" => Some(SeedQuerySetting::Always),
"selection" => Some(SeedQuerySetting::Selection),
"never" => Some(SeedQuerySetting::Never),
_ => None,
},
);
vscode.bool_setting("search.smartCase", &mut current.use_smartcase_search);
vscode.enum_setting(
"editor.multiCursorModifier",
&mut current.multi_cursor_modifier,
|s| match s {
"ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl),
"alt" => Some(MultiCursorModifier::Alt),
_ => None,
},
);
vscode.bool_setting(
"editor.parameterHints.enabled",
&mut current.auto_signature_help,
);
vscode.bool_setting(
"editor.parameterHints.enabled",
&mut current.show_signature_help_after_edits,
);
if let Some(use_ignored) = vscode.read_bool("search.useIgnoreFiles") {
let search = current.search.get_or_insert_default();
search.include_ignored = use_ignored;
}
}
}

View File

@@ -5825,6 +5825,13 @@ async fn test_select_next(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
// Test selection direction should be preserved
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
.unwrap();
cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
}
#[gpui::test]
@@ -6011,6 +6018,25 @@ let «fooˇ» = 2;
let foo = 2;
let foo = «2ˇ»;"#,
);
// Test last selection direction should be preserved
cx.set_state(
r#"let foo = 2;
let foo = 2;
let «fooˇ» = 2;
let «ˇfoo» = 2;
let foo = 2;"#,
);
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
.unwrap();
cx.assert_editor_state(
r#"let foo = 2;
let foo = 2;
let «fooˇ» = 2;
let «ˇfoo» = 2;
let «ˇfoo» = 2;"#,
);
}
#[gpui::test]
@@ -6138,25 +6164,26 @@ async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
// selection direction is preserved
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
}
#[gpui::test]
@@ -10419,6 +10446,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
},
multi_buffer.clone(),
Some(project.clone()),
@@ -12827,46 +12855,6 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
}
#[gpui::test]
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
fn func(abˇc def: i32) -> u32 {
}
"});
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
..Default::default()
}],
},
&[],
cx,
)
})
}).unwrap();
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
hover_popover::hover(editor, &Default::default(), window, cx)
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
}
#[gpui::test]
async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -2184,6 +2184,10 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
if self.editor.read(cx).disable_expand_excerpt_buttons {
return vec![];
}
let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
let scroll_top = scroll_position.y * line_height;
@@ -5537,7 +5541,9 @@ impl EditorElement {
}
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
self.paint_scroll_wheel_listener(layout, window, cx);
if !self.editor.read(cx).disable_scrolling {
self.paint_scroll_wheel_listener(layout, window, cx);
}
window.on_mouse_event({
let position_map = layout.position_map.clone();
@@ -6588,10 +6594,21 @@ impl Element for EditorElement {
},
)
}
EditorMode::Full { .. } => {
EditorMode::Full {
sized_by_content, ..
} => {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
if sized_by_content {
let snapshot = editor.snapshot(window, cx);
let line_height =
self.style.text.line_height_in_pixels(window.rem_size());
let scroll_height =
(snapshot.max_point().row().next_row().0 as f32) * line_height;
style.size.height = scroll_height.into();
} else {
style.size.height = relative(1.).into();
}
window.request_layout(style, None, cx)
}
};

View File

@@ -71,7 +71,7 @@ pub enum HoverLink {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InlayHighlight {
pub struct InlayHighlight {
pub inlay: InlayId,
pub inlay_position: Anchor,
pub range: Range<usize>,
@@ -1280,6 +1280,7 @@ mod tests {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_value_hints: false,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
show_type_hints: true,

View File

@@ -1,6 +1,6 @@
use crate::{
ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings,
EditorSnapshot, Hover,
EditorSnapshot, GlobalDiagnosticRenderer, Hover,
display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
hover_links::{InlayHighlight, RangeInEditor},
scroll::{Autoscroll, ScrollAmount},
@@ -15,7 +15,7 @@ use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use multi_buffer::{MultiOrSingleBufferOffsetRange, ToOffset};
use multi_buffer::{MultiOrSingleBufferOffsetRange, ToOffset, ToPoint};
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
use std::{borrow::Cow, cell::RefCell};
@@ -81,7 +81,7 @@ pub fn show_keyboard_hover(
.as_ref()
.and_then(|d| {
if *d.keyboard_grace.borrow() {
d.anchor
Some(d.anchor)
} else {
None
}
@@ -283,6 +283,7 @@ fn show_hover(
None
};
let renderer = GlobalDiagnosticRenderer::global(cx);
let task = cx.spawn_in(window, async move |this, cx| {
async move {
// If we need to delay, delay a set amount initially before making the lsp request
@@ -313,28 +314,35 @@ fn show_hover(
} else {
snapshot
.buffer_snapshot
.diagnostics_in_range::<usize>(offset..offset)
.filter(|diagnostic| Some(diagnostic.diagnostic.group_id) != active_group_id)
.diagnostics_with_buffer_ids_in_range::<usize>(offset..offset)
.filter(|(_, diagnostic)| {
Some(diagnostic.diagnostic.group_id) != active_group_id
})
// Find the entry with the most specific range
.min_by_key(|entry| entry.range.len())
.min_by_key(|(_, entry)| entry.range.len())
};
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
let text = match local_diagnostic.diagnostic.source {
Some(ref source) => {
format!("{source}: {}", local_diagnostic.diagnostic.message)
}
None => local_diagnostic.diagnostic.message.clone(),
};
let local_diagnostic = DiagnosticEntry {
diagnostic: local_diagnostic.diagnostic,
range: snapshot
.buffer_snapshot
.anchor_before(local_diagnostic.range.start)
..snapshot
.buffer_snapshot
.anchor_after(local_diagnostic.range.end),
};
let diagnostic_popover = if let Some((buffer_id, local_diagnostic)) = local_diagnostic {
let group = snapshot
.buffer_snapshot
.diagnostic_group(buffer_id, local_diagnostic.diagnostic.group_id)
.collect::<Vec<_>>();
let point_range = local_diagnostic
.range
.start
.to_point(&snapshot.buffer_snapshot)
..local_diagnostic
.range
.end
.to_point(&snapshot.buffer_snapshot);
let markdown = cx.update(|_, cx| {
renderer
.as_ref()
.and_then(|renderer| {
renderer.render_hover(group, point_range, buffer_id, cx)
})
.ok_or_else(|| anyhow::anyhow!("no rendered diagnostic"))
})??;
let (background_color, border_color) = cx.update(|_, cx| {
let status_colors = cx.theme().status();
@@ -359,28 +367,26 @@ fn show_hover(
}
})?;
let parsed_content = cx
.new(|cx| Markdown::new_text(SharedString::new(text), cx))
.ok();
let subscription =
this.update(cx, |_, cx| cx.observe(&markdown, |_, _, cx| cx.notify()))?;
let subscription = this
.update(cx, |_, cx| {
if let Some(parsed_content) = &parsed_content {
Some(cx.observe(parsed_content, |_, _, cx| cx.notify()))
} else {
None
}
})
.ok()
.flatten();
let local_diagnostic = DiagnosticEntry {
diagnostic: local_diagnostic.diagnostic,
range: snapshot
.buffer_snapshot
.anchor_before(local_diagnostic.range.start)
..snapshot
.buffer_snapshot
.anchor_after(local_diagnostic.range.end),
};
Some(DiagnosticPopover {
local_diagnostic,
parsed_content,
markdown,
border_color,
background_color,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
anchor,
_subscription: subscription,
})
} else {
@@ -719,7 +725,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
#[derive(Default)]
pub struct HoverState {
pub(crate) info_popovers: Vec<InfoPopover>,
pub info_popovers: Vec<InfoPopover>,
pub diagnostic_popover: Option<DiagnosticPopover>,
pub triggered_from: Option<Anchor>,
pub info_task: Option<Task<Option<()>>>,
@@ -789,23 +795,25 @@ impl HoverState {
}
}
if let Some(diagnostic_popover) = &self.diagnostic_popover {
if let Some(markdown_view) = &diagnostic_popover.parsed_content {
if markdown_view.focus_handle(cx).is_focused(window) {
hover_popover_is_focused = true;
}
if diagnostic_popover
.markdown
.focus_handle(cx)
.is_focused(window)
{
hover_popover_is_focused = true;
}
}
hover_popover_is_focused
}
}
pub(crate) struct InfoPopover {
pub(crate) symbol_range: RangeInEditor,
pub(crate) parsed_content: Option<Entity<Markdown>>,
pub(crate) scroll_handle: ScrollHandle,
pub(crate) scrollbar_state: ScrollbarState,
pub(crate) keyboard_grace: Rc<RefCell<bool>>,
pub(crate) anchor: Option<Anchor>,
pub struct InfoPopover {
pub symbol_range: RangeInEditor,
pub parsed_content: Option<Entity<Markdown>>,
pub scroll_handle: ScrollHandle,
pub scrollbar_state: ScrollbarState,
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>,
_subscription: Option<Subscription>,
}
@@ -897,12 +905,12 @@ impl InfoPopover {
pub struct DiagnosticPopover {
pub(crate) local_diagnostic: DiagnosticEntry<Anchor>,
parsed_content: Option<Entity<Markdown>>,
markdown: Entity<Markdown>,
border_color: Hsla,
background_color: Hsla,
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>,
_subscription: Option<Subscription>,
pub anchor: Anchor,
_subscription: Subscription,
}
impl DiagnosticPopover {
@@ -913,6 +921,7 @@ impl DiagnosticPopover {
cx: &mut Context<Editor>,
) -> AnyElement {
let keyboard_grace = Rc::clone(&self.keyboard_grace);
let this = cx.entity().downgrade();
div()
.id("diagnostic")
.block()
@@ -935,51 +944,29 @@ impl DiagnosticPopover {
*keyboard_grace = false;
cx.stop_propagation();
})
.when_some(self.parsed_content.clone(), |this, markdown| {
this.child(
div()
.py_1()
.px_2()
.child(
MarkdownElement::new(markdown, {
let settings = ThemeSettings::get_global(cx);
let mut base_text_style = window.text_style();
base_text_style.refine(&TextStyleRefinement {
font_family: Some(settings.ui_font.family.clone()),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_size: Some(settings.ui_font_size(cx).into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(gpui::transparent_black()),
..Default::default()
});
MarkdownStyle {
base_text_style,
selection_background_color: {
cx.theme().players().local().selection
},
link: TextStyleRefinement {
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(cx.theme().colors().editor_foreground),
wavy: false,
}),
..Default::default()
},
..Default::default()
}
})
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
border: false,
})
.on_url_click(open_markdown_url),
.child(
div()
.py_1()
.px_2()
.child(
MarkdownElement::new(
self.markdown.clone(),
hover_markdown_style(window, cx),
)
.bg(self.background_color)
.border_1()
.border_color(self.border_color)
.rounded_lg(),
)
})
.on_url_click(move |link, window, cx| {
if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) {
this.update(cx, |this, cx| {
renderer.as_ref().open_link(this, link, window, cx);
})
.ok();
}
}),
)
.bg(self.background_color)
.border_1()
.border_color(self.border_color)
.rounded_lg(),
)
.into_any_element()
}
}
@@ -998,8 +985,7 @@ mod tests {
use collections::BTreeSet;
use gpui::App;
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet, language_settings::InlayHintSettings};
use lsp::LanguageServerId;
use language::language_settings::InlayHintSettings;
use markdown::parser::MarkdownEvent;
use smol::stream::StreamExt;
use std::sync::atomic;
@@ -1484,76 +1470,6 @@ mod tests {
});
}
#[gpui::test]
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
// Hover with just diagnostic, pops DiagnosticPopover immediately and then
// info popover once request completes
cx.set_state(indoc! {"
fn teˇst() { println!(); }
"});
// Send diagnostic to client
let range = cx.text_anchor_range(indoc! {"
fn «test»() { println!(); }
"});
cx.update_buffer(|buffer, cx| {
let snapshot = buffer.text_snapshot();
let set = DiagnosticSet::from_sorted_entries(
vec![DiagnosticEntry {
range,
diagnostic: Diagnostic {
message: "A test diagnostic message.".to_string(),
..Default::default()
},
}],
&snapshot,
);
buffer.update_diagnostics(LanguageServerId(0), set, cx);
});
// Hover pops diagnostic immediately
cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _, _| {
assert!(
hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
)
});
// Info Popover shows after request responded to
let range = cx.lsp_range(indoc! {"
fn «test»() { println!(); }
"});
cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "some new docs".to_string(),
}),
range: Some(range),
}))
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _, _| {
hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
});
}
#[gpui::test]
// https://github.com/zed-industries/zed/issues/15498
async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
@@ -1614,6 +1530,7 @@ mod tests {
async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,

View File

@@ -989,6 +989,7 @@ fn fetch_and_update_hints(
}
let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
if !editor.registered_buffers.contains_key(&query.buffer_id) {
if let Some(project) = editor.project.as_ref() {
project.update(cx, |project, cx| {
@@ -999,6 +1000,7 @@ fn fetch_and_update_hints(
})
}
}
editor
.semantics_provider
.as_ref()?
@@ -1324,6 +1326,7 @@ pub mod tests {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -1430,6 +1433,7 @@ pub mod tests {
async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -1535,6 +1539,7 @@ pub mod tests {
async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -1760,6 +1765,7 @@ pub mod tests {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -1919,6 +1925,7 @@ pub mod tests {
] {
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -1962,6 +1969,7 @@ pub mod tests {
let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: false,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -2017,6 +2025,7 @@ pub mod tests {
let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -2090,6 +2099,7 @@ pub mod tests {
async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -2222,6 +2232,7 @@ pub mod tests {
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -2521,6 +2532,7 @@ pub mod tests {
async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -2829,6 +2841,7 @@ pub mod tests {
async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -3005,6 +3018,7 @@ pub mod tests {
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -3037,6 +3051,7 @@ pub mod tests {
async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -3129,6 +3144,7 @@ pub mod tests {
async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: false,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -3205,6 +3221,7 @@ pub mod tests {
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,
@@ -3265,6 +3282,7 @@ pub mod tests {
async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,

View File

@@ -455,6 +455,15 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
self.0.inlay_hints(buffer, range, cx)
}
fn inline_values(
&self,
_: Entity<Buffer>,
_: Range<text::Anchor>,
_: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
None
}
fn resolve_inlay_hint(
&self,
hint: project::InlayHint,

View File

@@ -11,6 +11,7 @@ assistant_tool.workspace = true
assistant_tools.workspace = true
async-trait.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
clap.workspace = true
client.workspace = true

View File

@@ -1,6 +1,7 @@
use std::{
error::Error,
fmt::{self, Debug},
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
@@ -12,6 +13,8 @@ use crate::{
use agent::ThreadEvent;
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use buffer_diff::DiffHunkStatus;
use collections::HashMap;
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
use gpui::{AppContext, AsyncApp, Entity};
use language_model::{LanguageModel, Role, StopReason};
@@ -234,9 +237,9 @@ impl ExampleContext {
let mut tool_metrics = tool_metrics.lock().unwrap();
if let Some(tool_result) = thread.tool_result(&tool_use_id) {
let message = if tool_result.is_error {
format!("TOOL FAILED: {}", tool_use.name)
format!("✖︎ {}", tool_use.name)
} else {
format!("TOOL FINISHED: {}", tool_use.name)
format!("✔︎ {}", tool_use.name)
};
println!("{log_prefix}{message}");
tool_metrics
@@ -278,7 +281,7 @@ impl ExampleContext {
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
thread.set_remaining_turns(iterations);
thread.send_to_model(model, cx);
thread.send_to_model(model, None, cx);
thread.messages().len()
})?;
@@ -320,6 +323,36 @@ impl ExampleContext {
Ok(response)
}
pub fn edits(&self) -> HashMap<Arc<Path>, FileEdits> {
self.app
.read_entity(&self.agent_thread, |thread, cx| {
let action_log = thread.action_log().read(cx);
HashMap::from_iter(action_log.changed_buffers(cx).into_iter().map(
|(buffer, diff)| {
let snapshot = buffer.read(cx).snapshot();
let file = snapshot.file().unwrap();
let diff = diff.read(cx);
let base_text = diff.base_text().text();
let hunks = diff
.hunks(&snapshot, cx)
.map(|hunk| FileEditHunk {
base_text: base_text[hunk.diff_base_byte_range.clone()].to_string(),
text: snapshot
.text_for_range(hunk.range.clone())
.collect::<String>(),
status: hunk.status(),
})
.collect();
(file.path().clone(), FileEdits { hunks })
},
))
})
.unwrap()
}
}
#[derive(Debug)]
@@ -344,6 +377,10 @@ impl Response {
});
cx.assert_some(result, format!("called `{}`", tool_name))
}
pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUse> {
self.messages.iter().flat_map(|msg| &msg.tool_use)
}
}
#[derive(Debug)]
@@ -355,17 +392,37 @@ pub struct Message {
#[derive(Debug)]
pub struct ToolUse {
name: String,
pub name: String,
value: serde_json::Value,
}
impl ToolUse {
pub fn expect_input<Input>(&self, cx: &mut ExampleContext) -> Result<Input>
pub fn parse_input<Input>(&self) -> Result<Input>
where
Input: for<'de> serde::Deserialize<'de>,
{
let result =
serde_json::from_value::<Input>(self.value.clone()).map_err(|err| anyhow!(err));
cx.log_assertion(result, format!("valid `{}` input", &self.name))
serde_json::from_value::<Input>(self.value.clone()).map_err(|err| anyhow!(err))
}
}
#[derive(Debug)]
pub struct FileEdits {
hunks: Vec<FileEditHunk>,
}
#[derive(Debug)]
struct FileEditHunk {
base_text: String,
text: String,
status: DiffHunkStatus,
}
impl FileEdits {
pub fn has_added_line(&self, line: &str) -> bool {
self.hunks.iter().any(|hunk| {
hunk.status == DiffHunkStatus::added_none()
&& hunk.base_text.is_empty()
&& hunk.text.contains(line)
})
}
}

View File

@@ -0,0 +1,147 @@
use std::{collections::HashSet, path::Path};
use anyhow::Result;
use assistant_tools::{CreateFileToolInput, EditFileToolInput, ReadFileToolInput};
use async_trait::async_trait;
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion, LanguageServer};
pub struct AddArgToTraitMethod;
#[async_trait(?Send)]
impl Example for AddArgToTraitMethod {
fn meta(&self) -> ExampleMetadata {
ExampleMetadata {
name: "add_arg_to_trait_method".to_string(),
url: "https://github.com/zed-industries/zed.git".to_string(),
revision: "f69aeb6311dde3c0b8979c293d019d66498d54f2".to_string(),
language_server: Some(LanguageServer {
file_extension: "rs".to_string(),
allow_preexisting_diagnostics: false,
}),
max_assertions: None,
}
}
async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
const FILENAME: &str = "assistant_tool.rs";
cx.push_user_message(format!(
r#"
Add a `window: Option<gpui::AnyWindowHandle>` argument to the `Tool::run` trait method in {FILENAME},
and update all the implementations of the trait and call sites accordingly.
"#
));
let response = cx.run_to_end().await?;
// Reads files before it edits them
let mut read_files = HashSet::new();
for tool_use in response.tool_uses() {
match tool_use.name.as_str() {
"read_file" => {
if let Ok(input) = tool_use.parse_input::<ReadFileToolInput>() {
read_files.insert(input.path);
}
}
"create_file" => {
if let Ok(input) = tool_use.parse_input::<CreateFileToolInput>() {
read_files.insert(input.path);
}
}
"edit_file" => {
if let Ok(input) = tool_use.parse_input::<EditFileToolInput>() {
cx.assert(
read_files.contains(input.path.to_str().unwrap()),
format!(
"Read before edit: {}",
&input.path.file_stem().unwrap().to_str().unwrap()
),
)
.ok();
}
}
_ => {}
}
}
// Adds ignored argument to all but `batch_tool`
let add_ignored_window_paths = &[
"code_action_tool",
"code_symbols_tool",
"contents_tool",
"copy_path_tool",
"create_directory_tool",
"create_file_tool",
"delete_path_tool",
"diagnostics_tool",
"edit_file_tool",
"fetch_tool",
"grep_tool",
"list_directory_tool",
"move_path_tool",
"now_tool",
"open_tool",
"path_search_tool",
"read_file_tool",
"rename_tool",
"symbol_info_tool",
"terminal_tool",
"thinking_tool",
"web_search_tool",
];
let edits = cx.edits();
for tool_name in add_ignored_window_paths {
let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name);
let edits = edits.get(Path::new(&path_str));
let ignored = edits.map_or(false, |edits| {
edits.has_added_line(" _window: Option<gpui::AnyWindowHandle>,\n")
});
let uningored = edits.map_or(false, |edits| {
edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n")
});
cx.assert(ignored || uningored, format!("Argument: {}", tool_name))
.ok();
cx.assert(ignored, format!("`_` prefix: {}", tool_name))
.ok();
}
// Adds unignored argument to `batch_tool`
let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs"));
cx.assert(
batch_tool_edits.map_or(false, |edits| {
edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n")
}),
"Argument: batch_tool",
)
.ok();
Ok(())
}
fn diff_assertions(&self) -> Vec<JudgeAssertion> {
vec![
JudgeAssertion {
id: "batch tool passes window to each".to_string(),
description:
"batch_tool is modified to pass a clone of the window to each tool it calls."
.to_string(),
},
JudgeAssertion {
id: "tool tests updated".to_string(),
description:
"tool tests are updated to pass the new `window` argument (`None` is ok)."
.to_string(),
},
]
}
}

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use assistant_tools::PathSearchToolInput;
use assistant_tools::FindPathToolInput;
use async_trait::async_trait;
use regex::Regex;
@@ -15,7 +15,7 @@ impl Example for FileSearchExample {
url: "https://github.com/zed-industries/zed.git".to_string(),
revision: "03ecb88fe30794873f191ddb728f597935b3101c".to_string(),
language_server: None,
max_assertions: Some(4),
max_assertions: Some(3),
}
}
@@ -32,21 +32,18 @@ impl Example for FileSearchExample {
));
let response = cx.run_turn().await?;
let tool_use = response.expect_tool("path_search", cx)?;
let input = tool_use.expect_input::<PathSearchToolInput>(cx)?;
let tool_use = response.expect_tool("find_path", cx)?;
let input = tool_use.parse_input::<FindPathToolInput>()?;
let glob = input.glob;
cx.assert(
glob.ends_with(FILENAME),
format!("glob ends with `{FILENAME}`"),
)?;
cx.assert(glob.ends_with(FILENAME), "glob ends with file name")?;
let without_filename = glob.replace(FILENAME, "");
let matches = Regex::new("(\\*\\*|zed)/(\\*\\*?/)?")
.unwrap()
.is_match(&without_filename);
cx.assert(matches, "glob starts with either `**` or `zed`")?;
cx.assert(matches, "glob starts with `**` or project")?;
Ok(())
}

View File

@@ -11,10 +11,16 @@ use util::serde::default_true;
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
mod add_arg_to_trait_method;
mod file_search;
mod no_empty_messages;
pub fn all(examples_dir: &Path) -> Vec<Rc<dyn Example>> {
let mut threads: Vec<Rc<dyn Example>> = vec![Rc::new(file_search::FileSearchExample)];
let mut threads: Vec<Rc<dyn Example>> = vec![
Rc::new(file_search::FileSearchExample),
Rc::new(add_arg_to_trait_method::AddArgToTraitMethod),
Rc::new(no_empty_messages::NoEmptyMessagesExample),
];
for example_path in list_declarative_examples(examples_dir).unwrap() {
threads.push(Rc::new(DeclarativeExample::load(&example_path).unwrap()));

View File

@@ -0,0 +1,62 @@
use anyhow::Result;
use assistant_tools::{ListDirectoryToolInput, ReadFileToolInput};
use async_trait::async_trait;
use crate::example::{Example, ExampleContext, ExampleMetadata};
pub struct NoEmptyMessagesExample;
#[async_trait(?Send)]
impl Example for NoEmptyMessagesExample {
fn meta(&self) -> ExampleMetadata {
ExampleMetadata {
name: "no_empty_messages".to_string(),
url: "https://github.com/zed-industries/zed.git".to_string(),
revision: "fcfeea4825c563715bcd1a1af809d88a37d12ccb".to_string(),
language_server: None,
max_assertions: Some(3),
}
}
async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
cx.push_user_message(format!(
r#"
Summarize all the files in crates/assistant/src.
Do tool calls only and do NOT include a description of what you are doing.
Just give me an explanation of what you did after you finished all tool calls
"#
));
let response = cx.run_turn().await?;
dbg!(response);
// let tool_use = response.expect_tool("list_directory", cx)?;
// let input = tool_use.parse_input::<ListDirectoryToolInput>()?;
// cx.assert(
// &input.path == "zed/crates/assistant/src",
// "Path matches directory",
// )?;
for file in [
"assistant.rs",
"assistant_configuration.rs",
"assistant_panel.rs",
"inline_assistant.rs",
"slash_command_settings.rs",
"terminal_inline_assistant.rs",
] {
let response = cx.run_turn().await?;
dbg!(response);
// let tool_use = response.expect_tool("read_file", cx)?;
// let input = tool_use.parse_input::<ReadFileToolInput>()?;
// dbg!(&input);
// cx.assert(
// input.path == format!("zed/crates/assistant/src/{file}"),
// format!("Path {} matches file {file}", input.path),
// )?;
}
cx.run_to_end().await?;
Ok(())
}
}

View File

@@ -0,0 +1,53 @@
url = "https://github.com/tree-sitter/tree-sitter.git"
revision = "635c49909ce4aa7f58a9375374f91b1b434f6f9c"
language_extension = "rs"
prompt = """
Change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten.
Use `ureq` to download the SDK for the current platform and architecture.
Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir.
Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows)
that's inside of the archive.
Don't re-download the SDK if that executable already exists.
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{language_name}
Here are the available wasi-sdk assets:
- wasi-sdk-25.0-x86_64-macos.tar.gz
- wasi-sdk-25.0-arm64-macos.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-windows.tar.gz
"""
[diff_assertions]
modify_function = """
The patch modifies the `compile_parser_to_wasm` function, removing logic for running `emscripten`,
and adding logic to download `wasi-sdk`.
"""
use_listed_assets = """
The patch implements logic for selecting from the assets listed in the prompt by detecting the
current platform and architecture.
"""
add_deps = """
The patch adds a dependency for `ureq` to the Cargo.toml, and adds an import to the top of `loader/lib.rs`
If the patch uses any other dependencies (such as `tar` or `flate2`), it also correctly adds them
to the Cargo.toml and imports them.
"""
[thread_assertions]
find_specified_function = """
The agent finds the specified function `compile_parser_to_wasm` in a logical way.
It does not begin by guessing any paths to files in the codebase, but rather searches for the function by name.
"""
no_syntax_errors = """
As it edits the file, the agent never introduces syntax errors. It's ok if there are other compile errors,
but it should not introduce glaring issues like mismatched curly braces.
"""

View File

@@ -50,4 +50,10 @@ impl Settings for ExtensionSettings {
.chain(sources.server),
)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {
// settingsSync.ignoredExtensions controls autoupdate for vscode extensions, but we
// don't have a mapping to zed-extensions. there's also extensions.autoCheckUpdates
// and extensions.autoUpdate which are global switches, we don't support those yet
}
}

View File

@@ -29,6 +29,8 @@ impl Settings for FileFinderSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]

View File

@@ -81,4 +81,6 @@ impl Settings for GitHostingProviderSettings {
fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -87,4 +87,9 @@ impl Settings for GitPanelSettings {
) -> anyhow::Result<Self> {
sources.json_merge()
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.bool_setting("git.enabled", &mut current.button);
vscode.string_setting("git.defaultBranchName", &mut current.fallback_branch_name);
}
}

View File

@@ -280,10 +280,6 @@ pub(crate) enum LineIndicatorFormat {
Long,
}
/// Whether or not to automatically check for updates.
///
/// Values: short, long
/// Default: short
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
#[serde(transparent)]
pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat);
@@ -301,4 +297,6 @@ impl Settings for LineIndicatorFormat {
Ok(format.0)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -338,6 +338,7 @@ pub struct CountTokensResponse {
#[derive(Debug, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub raw_args: String,
pub args: serde_json::Value,
}

View File

@@ -33,12 +33,12 @@ use util::ResultExt;
use crate::{
Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Asset,
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke,
LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay,
PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderImage,
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId,
WindowInvalidator, current_platform, hash, init_app_menus,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel,
Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString,
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
WindowHandle, WindowId, WindowInvalidator, current_platform, hash, init_app_menus,
};
mod async_context;
@@ -1859,6 +1859,9 @@ pub struct KeystrokeEvent {
/// The action that was resolved for the keystroke, if any
pub action: Option<Box<dyn Action>>,
/// The context stack at the time
pub context_stack: Vec<KeyContext>,
}
struct NullHttpClient;

View File

@@ -15,7 +15,7 @@ use std::{
num::NonZeroU64,
sync::{
Arc, Weak,
atomic::{AtomicUsize, Ordering::SeqCst},
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
},
thread::panicking,
};
@@ -572,6 +572,30 @@ impl AnyWeakEntity {
)
}
}
/// Creates a weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
/// To hold the invariant that all ids are unique, and considering that slotmap
/// increases their IDs from `0`, we can decrease ours from `u64::MAX` so these
/// two will never conflict (u64 is way too large).
static UNIQUE_NON_CONFLICTING_ID_GENERATOR: AtomicU64 = AtomicU64::new(u64::MAX);
let entity_id = UNIQUE_NON_CONFLICTING_ID_GENERATOR.fetch_sub(1, SeqCst);
Self {
// Safety:
// Docs say this is safe but can be unspecified if slotmap changes the representation
// after `1.0.7`, that said, providing a valid entity_id here is not necessary as long
// as we guarantee that that `entity_id` is never used if `entity_ref_counts` equals
// to `Weak::new()` (that is, it's unable to upgrade), that is the invariant that
// actually needs to be hold true.
//
// And there is no sane reason to read an entity slot if `entity_ref_counts` can't be
// read in the first place, so we're good!
entity_id: entity_id.into(),
entity_type: TypeId::of::<()>(),
entity_ref_counts: Weak::new(),
}
}
}
impl std::fmt::Debug for AnyWeakEntity {
@@ -707,6 +731,14 @@ impl<T: 'static> WeakEntity<T> {
.map(|this| cx.read_entity(&this, read)),
)
}
/// Create a new weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
Self {
any_entity: AnyWeakEntity::new_invalid(),
entity_type: PhantomData,
}
}
}
impl<T> Hash for WeakEntity<T> {

View File

@@ -121,6 +121,7 @@ pub(crate) struct DispatchResult {
pub(crate) pending: SmallVec<[Keystroke; 1]>,
pub(crate) bindings: SmallVec<[KeyBinding; 1]>,
pub(crate) to_replay: SmallVec<[Replay; 1]>,
pub(crate) context_stack: Vec<KeyContext>,
}
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Window, &mut App)>;
@@ -411,15 +412,17 @@ impl DispatchTree {
&self,
input: &[Keystroke],
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let context_stack: SmallVec<[KeyContext; 4]> = dispatch_path
) -> (SmallVec<[KeyBinding; 1]>, bool, Vec<KeyContext>) {
let context_stack: Vec<KeyContext> = dispatch_path
.iter()
.filter_map(|node_id| self.node(*node_id).context.clone())
.collect();
self.keymap
let (bindings, partial) = self
.keymap
.borrow()
.bindings_for_input(input, &context_stack)
.bindings_for_input(input, &context_stack);
return (bindings, partial, context_stack);
}
/// dispatch_key processes the keystroke
@@ -436,20 +439,25 @@ impl DispatchTree {
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) -> DispatchResult {
input.push(keystroke.clone());
let (bindings, pending) = self.bindings_for_input(&input, dispatch_path);
let (bindings, pending, context_stack) = self.bindings_for_input(&input, dispatch_path);
if pending {
return DispatchResult {
pending: input,
context_stack,
..Default::default()
};
} else if !bindings.is_empty() {
return DispatchResult {
bindings,
context_stack,
..Default::default()
};
} else if input.len() == 1 {
return DispatchResult::default();
return DispatchResult {
context_stack,
..Default::default()
};
}
input.pop();
@@ -485,7 +493,7 @@ impl DispatchTree {
) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) {
let mut to_replay: SmallVec<[Replay; 1]> = Default::default();
for last in (0..input.len()).rev() {
let (bindings, _) = self.bindings_for_input(&input[0..=last], dispatch_path);
let (bindings, _, _) = self.bindings_for_input(&input[0..=last], dispatch_path);
if !bindings.is_empty() {
to_replay.push(Replay {
keystroke: input.drain(0..=last).next_back().unwrap(),

View File

@@ -1154,6 +1154,7 @@ impl Window {
&mut self,
event: &dyn Any,
action: Option<Box<dyn Action>>,
context_stack: Vec<KeyContext>,
cx: &mut App,
) {
let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() else {
@@ -1165,6 +1166,7 @@ impl Window {
&KeystrokeEvent {
keystroke: key_down_event.keystroke.clone(),
action: action.as_ref().map(|action| action.boxed_clone()),
context_stack: context_stack.clone(),
},
self,
cx,
@@ -3275,7 +3277,7 @@ impl Window {
}
let Some(keystroke) = keystroke else {
self.finish_dispatch_key_event(event, dispatch_path, cx);
self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx);
return;
};
@@ -3329,13 +3331,18 @@ impl Window {
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
if !cx.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action), cx);
self.dispatch_keystroke_observers(
event,
Some(binding.action),
match_result.context_stack.clone(),
cx,
);
self.pending_input_changed(cx);
return;
}
}
self.finish_dispatch_key_event(event, dispatch_path, cx);
self.finish_dispatch_key_event(event, dispatch_path, match_result.context_stack, cx);
self.pending_input_changed(cx);
}
@@ -3343,6 +3350,7 @@ impl Window {
&mut self,
event: &dyn Any,
dispatch_path: SmallVec<[DispatchNodeId; 32]>,
context_stack: Vec<KeyContext>,
cx: &mut App,
) {
self.dispatch_key_down_up_event(event, &dispatch_path, cx);
@@ -3355,7 +3363,7 @@ impl Window {
return;
}
self.dispatch_keystroke_observers(event, None, cx);
self.dispatch_keystroke_observers(event, None, context_stack, cx);
}
fn pending_input_changed(&mut self, cx: &mut App) {
@@ -3453,7 +3461,12 @@ impl Window {
for binding in replay.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
if !cx.propagate_event {
self.dispatch_keystroke_observers(&event, Some(binding.action), cx);
self.dispatch_keystroke_observers(
&event,
Some(binding.action),
Vec::default(),
cx,
);
continue 'replay;
}
}

View File

@@ -39,4 +39,6 @@ impl Settings for ImageViewerSettings {
.chain(sources.server),
)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -53,6 +53,8 @@ impl settings::Settings for JournalSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
pub fn init(_: Arc<AppState>, cx: &mut App) {

View File

@@ -1,12 +1,6 @@
pub use crate::{
Grammar, Language, LanguageRegistry,
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
proto,
};
use crate::{
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject,
TreeSitterOptions,
DebugVariableCapture, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
TextObject, TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{LanguageSettings, language_settings},
outline::OutlineItem,
@@ -17,6 +11,12 @@ use crate::{
task_context::RunnableRange,
text_diff::text_diff,
};
pub use crate::{
Grammar, Language, LanguageRegistry,
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
proto,
};
use anyhow::{Context as _, Result, anyhow};
use async_watch as watch;
use clock::Lamport;
@@ -73,6 +73,12 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
pub use lsp::DiagnosticSeverity;
#[derive(Debug)]
pub struct DebugVariableRanges {
pub buffer_id: BufferId,
pub range: Range<usize>,
}
/// A label for the background task spawned by the buffer to compute
/// a diff against the contents of its file.
pub static BUFFER_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
@@ -202,6 +208,7 @@ pub struct Diagnostic {
pub source: Option<String>,
/// A machine-readable code that identifies this diagnostic.
pub code: Option<NumberOrString>,
pub code_description: Option<lsp::Url>,
/// Whether this diagnostic is a hint, warning, or error.
pub severity: DiagnosticSeverity,
/// The human-readable message associated with this diagnostic.
@@ -3888,6 +3895,79 @@ impl BufferSnapshot {
})
}
pub fn debug_variable_ranges(
&self,
offset_range: Range<usize>,
) -> impl Iterator<Item = DebugVariableRanges> + '_ {
let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
grammar
.debug_variables_config
.as_ref()
.map(|config| &config.query)
});
let configs = syntax_matches
.grammars()
.iter()
.map(|grammar| grammar.debug_variables_config.as_ref())
.collect::<Vec<_>>();
iter::from_fn(move || {
loop {
let mat = syntax_matches.peek()?;
let variable_ranges = configs[mat.grammar_index].and_then(|config| {
let full_range = mat.captures.iter().fold(
Range {
start: usize::MAX,
end: 0,
},
|mut acc, next| {
let byte_range = next.node.byte_range();
if acc.start > byte_range.start {
acc.start = byte_range.start;
}
if acc.end < byte_range.end {
acc.end = byte_range.end;
}
acc
},
);
if full_range.start > full_range.end {
// We did not find a full spanning range of this match.
return None;
}
let captures = mat.captures.iter().filter_map(|capture| {
Some((
capture,
config.captures.get(capture.index as usize).cloned()?,
))
});
let mut variable_range = None;
for (query, capture) in captures {
if let DebugVariableCapture::Variable = capture {
let _ = variable_range.insert(query.node.byte_range());
}
}
Some(DebugVariableRanges {
buffer_id: self.remote_id(),
range: variable_range?,
})
});
syntax_matches.advance();
if variable_ranges.is_some() {
// It's fine for us to short-circuit on .peek()? returning None. We don't want to return None from this iter if we
// had a capture that did not contain a run marker, hence we'll just loop around for the next capture.
return variable_ranges;
}
}
})
}
pub fn runnable_ranges(
&self,
offset_range: Range<usize>,
@@ -4533,6 +4613,7 @@ impl Default for Diagnostic {
Self {
source: Default::default(),
code: None,
code_description: None,
severity: DiagnosticSeverity::ERROR,
message: Default::default(),
group_id: 0,

View File

@@ -1015,6 +1015,7 @@ pub struct Grammar {
pub(crate) brackets_config: Option<BracketsConfig>,
pub(crate) redactions_config: Option<RedactionConfig>,
pub(crate) runnable_config: Option<RunnableConfig>,
pub(crate) debug_variables_config: Option<DebugVariablesConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub text_object_config: Option<TextObjectConfig>,
@@ -1115,6 +1116,18 @@ struct RunnableConfig {
pub extra_captures: Vec<RunnableCapture>,
}
#[derive(Clone, Debug, PartialEq)]
enum DebugVariableCapture {
Named(SharedString),
Variable,
}
#[derive(Debug)]
struct DebugVariablesConfig {
pub query: Query,
pub captures: Vec<DebugVariableCapture>,
}
struct OverrideConfig {
query: Query,
values: HashMap<u32, OverrideEntry>,
@@ -1175,6 +1188,7 @@ impl Language {
override_config: None,
redactions_config: None,
runnable_config: None,
debug_variables_config: None,
error_query: Query::new(&ts_language, "(ERROR) @error").ok(),
ts_language,
highlight_map: Default::default(),
@@ -1246,6 +1260,11 @@ impl Language {
.with_text_object_query(query.as_ref())
.context("Error loading textobject query")?;
}
if let Some(query) = queries.debug_variables {
self = self
.with_debug_variables_query(query.as_ref())
.context("Error loading debug variable query")?;
}
Ok(self)
}
@@ -1341,6 +1360,25 @@ impl Language {
Ok(self)
}
pub fn with_debug_variables_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()
.ok_or_else(|| anyhow!("cannot mutate grammar"))?;
let query = Query::new(&grammar.ts_language, source)?;
let mut captures = Vec::new();
for name in query.capture_names() {
captures.push(if *name == "debug_variable" {
DebugVariableCapture::Variable
} else {
DebugVariableCapture::Named(name.to_string().into())
});
}
grammar.debug_variables_config = Some(DebugVariablesConfig { query, captures });
Ok(self)
}
pub fn with_embedding_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()

View File

@@ -214,6 +214,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
("overrides", |q| &mut q.overrides),
("redactions", |q| &mut q.redactions),
("runnables", |q| &mut q.runnables),
("debug_variables", |q| &mut q.debug_variables),
("textobjects", |q| &mut q.text_objects),
];
@@ -230,6 +231,7 @@ pub struct LanguageQueries {
pub redactions: Option<Cow<'static, str>>,
pub runnables: Option<Cow<'static, str>>,
pub text_objects: Option<Cow<'static, str>>,
pub debug_variables: Option<Cow<'static, str>>,
}
#[derive(Clone, Default)]

View File

@@ -971,6 +971,11 @@ pub struct InlayHintSettings {
/// Default: false
#[serde(default)]
pub enabled: bool,
/// Global switch to toggle inline values on and off.
///
/// Default: false
#[serde(default)]
pub show_value_hints: bool,
/// Whether type hints should be shown.
///
/// Default: true
@@ -1219,11 +1224,11 @@ impl settings::Settings for AllLanguageSettings {
let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
for (language, suffixes) in &default_value.file_types {
for (language, patterns) in &default_value.file_types {
let mut builder = GlobSetBuilder::new();
for suffix in suffixes {
builder.add(Glob::new(suffix)?);
for pattern in patterns {
builder.add(Glob::new(pattern)?);
}
file_types.insert(language.clone(), builder.build()?);
@@ -1280,20 +1285,20 @@ impl settings::Settings for AllLanguageSettings {
);
}
for (language, suffixes) in &user_settings.file_types {
for (language, patterns) in &user_settings.file_types {
let mut builder = GlobSetBuilder::new();
let default_value = default_value.file_types.get(&language.clone());
// Merge the default value with the user's value.
if let Some(suffixes) = default_value {
for suffix in suffixes {
builder.add(Glob::new(suffix)?);
if let Some(patterns) = default_value {
for pattern in patterns {
builder.add(Glob::new(pattern)?);
}
}
for suffix in suffixes {
builder.add(Glob::new(suffix)?);
for pattern in patterns {
builder.add(Glob::new(pattern)?);
}
file_types.insert(language.clone(), builder.build()?);
@@ -1370,6 +1375,120 @@ impl settings::Settings for AllLanguageSettings {
root_schema
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
let d = &mut current.defaults;
if let Some(size) = vscode
.read_value("editor.tabSize")
.and_then(|v| v.as_u64())
.and_then(|n| NonZeroU32::new(n as u32))
{
d.tab_size = Some(size);
}
if let Some(v) = vscode.read_bool("editor.insertSpaces") {
d.hard_tabs = Some(!v);
}
vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s {
"on" => Some(SoftWrap::EditorWidth),
"wordWrapColumn" => Some(SoftWrap::PreferLine),
"bounded" => Some(SoftWrap::Bounded),
"off" => Some(SoftWrap::None),
_ => None,
});
vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length);
if let Some(arr) = vscode
.read_value("editor.rulers")
.and_then(|v| v.as_array())
.map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect())
{
d.wrap_guides = arr;
}
if let Some(b) = vscode.read_bool("editor.guides.indentation") {
if let Some(guide_settings) = d.indent_guides.as_mut() {
guide_settings.enabled = b;
} else {
d.indent_guides = Some(IndentGuideSettings {
enabled: b,
..Default::default()
});
}
}
if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") {
d.format_on_save = Some(if b {
FormatOnSave::On
} else {
FormatOnSave::Off
});
}
vscode.bool_setting(
"editor.trimAutoWhitespace",
&mut d.remove_trailing_whitespace_on_save,
);
vscode.bool_setting(
"files.insertFinalNewline",
&mut d.ensure_final_newline_on_save,
);
vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
Some(match s {
"boundary" | "trailing" => ShowWhitespaceSetting::Boundary,
"selection" => ShowWhitespaceSetting::Selection,
"all" => ShowWhitespaceSetting::All,
_ => ShowWhitespaceSetting::None,
})
});
vscode.enum_setting(
"editor.autoSurround",
&mut d.use_auto_surround,
|s| match s {
"languageDefined" | "quotes" | "brackets" => Some(true),
"never" => Some(false),
_ => None,
},
);
vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format);
vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits);
vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste);
vscode.bool_setting(
"editor.suggestOnTriggerCharacters",
&mut d.show_completions_on_input,
);
if let Some(b) = vscode.read_bool("editor.suggest.showWords") {
let mode = if b {
WordsCompletionMode::Enabled
} else {
WordsCompletionMode::Disabled
};
if let Some(completion_settings) = d.completions.as_mut() {
completion_settings.words = mode;
} else {
d.completions = Some(CompletionSettings {
words: mode,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::ReplaceSuffix,
});
}
}
// TODO: pull ^ out into helper and reuse for per-language settings
// vscodes file association map is inverted from ours, so we flip the mapping before merging
let mut associations: HashMap<Arc<str>, Vec<String>> = HashMap::default();
if let Some(map) = vscode
.read_value("files.associations")
.and_then(|v| v.as_object())
{
for (k, v) in map {
let Some(v) = v.as_str() else { continue };
associations.entry(v.into()).or_default().push(k.clone());
}
}
// TODO: do we want to merge imported globs per filetype? for now we'll just replace
current.file_types.extend(associations);
}
}
fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {

View File

@@ -213,6 +213,11 @@ pub fn serialize_diagnostics<'a>(
group_id: entry.diagnostic.group_id as u64,
is_primary: entry.diagnostic.is_primary,
code: entry.diagnostic.code.as_ref().map(|s| s.to_string()),
code_description: entry
.diagnostic
.code_description
.as_ref()
.map(|s| s.to_string()),
is_disk_based: entry.diagnostic.is_disk_based,
is_unnecessary: entry.diagnostic.is_unnecessary,
data: entry.diagnostic.data.as_ref().map(|data| data.to_string()),
@@ -419,6 +424,9 @@ pub fn deserialize_diagnostics(
message: diagnostic.message,
group_id: diagnostic.group_id as usize,
code: diagnostic.code.map(lsp::NumberOrString::from_string),
code_description: diagnostic
.code_description
.and_then(|s| lsp::Url::parse(&s).ok()),
is_primary: diagnostic.is_primary,
is_disk_based: diagnostic.is_disk_based,
is_unnecessary: diagnostic.is_unnecessary,

View File

@@ -186,6 +186,7 @@ where
pub struct LanguageModelToolUse {
pub id: LanguageModelToolUseId,
pub name: Arc<str>,
pub raw_input: String,
pub input: serde_json::Value,
pub is_input_complete: bool,
}

View File

@@ -727,6 +727,7 @@ pub fn map_to_language_model_completion_events(
id: tool_use.id.clone().into(),
name: tool_use.name.clone().into(),
is_input_complete: false,
raw_input: tool_use.input_json.clone(),
input,
},
))],
@@ -757,6 +758,7 @@ pub fn map_to_language_model_completion_events(
)
.map_err(|err| anyhow!("Error parsing tool call input JSON: {err:?} - JSON string was: {input_json:?}"))?
},
raw_input: tool_use.input_json.clone(),
},
))
})],

View File

@@ -894,6 +894,7 @@ pub fn map_to_language_model_completion_events(
id: tool_use.id.into(),
name: tool_use.name.into(),
is_input_complete: true,
raw_input: tool_use.input_json.clone(),
input: if tool_use.input_json.is_empty() {
Value::Null
} else {

Some files were not shown because too many files have changed in this diff Show More