Compare commits

..

7 Commits

Author SHA1 Message Date
Max Brunsfeld
35a773c492 Use diagnostic updates in trailing user messages
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-19 12:58:21 +02:00
Agus Zubiaga
ea7fe49fb5 Do not use tag for "Using tool" marker
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-19 12:57:12 +02:00
Agus Zubiaga
7340513eee Wait for diagnostics after saving a buffer from any tool 2025-04-19 12:57:09 +02:00
Agus Zubiaga
4568ed12c3 Do not set edited_since_diagnostics_report unnecessarily 2025-04-19 12:57:05 +02:00
Agus Zubiaga
fd00f0ba73 WIP - Comparing non open path diagnostics 2025-04-19 12:57:01 +02:00
Max Brunsfeld
43c5db9583 WIP - tweak approach to reporting changed diagnostics
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-04-19 12:56:56 +02:00
Antonio Scandurra
cba4effb2d WIP: start reworking diagnostics 2025-04-19 12:56:51 +02:00
170 changed files with 2970 additions and 4246 deletions

View File

@@ -45,6 +45,5 @@
"hard_tabs": false,
"formatter": "auto",
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true,
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
"ensure_final_newline_on_save": true
}

19
Cargo.lock generated
View File

@@ -4013,7 +4013,6 @@ dependencies = [
"node_runtime",
"parking_lot",
"paths",
"proto",
"schemars",
"serde",
"serde_json",
@@ -4898,6 +4897,7 @@ dependencies = [
"client",
"collections",
"context_server",
"dap",
"dirs 5.0.1",
"env_logger 0.11.8",
"extension",
@@ -6171,7 +6171,6 @@ dependencies = [
"windows 0.61.1",
"windows-core 0.61.0",
"windows-numerics",
"windows-registry 0.5.1",
"workspace-hack",
"x11-clipboard",
"x11rb",
@@ -11743,7 +11742,6 @@ dependencies = [
"client",
"clock",
"dap",
"dap_adapters",
"env_logger 0.11.8",
"extension",
"extension_host",
@@ -11932,7 +11930,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"windows-registry 0.2.0",
"windows-registry",
]
[[package]]
@@ -14215,11 +14213,11 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"dap-types",
"futures 0.3.31",
"gpui",
"hex",
"parking_lot",
"proto",
"schemars",
"serde",
"serde_json",
@@ -17046,17 +17044,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-registry"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
dependencies = [
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
[[package]]
name = "windows-result"
version = "0.1.2"

View File

@@ -49,6 +49,15 @@
"down": "menu::SelectNext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrevious",
"right": "menu::SelectNext",
"h": "menu::SelectPrevious",
"l": "menu::SelectNext"
}
},
{
"context": "Editor",
"bindings": {
@@ -130,6 +139,24 @@
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "Editor && mode == full",
"bindings": {
@@ -178,31 +205,6 @@
"ctrl-c": "markdown::Copy"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "AssistantPanel",
"bindings": {
@@ -218,93 +220,6 @@
"ctrl-n": "assistant::NewChat"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-p": "assistant::OpenPromptLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-e": "agent::ChatMode",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "agent::Chat",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptLibrary",
"bindings": {
@@ -687,6 +602,100 @@
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-p": "assistant::OpenPromptLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-e": "agent::ChatMode",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "agent::Chat",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptEditor",
"bindings": {
@@ -695,15 +704,6 @@
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrevious",
"right": "menu::SelectNext",
"h": "menu::SelectPrevious",
"l": "menu::SelectNext"
}
},
{
"context": "ProjectSearchBar && !in_replace",
"bindings": {
@@ -920,7 +920,6 @@
"ctrl-enter": "assistant::InlineAssist",
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],

View File

@@ -1005,7 +1005,6 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],

View File

@@ -8,6 +8,16 @@ You are a highly skilled software engineer with extensive knowledge in many prog
4. NEVER lie or make things up.
5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing.
## Searching and Reading
If you are unsure about the answer to the user's request or how to satiate their request, you should gather more information.
This can be done with additional tool calls, asking clarifying questions, etc.
For example, if you've performed a semantic search, and the results may not fully answer the user's request, or merit gathering more information, feel free to call more tools. Similarly, if you've performed an edit that may partially
satiate the user's query, but you're not confident, gather more information or use more tools before ending your turn.
Bias towards not asking the user for help if you can find the answer yourself.
## Tool Use
1. Make sure to adhere to the tools schema.
@@ -16,22 +26,6 @@ You are a highly skilled software engineer with extensive knowledge in many prog
4. Use only the tools that are currently available.
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
## Searching and Reading
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
If appropriate, use tool calls to explore the current project, which contains the following root directories:
{{#each worktrees}}
- `{{root_name}}`
{{/each}}
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
- 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.
## Fixing Diagnostics
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
@@ -56,6 +50,12 @@ Otherwise, follow debugging best practices:
Operating System: {{os}}
Default Shell: {{shell}}
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
{{#each worktrees}}
- `{{root_name}}`
{{/each}}
{{#if (or has_rules has_default_user_rules)}}
## User's Custom Instructions

View File

@@ -181,6 +181,8 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// The debounce delay before querying highlights based on the selected text.
"selection_highlight_debounce": 50,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
@@ -212,7 +214,14 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -648,7 +657,7 @@
"now": true,
"path_search": true,
"read_file": true,
"grep": true,
"regex_search": true,
"thinking": true,
"web_search": true
}
@@ -672,7 +681,7 @@
"now": false,
"path_search": true,
"read_file": true,
"grep": true,
"regex_search": true,
"rename": false,
"symbol_info": false,
"terminal": true,
@@ -712,7 +721,9 @@
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
"language_servers": ["..."],
"language_servers": [
"..."
],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -908,7 +919,9 @@
// for files that are not tracked by git, but are still important to your project. Note that globs
// that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes
// precedence over these inclusions.
"file_scan_inclusions": [".env*"],
"file_scan_inclusions": [
".env*"
],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -960,7 +973,15 @@
// Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/.dev.vars",
"**/secrets.yml"
],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available.
@@ -1093,7 +1114,12 @@
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"],
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be `csh`, `fish`, `nushell` and `power_shell`
"activate_script": "default"
}
@@ -1157,8 +1183,15 @@
// }
//
"file_types": {
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"Shell Script": [".env.*"]
"JSONC": [
"**/.zed/**/*.json",
"**/zed/**/*.json",
"**/Zed/**/*.json",
"**/.vscode/**/*.json"
],
"Shell Script": [
".env.*"
]
},
// By default use a recent system version of node, or install our own.
// You can override this to use a version of node that is not in $PATH with:
@@ -1231,10 +1264,15 @@
// Different settings for specific languages.
"languages": {
"Astro": {
"language_servers": ["astro-language-server", "..."],
"language_servers": [
"astro-language-server",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-astro"]
"plugins": [
"prettier-plugin-astro"
]
}
},
"Blade": {
@@ -1270,10 +1308,19 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"Erlang": {
"language_servers": ["erlang-ls", "!elp", "..."]
"language_servers": [
"erlang-ls",
"!elp",
"..."
]
},
"Git Commit": {
"allow_rewrap": "anywhere"
@@ -1289,7 +1336,12 @@
}
},
"HEEX": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"HTML": {
"prettier": {
@@ -1299,11 +1351,17 @@
"Java": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-java"]
"plugins": [
"prettier-plugin-java"
]
}
},
"JavaScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
@@ -1321,7 +1379,10 @@
"LaTeX": {
"format_on_save": "on",
"formatter": "language_server",
"language_servers": ["texlab", "..."],
"language_servers": [
"texlab",
"..."
],
"prettier": {
"allowed": false
}
@@ -1336,10 +1397,16 @@
}
},
"PHP": {
"language_servers": ["phpactor", "!intelephense", "..."],
"language_servers": [
"phpactor",
"!intelephense",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"],
"plugins": [
"@prettier/plugin-php"
],
"parser": "php"
}
},
@@ -1347,7 +1414,12 @@
"allow_rewrap": "anywhere"
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
"language_servers": [
"solargraph",
"!ruby-lsp",
"!rubocop",
"..."
]
},
"SCSS": {
"prettier": {
@@ -1357,21 +1429,36 @@
"SQL": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-sql"]
"plugins": [
"prettier-plugin-sql"
]
}
},
"Starlark": {
"language_servers": ["starpls", "!buck2-lsp", "..."]
"language_servers": [
"starpls",
"!buck2-lsp",
"..."
]
},
"Svelte": {
"language_servers": ["svelte-language-server", "..."],
"language_servers": [
"svelte-language-server",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
"plugins": [
"prettier-plugin-svelte"
]
}
},
"TSX": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
@@ -1382,13 +1469,20 @@
}
},
"TypeScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"language_servers": [
"vue-language-server",
"..."
],
"prettier": {
"allowed": true
}
@@ -1396,7 +1490,9 @@
"XML": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-xml"]
"plugins": [
"@prettier/plugin-xml"
]
}
},
"YAML": {
@@ -1405,7 +1501,10 @@
}
},
"Zig": {
"language_servers": ["zls", "..."]
"language_servers": [
"zls",
"..."
]
}
},
// Different settings for specific language models.

View File

@@ -1,8 +1,8 @@
use crate::context::{AssistantContext, ContextId, format_context_as_string};
use crate::context_picker::MentionLink;
use crate::thread::{
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
ThreadFeedback,
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
ThreadEvent, ThreadFeedback,
};
use crate::thread_store::{RulesLoadingError, ThreadStore};
use crate::tool_use::{PendingToolUseStatus, ToolUse};
@@ -133,23 +133,18 @@ impl RenderedMessage {
}
fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
match segment {
MessageSegment::Thinking { text, .. } => {
self.segments.push(RenderedMessageSegment::Thinking {
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
scroll_handle: ScrollHandle::default(),
})
}
MessageSegment::Text(text) => {
self.segments
.push(RenderedMessageSegment::Text(parse_markdown(
text.into(),
self.language_registry.clone(),
cx,
)))
}
MessageSegment::RedactedThinking(_) => {}
let rendered_segment = match segment {
MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
scroll_handle: ScrollHandle::default(),
},
MessageSegment::Text(text) => RenderedMessageSegment::Text(parse_markdown(
text.into(),
self.language_registry.clone(),
cx,
)),
};
self.segments.push(rendered_segment);
}
}
@@ -1285,7 +1280,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, RequestKind::Chat, cx)
});
cx.notify();
}
@@ -1503,11 +1498,6 @@ impl ActiveThread {
return Empty.into_any();
}
let message_is_empty = message.should_display_content();
if message_is_empty && context.is_empty() {
return Empty.into_any();
}
let allow_editing_message = message.role == Role::User;
let edit_message_editor = self
@@ -1640,9 +1630,11 @@ impl ActiveThread {
.into_any_element(),
};
let message_has_content = !message_is_empty || !context.is_empty();
let message_is_empty = message.should_display_content();
let has_content = !message_is_empty || !context.is_empty();
let message_content =
message_has_content.then(|| {
has_content.then(|| {
v_flex()
.gap_1p5()
.when(!message_is_empty, |parent| {

View File

@@ -984,8 +984,10 @@ mod tests {
)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
// When opening the assistant diff, the cursor is positioned on the first hunk.

View File

@@ -40,7 +40,7 @@ pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, Thread, ThreadEvent};
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
pub use crate::thread_store::ThreadStore;
pub use agent_diff::{AgentDiff, AgentDiffToolbar};

View File

@@ -16,7 +16,7 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
use settings::{Settings, update_settings_file};
use ui::{
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
Switch, SwitchColor, Tooltip, prelude::*,
Switch, Tooltip, prelude::*,
};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -236,7 +236,6 @@ impl AssistantConfiguration {
"always-allow-tool-actions-switch",
always_allow_tool_actions.into(),
)
.color(SwitchColor::Accent)
.on_click({
let fs = self.fs.clone();
move |state, _window, cx| {
@@ -333,44 +332,41 @@ impl AssistantConfiguration {
),
)
.child(
Switch::new("context-server-switch", is_running.into())
.color(SwitchColor::Accent)
.on_click({
let context_server_manager =
self.context_server_manager.clone();
let context_server = context_server.clone();
move |state, _window, cx| match state {
ToggleState::Unselected
| ToggleState::Indeterminate => {
context_server_manager.update(cx, |this, cx| {
this.stop_server(context_server.clone(), cx)
.log_err();
});
}
ToggleState::Selected => {
cx.spawn({
let context_server_manager =
context_server_manager.clone();
let context_server = context_server.clone();
async move |cx| {
if let Some(start_server_task) =
context_server_manager
.update(cx, |this, cx| {
this.start_server(
context_server,
cx,
)
})
.log_err()
{
start_server_task.await.log_err();
}
}
})
.detach();
}
Switch::new("context-server-switch", is_running.into()).on_click({
let context_server_manager =
self.context_server_manager.clone();
let context_server = context_server.clone();
move |state, _window, cx| match state {
ToggleState::Unselected | ToggleState::Indeterminate => {
context_server_manager.update(cx, |this, cx| {
this.stop_server(context_server.clone(), cx)
.log_err();
});
}
}),
ToggleState::Selected => {
cx.spawn({
let context_server_manager =
context_server_manager.clone();
let context_server = context_server.clone();
async move |cx| {
if let Some(start_server_task) =
context_server_manager
.update(cx, |this, cx| {
this.start_server(
context_server,
cx,
)
})
.log_err()
{
start_server_task.await.log_err();
}
}
})
.detach();
}
}
}),
),
)
.map(|parent| {
@@ -408,7 +404,7 @@ impl AssistantConfiguration {
.gap_2()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server")
Button::new("add-context-server", "Add MCPs Directly")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()

View File

@@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use serde_json::json;
use settings::update_settings_file;
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use workspace::{ModalView, Workspace};
@@ -34,9 +34,9 @@ impl AddContextServerModal {
cx: &mut Context<Self>,
) -> Self {
let name_editor =
cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
let command_editor = cx.new(|cx| {
SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
});
Self {
@@ -46,7 +46,7 @@ impl AddContextServerModal {
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
fn confirm(&mut self, cx: &mut Context<Self>) {
let name = self
.name_editor
.read(cx)
@@ -96,7 +96,7 @@ impl AddContextServerModal {
cx.emit(DismissEvent);
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
fn cancel(&mut self, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
@@ -112,68 +112,38 @@ impl Focusable for AddContextServerModal {
impl EventEmitter<DismissEvent> for AddContextServerModal {}
impl Render for AddContextServerModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
let focus_handle = self.focus_handle(cx);
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("AddContextServerModal")
.on_action(
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
)
.on_action(
cx.listener(|this, _: &menu::Confirm, _window, cx| {
this.confirm(&menu::Confirm, cx)
}),
)
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(
Modal::new("add-context-server", None)
.header(ModalHeader::new().headline("Add MCP Server"))
.header(ModalHeader::new().headline("Add Context Server"))
.section(
Section::new().child(
v_flex()
.gap_2()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
),
Section::new()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
)
.footer(
ModalFooter::new()
.start_slot(
Button::new("cancel", "Cancel")
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _event, _window, cx| {
this.cancel(&menu::Cancel, cx)
})),
Button::new("cancel", "Cancel").on_click(
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
),
)
.end_slot(
Button::new("add-server", "Add Server")
.disabled(is_name_empty || is_command_empty)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.map(|button| {
if is_name_empty {
button.tooltip(Tooltip::text("Name is required"))
@@ -183,9 +153,9 @@ impl Render for AddContextServerModal {
button
}
})
.on_click(cx.listener(|this, _event, _window, cx| {
this.confirm(&menu::Confirm, cx)
})),
.on_click(
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
),
),
),
)

View File

@@ -1,7 +1,7 @@
use assistant_settings::AssistantSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use language_model::LanguageModelRegistry;
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
@@ -9,12 +9,17 @@ use settings::update_settings_file;
use std::sync::Arc;
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
pub use language_model_selector::ModelType;
#[derive(Clone, Copy)]
pub enum ModelType {
Default,
InlineAssistant,
}
pub struct AssistantModelSelector {
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
model_type: ModelType,
}
impl AssistantModelSelector {
@@ -58,13 +63,13 @@ impl AssistantModelSelector {
}
}
},
model_type,
window,
cx,
)
}),
menu_handle,
focus_handle,
model_type,
}
}
@@ -77,7 +82,11 @@ impl Render for AssistantModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let model = self.selector.read(cx).active_model(cx);
let model_registry = LanguageModelRegistry::read_global(cx);
let model = match self.model_type {
ModelType::Default => model_registry.default_model(),
ModelType::InlineAssistant => model_registry.inline_assistant_model(),
};
let (model_name, model_icon) = match model {
Some(model) => (model.model.name().0, Some(model.provider.icon())),
_ => (SharedString::from("No model selected"), None),

View File

@@ -47,7 +47,7 @@ use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::ui::UsageBanner;
use crate::{
AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
};
@@ -1123,16 +1123,14 @@ impl AssistantPanel {
.action("Prompt Library", Box::new(OpenPromptLibrary::default()))
.action("Settings", Box::new(OpenConfiguration))
.separator()
.header("MCPs")
.action(
"View Server Extensions",
"Install MCPs",
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
}),
)
.action("Add Custom Server", Box::new(AddContextServer))
},
))
}),

View File

@@ -1,7 +1,7 @@
use crate::context::attach_context_to_message;
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::CodegenStatus;
use anyhow::Result;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
@@ -131,12 +131,7 @@ impl BufferCodegen {
cx.notify();
}
pub fn start(
&mut self,
primary_model: Arc<dyn LanguageModel>,
user_prompt: String,
cx: &mut Context<Self>,
) -> Result<()> {
pub fn start(&mut self, user_prompt: String, cx: &mut Context<Self>) -> Result<()> {
let alternative_models = LanguageModelRegistry::read_global(cx)
.inline_alternative_models()
.to_vec();
@@ -160,6 +155,11 @@ impl BufferCodegen {
}));
}
let primary_model = LanguageModelRegistry::read_global(cx)
.default_model()
.context("no active model")?
.model;
for (model, alternative) in iter::once(primary_model)
.chain(alternative_models)
.zip(&self.alternatives)

View File

@@ -24,7 +24,6 @@ use gpui::{
WeakEntity, Window, point,
};
use language::{Buffer, Point, Selection, TransactionId};
use language_model::ConfiguredModel;
use language_model::{LanguageModelRegistry, report_assistant_event};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
@@ -1222,15 +1221,9 @@ impl InlineAssistant {
self.prompt_history.pop_front();
}
let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
else {
return;
};
assist
.codegen
.update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
.update(cx, |codegen, cx| codegen.start(user_prompt, cx))
.log_err();
}

View File

@@ -1,4 +1,4 @@
use crate::assistant_model_selector::AssistantModelSelector;
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::buffer_codegen::BufferCodegen;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
@@ -20,7 +20,7 @@ use gpui::{
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::{ModelType, ToggleModelSelector};
use language_model_selector::ToggleModelSelector;
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;

View File

@@ -34,7 +34,7 @@ use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{ContextStore, refresh_context_store_text};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{Thread, TokenUsageRatio};
use crate::thread::{RequestKind, Thread, TokenUsageRatio};
use crate::thread_store::ThreadStore;
use crate::{
AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
@@ -234,7 +234,7 @@ impl MessageEditor {
}
self.set_editor_is_expanded(false, cx);
self.send_to_model(window, cx);
self.send_to_model(RequestKind::Chat, window, cx);
cx.notify();
}
@@ -249,7 +249,12 @@ impl MessageEditor {
.is_some()
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn send_to_model(
&mut self,
request_kind: RequestKind,
window: &mut Window,
cx: &mut Context<Self>,
) {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
return;
@@ -326,7 +331,7 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, cx);
thread.send_to_model(model, request_kind, cx);
})
.log_err();
})
@@ -340,7 +345,7 @@ impl MessageEditor {
if cancelled {
self.set_editor_is_expanded(false, cx);
self.send_to_model(window, cx);
self.send_to_model(RequestKind::Chat, window, cx);
}
}

View File

@@ -1,6 +1,7 @@
use std::fmt::Write as _;
use std::io::Write;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
@@ -38,7 +39,14 @@ use crate::thread_store::{
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
SerializedToolUse, SharedProjectContext,
};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState, USING_TOOL_MARKER};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState, USING_TOOL_MARKER};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
/// Used when summarizing a thread.
Summarize,
}
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
@@ -106,21 +114,12 @@ impl Message {
self.segments.iter().all(|segment| segment.should_display())
}
pub fn push_thinking(&mut self, text: &str, signature: Option<String>) {
if let Some(MessageSegment::Thinking {
text: segment,
signature: current_signature,
}) = self.segments.last_mut()
{
if let Some(signature) = signature {
*current_signature = Some(signature);
}
pub fn push_thinking(&mut self, text: &str) {
if let Some(MessageSegment::Thinking(segment)) = self.segments.last_mut() {
segment.push_str(text);
} else {
self.segments.push(MessageSegment::Thinking {
text: text.to_string(),
signature,
});
self.segments
.push(MessageSegment::Thinking(text.to_string()));
}
}
@@ -142,12 +141,11 @@ impl Message {
for segment in &self.segments {
match segment {
MessageSegment::Text(text) => result.push_str(text),
MessageSegment::Thinking { text, .. } => {
result.push_str("<think>\n");
MessageSegment::Thinking(text) => {
result.push_str("<think>");
result.push_str(text);
result.push_str("\n</think>");
result.push_str("</think>");
}
MessageSegment::RedactedThinking(_) => {}
}
}
@@ -158,22 +156,24 @@ impl Message {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MessageSegment {
Text(String),
Thinking {
text: String,
signature: Option<String>,
},
RedactedThinking(Vec<u8>),
Thinking(String),
}
impl MessageSegment {
pub fn text_mut(&mut self) -> &mut String {
match self {
Self::Text(text) => text,
Self::Thinking(text) => text,
}
}
pub fn should_display(&self) -> bool {
// We add USING_TOOL_MARKER when making a request that includes tool uses
// without non-whitespace text around them, and this can cause the model
// to mimic the pattern, so we consider those segments not displayable.
match self {
Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
Self::Thinking { text, .. } => text.is_empty() || text.trim() == USING_TOOL_MARKER,
Self::RedactedThinking(_) => false,
Self::Thinking(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
}
}
}
@@ -356,7 +356,7 @@ impl Thread {
last_restore_checkpoint: None,
pending_checkpoint: None,
tool_use: ToolUseState::new(tools.clone()),
action_log: cx.new(|_| ActionLog::new(project.clone())),
action_log: cx.new(|cx| ActionLog::new(project.clone(), cx)),
initial_project_snapshot: {
let project_snapshot = Self::project_snapshot(project, cx);
cx.foreground_executor()
@@ -409,11 +409,8 @@ impl Thread {
.into_iter()
.map(|segment| match segment {
SerializedMessageSegment::Text { text } => MessageSegment::Text(text),
SerializedMessageSegment::Thinking { text, signature } => {
MessageSegment::Thinking { text, signature }
}
SerializedMessageSegment::RedactedThinking { data } => {
MessageSegment::RedactedThinking(data)
SerializedMessageSegment::Thinking { text } => {
MessageSegment::Thinking(text)
}
})
.collect(),
@@ -434,7 +431,7 @@ impl Thread {
prompt_builder,
tools,
tool_use,
action_log: cx.new(|_| ActionLog::new(project)),
action_log: cx.new(|cx| ActionLog::new(project, cx)),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
request_token_usage: serialized.request_token_usage,
cumulative_token_usage: serialized.cumulative_token_usage,
@@ -866,10 +863,9 @@ impl Thread {
for segment in &message.segments {
match segment {
MessageSegment::Text(content) => text.push_str(content),
MessageSegment::Thinking { text: content, .. } => {
MessageSegment::Thinking(content) => {
text.push_str(&format!("<think>{}</think>", content))
}
MessageSegment::RedactedThinking(_) => {}
}
}
text.push('\n');
@@ -899,16 +895,8 @@ impl Thread {
MessageSegment::Text(text) => {
SerializedMessageSegment::Text { text: text.clone() }
}
MessageSegment::Thinking { text, signature } => {
SerializedMessageSegment::Thinking {
text: text.clone(),
signature: signature.clone(),
}
}
MessageSegment::RedactedThinking(data) => {
SerializedMessageSegment::RedactedThinking {
data: data.clone(),
}
MessageSegment::Thinking(text) => {
SerializedMessageSegment::Thinking { text: text.clone() }
}
})
.collect(),
@@ -942,8 +930,13 @@ impl Thread {
})
}
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
let mut request = self.to_completion_request(cx);
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
request_kind: RequestKind,
cx: &mut Context<Self>,
) {
let mut request = self.to_completion_request(request_kind, cx);
if model.supports_tools() {
request.tools = {
let mut tools = Vec::new();
@@ -982,7 +975,11 @@ impl Thread {
false
}
pub fn to_completion_request(&self, cx: &mut Context<Self>) -> LanguageModelRequest {
pub fn to_completion_request(
&self,
request_kind: RequestKind,
cx: &mut Context<Self>,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: Some(self.id.to_string()),
prompt_id: Some(self.last_prompt_id.to_string()),
@@ -1029,42 +1026,34 @@ impl Thread {
cache: false,
};
self.tool_use
.attach_tool_results(message.id, &mut request_message);
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_results(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
if self.tool_use.message_has_tool_results(message.id) {
continue;
}
}
}
if !message.context.is_empty() {
if !message.segments.is_empty() {
request_message
.content
.push(MessageContent::Text(message.context.to_string()));
.push(MessageContent::Text(message.to_string()));
}
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => {
if !text.is_empty() {
request_message
.content
.push(MessageContent::Text(text.into()));
}
}
MessageSegment::Thinking { text, signature } => {
if !text.is_empty() {
request_message.content.push(MessageContent::Thinking {
text: text.into(),
signature: signature.clone(),
});
}
}
MessageSegment::RedactedThinking(data) => {
request_message
.content
.push(MessageContent::RedactedThinking(data.clone()));
}
};
}
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
}
};
request.messages.push(request_message);
}
@@ -1079,81 +1068,94 @@ impl Thread {
request
}
fn to_summarize_request(&self, added_user_message: String) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: vec![],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
};
for message in &self.messages {
let mut request_message = LanguageModelRequestMessage {
role: message.role,
content: Vec::new(),
cache: false,
};
// Skip tool results during summarization.
if self.tool_use.message_has_tool_results(message.id) {
continue;
}
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => request_message
.content
.push(MessageContent::Text(text.clone())),
MessageSegment::Thinking { .. } => {}
MessageSegment::RedactedThinking(_) => {}
}
}
if request_message.content.is_empty() {
continue;
}
request.messages.push(request_message);
}
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(added_user_message)],
cache: false,
});
request
}
fn attached_tracked_files_state(
&self,
messages: &mut Vec<LanguageModelRequestMessage>,
cx: &App,
cx: &mut App,
) {
const STALE_FILES_HEADER: &str = "These files changed since last read:";
let mut message = String::new();
let mut stale_message = String::new();
let action_log = self.action_log.read(cx);
for stale_file in action_log.stale_buffers(cx) {
let Some(file) = stale_file.read(cx).file() else {
continue;
};
if stale_message.is_empty() {
write!(&mut stale_message, "{}\n", STALE_FILES_HEADER).ok();
self.action_log.update(cx, |action_log, cx| {
let stale_files = action_log
.stale_buffers(cx)
.filter_map(|buffer| buffer.read(cx).file());
for (i, file) in stale_files.enumerate() {
if i == 0 {
writeln!(&mut message, "These files changed since last read:").ok();
}
writeln!(&mut message, "- {}", file.full_path(cx).display()).ok();
}
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
}
if let Some(diagnostic_changes) = action_log.flush_diagnostic_changes(cx) {
let project = self.project.read(cx);
writeln!(
&mut message,
"Diagnostics have changed in the following files:",
)
.ok();
for change in diagnostic_changes {
let path = change.project_path;
let Some(worktree) = project.worktree_for_id(path.worktree_id, cx) else {
continue;
};
let path = PathBuf::from(worktree.read(cx).root_name()).join(path.path);
write!(&mut message, "- {} (", path.display()).ok();
if change.fixed_diagnostic_count > 0 {
write!(&mut message, "{} fixed", change.fixed_diagnostic_count).ok();
if change.introduced_diagnostic_count > 0 {
write!(
&mut message,
", {} introduced",
change.introduced_diagnostic_count
)
.ok();
}
} else if change.introduced_diagnostic_count > 0 {
write!(
&mut message,
"{} introduced",
change.introduced_diagnostic_count
)
.ok();
}
write!(&mut message, ") ").ok();
if change.diagnostics.is_empty() {
writeln!(&mut message, "No diagnostics remaining.").ok();
} else {
writeln!(&mut message, "Remaining diagnostics:").ok();
}
for entry in change.diagnostics {
let mut lines = entry.diagnostic.message.split('\n');
writeln!(
&mut message,
" - line {}: {}",
entry.range.start.0.row + 1,
lines.next().unwrap()
)
.ok();
for line in lines {
writeln!(&mut message, " {}", line).ok();
}
}
if action_log
.last_edited_buffer()
.map_or(false, |last_edited| last_edited.consecutive_edit_count > 2)
{
writeln!(&mut message, "Because you've failed repeatedly, give up. Don't attempt to fix the diagnostics. Wait user input or continue with the next task.").ok();
writeln!(&mut message, "Don't keep trying to fix the diagnostics. Stop and wait for the user to help you fix them.").ok();
}
}
}
});
let mut content = Vec::with_capacity(2);
if !stale_message.is_empty() {
content.push(stale_message.into());
if !message.is_empty() {
content.push(message.into());
}
if !content.is_empty() {
@@ -1180,12 +1182,6 @@ impl Thread {
None
};
let prompt_id = self.last_prompt_id.clone();
let tool_use_metadata = ToolUseMetadata {
model: model.clone(),
thread_id: self.id.clone(),
prompt_id: prompt_id.clone(),
};
let task = cx.spawn(async move |thread, cx| {
let stream_completion_future = model.stream_completion_with_usage(request, &cx);
let initial_token_usage =
@@ -1253,13 +1249,10 @@ impl Thread {
};
}
}
LanguageModelCompletionEvent::Thinking {
text: chunk,
signature,
} => {
LanguageModelCompletionEvent::Thinking(chunk) => {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.push_thinking(&chunk, signature);
last_message.push_thinking(&chunk);
cx.emit(ThreadEvent::StreamedAssistantThinking(
last_message.id,
chunk,
@@ -1272,10 +1265,7 @@ impl Thread {
// will result in duplicating the text of the chunk in the rendered Markdown.
thread.insert_message(
Role::Assistant,
vec![MessageSegment::Thinking {
text: chunk.to_string(),
signature,
}],
vec![MessageSegment::Thinking(chunk.to_string())],
cx,
);
};
@@ -1294,7 +1284,6 @@ impl Thread {
thread.tool_use.request_tool_use(
last_assistant_message_id,
tool_use,
tool_use_metadata.clone(),
cx,
);
}
@@ -1315,12 +1304,7 @@ impl Thread {
.pending_completions
.retain(|completion| completion.id != pending_completion_id);
// If there is a response without tool use, summarize the message. Otherwise,
// allow two tool uses before summarizing.
if thread.summary.is_none()
&& thread.messages.len() >= 2
&& (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
{
if thread.summary.is_none() && thread.messages.len() >= 2 {
thread.summarize(cx);
}
})?;
@@ -1430,12 +1414,18 @@ impl Thread {
return;
}
let added_user_message = "Generate a concise 3-7 word title for this conversation, omitting punctuation. \
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person.";
let request = self.to_summarize_request(added_user_message.into());
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation. \
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person."
.into(),
],
cache: false,
});
self.pending_summary = cx.spawn(async move |this, cx| {
async move {
@@ -1497,14 +1487,21 @@ impl Thread {
return None;
}
let added_user_message = "Generate a detailed summary of this conversation. Include:\n\
1. A brief overview of what was discussed\n\
2. Key facts or information discovered\n\
3. Outcomes or conclusions reached\n\
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points.";
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
let request = self.to_summarize_request(added_user_message.into());
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a detailed summary of this conversation. Include:\n\
1. A brief overview of what was discussed\n\
2. Key facts or information discovered\n\
3. Outcomes or conclusions reached\n\
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points."
.into(),
],
cache: false,
});
let task = cx.spawn(async move |thread, cx| {
let stream = model.stream_completion_text(request, &cx);
@@ -1552,7 +1549,7 @@ impl Thread {
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = self.to_completion_request(cx);
let request = self.to_completion_request(RequestKind::Chat, cx);
let messages = Arc::new(request.messages);
let pending_tool_uses = self
.tool_use
@@ -1664,7 +1661,7 @@ impl Thread {
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
self.attach_tool_results(cx);
if !canceled {
self.send_to_model(model, cx);
self.send_to_model(model, RequestKind::Chat, cx);
}
}
}
@@ -1677,10 +1674,9 @@ impl Thread {
/// Insert an empty message to be populated with tool results upon send.
pub fn attach_tool_results(&mut self, cx: &mut Context<Self>) {
// Tool results are assumed to be waiting on the next message id, so they will populate
// this empty message before sending to model. Would prefer this to be more straightforward.
self.insert_message(Role::User, vec![], cx);
self.auto_capture_telemetry(cx);
// TODO: Don't insert a dummy user message here. Ensure this works with the thinking model.
// Insert a user message to contain the tool results.
self.insert_user_message("Here are the tool results.", Vec::new(), None, cx);
}
/// Cancels the last pending completion, if there are any pending.
@@ -1959,10 +1955,9 @@ impl Thread {
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => writeln!(markdown, "{}\n", text)?,
MessageSegment::Thinking { text, .. } => {
writeln!(markdown, "<think>\n{}\n</think>\n", text)?
MessageSegment::Thinking(text) => {
writeln!(markdown, "<think>{}</think>\n", text)?
}
MessageSegment::RedactedThinking(_) => {}
}
}
@@ -2289,7 +2284,9 @@ fn main() {{
assert_eq!(message.context, expected_context);
// Check message in request
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
assert_eq!(request.messages.len(), 2);
let expected_full_message = format!("{}Please explain this code", expected_context);
@@ -2379,7 +2376,9 @@ fn main() {{
assert!(message3.context.contains("file3.rs"));
// Check entire request to make sure all contexts are properly included
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
// The request should contain all 3 messages
assert_eq!(request.messages.len(), 4);
@@ -2429,7 +2428,9 @@ fn main() {{
assert_eq!(message.context, "");
// Check message in request
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
assert_eq!(request.messages.len(), 2);
assert_eq!(
@@ -2447,7 +2448,9 @@ fn main() {{
assert_eq!(message2.context, "");
// Check that both messages appear in the request
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
assert_eq!(request.messages.len(), 3);
assert_eq!(
@@ -2487,7 +2490,9 @@ fn main() {{
});
// Create a request and check that it doesn't have a stale buffer warning yet
let initial_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
let initial_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
// Make sure we don't have a stale file warning yet
let has_stale_warning = initial_request.messages.iter().any(|msg| {
@@ -2515,7 +2520,9 @@ fn main() {{
});
// Create a new request and check for the stale buffer warning
let new_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
let new_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
// We should have a stale file warning as the last message
let last_message = new_request

View File

@@ -660,18 +660,9 @@ pub struct SerializedMessage {
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
Text {
text: String,
},
Text { text: String },
#[serde(rename = "thinking")]
Thinking {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
RedactedThinking {
data: Vec<u8>,
},
Thinking { text: String },
}
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -7,13 +7,13 @@ use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, Entity, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
};
use ui::IconName;
use util::truncate_lines_to_byte_limit;
use crate::thread::{MessageId, PromptId, ThreadId};
use crate::thread::MessageId;
use crate::thread_store::SerializedMessage;
#[derive(Debug)]
@@ -27,7 +27,7 @@ pub struct ToolUse {
pub needs_confirmation: bool,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
pub const USING_TOOL_MARKER: &str = "Using tool:";
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,
@@ -36,7 +36,6 @@ pub struct ToolUseState {
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
}
impl ToolUseState {
@@ -48,7 +47,6 @@ impl ToolUseState {
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
tool_use_metadata_by_id: HashMap::default(),
}
}
@@ -256,7 +254,6 @@ impl ToolUseState {
&mut self,
assistant_message_id: MessageId,
tool_use: LanguageModelToolUse,
metadata: ToolUseMetadata,
cx: &App,
) {
self.tool_uses_by_assistant_message
@@ -264,9 +261,6 @@ impl ToolUseState {
.or_default()
.push(tool_use.clone());
self.tool_use_metadata_by_id
.insert(tool_use.id.clone(), metadata);
// The tool use is being requested by the Assistant, so we want to
// attach the tool results to the next user message.
let next_user_message_id = MessageId(assistant_message_id.0 + 1);
@@ -333,21 +327,7 @@ impl ToolUseState {
output: Result<String>,
cx: &App,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
telemetry::event!(
"Agent Tool Finished",
model = metadata
.as_ref()
.map(|metadata| metadata.model.telemetry_id()),
model_provider = metadata
.as_ref()
.map(|metadata| metadata.model.provider_id().to_string()),
thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
tool_name,
success = output.is_ok()
);
telemetry::event!("Agent Tool Finished", tool_name, success = output.is_ok());
match output {
Ok(tool_result) => {
@@ -516,10 +496,3 @@ impl PendingToolUseStatus {
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
}
}
#[derive(Clone)]
pub struct ToolUseMetadata {
pub model: Arc<dyn LanguageModel>,
pub thread_id: ThreadId,
pub prompt_id: PromptId,
}

View File

@@ -74,10 +74,6 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Self {
Self::Claude3_5Haiku
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
@@ -511,15 +507,6 @@ pub enum RequestContent {
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "thinking")]
Thinking {
thinking: String,
signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "image")]
Image {
source: ImageSource,

View File

@@ -37,7 +37,7 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role, report_assistant_event,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, LspAction, ProjectTransaction};
@@ -1766,7 +1766,6 @@ impl PromptEditor {
move |settings, _| settings.set_model(model.clone()),
);
},
ModelType::Default,
window,
cx,
)

View File

@@ -19,7 +19,7 @@ use language_model::{
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
Role, report_assistant_event,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use prompt_store::PromptBuilder;
use settings::{Settings, update_settings_file};
use std::{
@@ -755,7 +755,6 @@ impl PromptEditor {
move |settings, _| settings.set_model(model.clone()),
);
},
ModelType::Default,
window,
cx,
)

View File

@@ -2373,7 +2373,7 @@ impl AssistantContext {
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
LanguageModelCompletionEvent::Thinking { text: chunk, .. } => {
LanguageModelCompletionEvent::Thinking(chunk) => {
if thought_process_stack.is_empty() {
let start =
buffer.anchor_before(message_old_end_offset);

View File

@@ -39,7 +39,7 @@ use language_model::{
Role,
};
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType, ToggleModelSelector,
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use multi_buffer::MultiBufferRow;
use picker::Picker;
@@ -298,7 +298,6 @@ impl ContextEditor {
move |settings, _| settings.set_model(model.clone()),
);
},
ModelType::Default,
window,
cx,
)

View File

@@ -22,6 +22,7 @@ gpui.workspace = true
icons.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
parking_lot.workspace = true
project.workspace = true
serde.workspace = true

View File

@@ -1,42 +1,123 @@
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
use collections::BTreeMap;
use futures::{StreamExt, channel::mpsc};
use collections::{BTreeMap, HashMap, HashSet};
use futures::{
FutureExt as _, StreamExt,
channel::{mpsc, oneshot},
};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
use language::{Anchor, Buffer, BufferEvent, DiagnosticEntry, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, ProjectPath, lsp_store::OpenLspBufferHandle};
use std::{
cmp::{self, Ordering},
ops::Range,
sync::Arc,
time::Duration,
};
use text::{BufferId, Edit, Patch, PointUtf16, Rope, Unclipped};
use util::RangeExt;
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
/// Buffers that we want to notify the model about when they change.
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
/// Has the model edited a file since it last checked diagnostics?
edited_since_project_diagnostics_check: bool,
paths_with_pre_existing_diagnostics: HashSet<ProjectPath>,
edited_since_diagnostics_report: bool,
diagnostic_state: DiagnosticState,
last_edited_buffer: Option<LastEditedBuffer>,
/// The project this action log is associated with
project: Entity<Project>,
_project_subscription: Subscription,
}
#[derive(Clone)]
pub struct LastEditedBuffer {
pub buffer_id: BufferId,
pub consecutive_edit_count: usize,
}
impl ActionLog {
/// Creates a new, empty action log associated with the given project.
pub fn new(project: Entity<Project>) -> Self {
pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
let pre_existing_diagnostics = project.update(cx, |project, cx| {
project
.lsp_store()
.read(cx)
.diagnostic_summaries(true, cx)
.map(|(path, _, _)| path.clone())
.collect()
});
let _project_subscription = cx.subscribe(&project, |this, _, event, cx| {
if let project::Event::BufferEdited(buffer) = event {
if let Some(project_path) = buffer.read(cx).project_path(cx) {
this.paths_with_pre_existing_diagnostics
.remove(&project_path);
}
}
});
Self {
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
paths_with_pre_existing_diagnostics: pre_existing_diagnostics,
edited_since_diagnostics_report: false,
diagnostic_state: Default::default(),
project,
last_edited_buffer: None,
_project_subscription,
}
}
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
pub fn flush_diagnostic_changes(&mut self, cx: &App) -> Option<Vec<DiagnosticChange>> {
if !self.edited_since_diagnostics_report {
return None;
}
let new_state = self.diagnostic_state(cx);
let changes = new_state.compare(&self.diagnostic_state, cx);
self.diagnostic_state = new_state;
self.edited_since_diagnostics_report = false;
Some(changes).filter(|changes| !changes.is_empty())
}
/// Returns true if any files have been edited since the last project diagnostics check
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
self.edited_since_project_diagnostics_check
pub fn last_edited_buffer(&self) -> Option<LastEditedBuffer> {
self.last_edited_buffer.clone()
}
fn diagnostic_state(&self, cx: &App) -> DiagnosticState {
let mut diagnostics_for_open_buffers = HashMap::default();
let mut diagnostics_for_non_open_buffers = HashMap::default();
let project = self.project.read(cx);
let all_diagnostics = project.lsp_store().read(cx).all_diagnostics();
for (project_path, diagnostics) in all_diagnostics {
if self
.paths_with_pre_existing_diagnostics
.contains(&project_path)
{
continue;
}
match project.get_open_buffer(&project_path, cx) {
Some(buffer) => {
let diagnostics = buffer
.read(cx)
.snapshot()
.diagnostics_in_range(Anchor::MIN..Anchor::MAX, false)
.filter(|entry| entry.diagnostic.is_primary)
.collect();
diagnostics_for_open_buffers.insert(buffer, diagnostics);
}
None => {
diagnostics_for_non_open_buffers.insert(project_path.clone(), diagnostics);
}
}
}
DiagnosticState {
diagnostics_for_open_buffers,
diagnostics_for_non_open_paths: diagnostics_for_non_open_buffers,
}
}
fn track_buffer(
@@ -71,6 +152,12 @@ impl ActionLog {
status = TrackedBufferStatus::Modified;
unreviewed_changes = Patch::default();
}
if let Some(project_path) = buffer.read(cx).project_path(cx) {
self.paths_with_pre_existing_diagnostics
.remove(&project_path);
}
TrackedBuffer {
buffer: buffer.clone(),
base_text,
@@ -269,21 +356,88 @@ impl ActionLog {
self.track_buffer(buffer, false, cx);
}
/// Track a buffer as read, so we can notify the model about user edits.
pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
/// Save and track a new buffer
pub fn save_new_buffer(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.track_buffer(buffer.clone(), true, cx);
self.buffer_edited(buffer, cx)
self.save_edited_buffer(buffer, cx)
}
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
/// Save and track an edited buffer
pub fn save_edited_buffer(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.edited_since_diagnostics_report = true;
let saved_buffer_id = buffer.read(cx).remote_id();
match &mut self.last_edited_buffer {
Some(LastEditedBuffer {
buffer_id,
consecutive_edit_count,
}) if *buffer_id == saved_buffer_id => *consecutive_edit_count += 1,
_ => {
self.last_edited_buffer = Some(LastEditedBuffer {
buffer_id: saved_buffer_id,
consecutive_edit_count: 1,
})
}
}
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;
}
tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
let project = self.project.clone();
cx.spawn(async move |_this, cx| {
let (tx, mut rx) = oneshot::channel();
let mut tx = Some(tx);
let _subscription = cx.subscribe(&project, move |_, event, _| match event {
project::Event::DiskBasedDiagnosticsFinished { .. } => {
if let Some(tx) = tx.take() {
tx.send(()).ok();
}
}
_ => {}
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
let has_lang_server = project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
buffer.update(cx, |buffer, cx| {
lsp_store
.language_servers_for_local_buffer(buffer, cx)
.next()
.is_some()
})
})
})?;
if has_lang_server {
let timeout = cx.background_executor().timer(Duration::from_secs(30));
futures::select! {
_ = rx => Ok(()),
_ = timeout.fuse() => {
log::info!("Did not receive diagnostics update 30s after agent edit");
// We don't want to fail the tool here
Ok(())
}
}
} else {
Ok(())
}
})
}
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
@@ -497,6 +651,149 @@ impl ActionLog {
}
}
#[derive(Default)]
struct DiagnosticState {
diagnostics_for_open_buffers: HashMap<Entity<Buffer>, Vec<DiagnosticEntry<Anchor>>>,
diagnostics_for_non_open_paths:
HashMap<ProjectPath, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct DiagnosticChange {
pub project_path: ProjectPath,
pub fixed_diagnostic_count: usize,
pub introduced_diagnostic_count: usize,
pub diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
}
impl DiagnosticState {
fn compare(&self, old_state: &Self, cx: &App) -> Vec<DiagnosticChange> {
let mut changes = Vec::new();
let empty = Vec::new();
for (buffer, new) in &self.diagnostics_for_open_buffers {
let old = old_state
.diagnostics_for_open_buffers
.get(&buffer)
.unwrap_or(&empty);
let buffer = buffer.read(cx);
let Some(project_path) = buffer.project_path(cx) else {
continue;
};
let (introduced, fixed) = Self::compare_diagnostics(new, old, |a, b| a.cmp(b, buffer));
if introduced > 0 || fixed > 0 {
changes.push(DiagnosticChange {
project_path,
fixed_diagnostic_count: fixed,
introduced_diagnostic_count: introduced,
diagnostics: new.into_iter().map(|entry| entry.resolve(buffer)).collect(),
});
}
}
for (buffer, old) in &old_state.diagnostics_for_open_buffers {
if !self.diagnostics_for_open_buffers.contains_key(&buffer) && old.len() > 0 {
let buffer = buffer.read(cx);
let Some(project_path) = buffer.project_path(cx) else {
continue;
};
changes.push(DiagnosticChange {
project_path,
fixed_diagnostic_count: old.len(),
introduced_diagnostic_count: 0,
diagnostics: vec![],
});
}
}
let empty = Vec::new();
for (project_path, new) in &self.diagnostics_for_non_open_paths {
let old = old_state
.diagnostics_for_non_open_paths
.get(&project_path)
.unwrap_or(&empty);
let (introduced, fixed) = Self::compare_diagnostics(new, old, |a, b| a.cmp(b));
if introduced > 0 || fixed > 0 {
changes.push(DiagnosticChange {
project_path: project_path.clone(),
fixed_diagnostic_count: fixed,
introduced_diagnostic_count: introduced,
diagnostics: new.clone(),
});
}
}
for (project_path, old) in &old_state.diagnostics_for_non_open_paths {
if !self
.diagnostics_for_non_open_paths
.contains_key(&project_path)
&& old.len() > 0
{
changes.push(DiagnosticChange {
project_path: project_path.clone(),
fixed_diagnostic_count: old.len(),
introduced_diagnostic_count: 0,
diagnostics: vec![],
});
}
}
changes
}
fn compare_diagnostics<T>(
new: &[DiagnosticEntry<T>],
old: &[DiagnosticEntry<T>],
cmp: impl Fn(&DiagnosticEntry<T>, &DiagnosticEntry<T>) -> Ordering,
) -> (usize, usize) {
let mut introduced = 0;
let mut fixed = 0;
let mut old_iter = old.iter().peekable();
let mut new_iter = new.iter().peekable();
loop {
match (old_iter.peek(), new_iter.peek()) {
(Some(old_entry), Some(new_entry)) => {
match cmp(old_entry, new_entry) {
Ordering::Less => {
// Old entry comes first and isn't in new - it's fixed
fixed += 1;
old_iter.next();
}
Ordering::Greater => {
// New entry comes first and isn't in old - it's introduced
introduced += 1;
new_iter.next();
}
Ordering::Equal => {
// They're the same - just advance both iterators
old_iter.next();
new_iter.next();
}
}
}
(Some(_), None) => {
// Only old entries left - they're all fixed
old_iter.next();
fixed += 1;
}
(None, Some(_)) => {
// Only new entries left - they're all introduced
new_iter.next();
introduced += 1;
}
(None, None) => break,
}
}
(introduced, fixed)
}
}
fn apply_non_conflicting_edits(
patch: &Patch<u32>,
edits: Vec<Edit<u32>>,
@@ -667,7 +964,7 @@ mod tests {
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
use language::Point;
use language::{Diagnostic, LanguageServerId, Point};
use project::{FakeFs, Fs, Project, RemoveOptions};
use rand::prelude::*;
use serde_json::json;
@@ -696,7 +993,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
cx.update(|cx| {
@@ -711,8 +1008,10 @@ mod tests {
.edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -766,7 +1065,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
cx.update(|cx| {
@@ -783,8 +1082,10 @@ mod tests {
.unwrap();
buffer.finalize_last_transaction();
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -840,7 +1141,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
cx.update(|cx| {
@@ -850,8 +1151,10 @@ mod tests {
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -929,7 +1232,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({})).await;
@@ -946,12 +1249,10 @@ mod tests {
.unwrap();
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
action_log.update(cx, |log, cx| log.save_new_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
@@ -1005,7 +1306,7 @@ mod tests {
.read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
.unwrap();
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer1 = project
.update(cx, |project, cx| {
project.open_buffer(file1_path.clone(), cx)
@@ -1068,9 +1369,8 @@ mod tests {
.await
.unwrap();
buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
project
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
action_log
.update(cx, |log, cx| log.save_new_buffer(buffer2.clone(), cx))
.await
.unwrap();
@@ -1103,7 +1403,7 @@ mod tests {
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1124,8 +1424,11 @@ mod tests {
.edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1238,7 +1541,7 @@ mod tests {
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1259,8 +1562,10 @@ mod tests {
.edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1314,7 +1619,7 @@ mod tests {
fs.insert_tree(path!("/dir"), json!({"file": "content"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1369,7 +1674,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| {
project.find_project_path("dir/new_file", cx)
@@ -1382,8 +1687,10 @@ mod tests {
.unwrap();
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_new_buffer(buffer.clone(), cx))
})
.await
.unwrap();
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
@@ -1417,6 +1724,177 @@ mod tests {
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test]
async fn test_track_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
json!({
"src": {
"one.rs": "fn one(a: B) -> C { d }",
"two.rs": "fn two(e: F) { G::H }",
"three.rs": "fn three() -> { i(); }",
}
}),
)
.await;
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let diagnostics_1 = vec![language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 11)),
diagnostic: Diagnostic {
message: "pre-existing error 1".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
}];
let diagnostics_2 = vec![language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 18))..Unclipped(PointUtf16::new(0, 19)),
diagnostic: Diagnostic {
message: "pre-existing error 2".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
}];
project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/one.rs".into(),
None,
diagnostics_1.clone(),
cx,
)
.unwrap();
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/two.rs".into(),
None,
diagnostics_2.clone(),
cx,
)
.unwrap();
});
});
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/src/one.rs", cx)
})
.await
.unwrap();
let worktree_id = buffer.read_with(cx, |buffer, cx| buffer.file().unwrap().worktree_id(cx));
action_log.update(cx, |action_log, cx| {
action_log.track_buffer(buffer.clone(), false, cx);
});
let diagnostic_changes = action_log
.update(cx, |action_log, cx| action_log.flush_diagnostic_changes(cx))
.unwrap();
assert_eq!(
diagnostic_changes,
vec![DiagnosticChange {
project_path: (worktree_id, "src/one.rs").into(),
fixed_diagnostic_count: 0,
introduced_diagnostic_count: 1,
diagnostics: diagnostics_1
},]
);
let save_task = action_log.update(cx, |action_log, cx| {
action_log.save_edited_buffer(buffer.clone(), cx)
});
let diagnostics_1 = vec![
language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 11)),
diagnostic: Diagnostic {
message: "pre-existing error 1".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
},
language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 20))..Unclipped(PointUtf16::new(0, 21)),
diagnostic: Diagnostic {
message: "new error".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
},
];
let diagnostics_3 = vec![language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 0)),
diagnostic: Diagnostic {
message: "new error 3".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
}];
project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/one.rs".into(),
None,
diagnostics_1.clone(),
cx,
)
.unwrap();
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/three.rs".into(),
None,
diagnostics_3.clone(),
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
});
});
save_task.await.unwrap();
// The diagnostics in the file `two.rs` existed are pre-existing, and
// that file has not been edited, so they are not included.
let diagnostic_changes = action_log
.update(cx, |action_log, cx| action_log.flush_diagnostic_changes(cx))
.unwrap();
assert_eq!(
diagnostic_changes,
vec![
DiagnosticChange {
project_path: (worktree_id, "src/one.rs").into(),
fixed_diagnostic_count: 0,
introduced_diagnostic_count: 1,
diagnostics: diagnostics_1
},
DiagnosticChange {
project_path: (worktree_id, "src/three.rs").into(),
fixed_diagnostic_count: 0,
introduced_diagnostic_count: 1,
diagnostics: diagnostics_3
}
]
);
}
#[gpui::test(iterations = 100)]
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
init_test(cx);
@@ -1429,7 +1907,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({"file": text})).await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1469,9 +1947,14 @@ mod tests {
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
if is_agent_change {
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
action_log
.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
} else {
Task::ready(Ok(()))
}
});
})
.await
.unwrap();
}
}

View File

@@ -9,13 +9,13 @@ mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_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 regex_search_tool;
mod rename_tool;
mod replace;
mod schema;
@@ -44,12 +44,12 @@ use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_file_tool::EditFileTool;
use crate::fetch_tool::FetchTool;
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::regex_search_tool::RegexSearchTool;
use crate::rename_tool::RenameTool;
use crate::symbol_info_tool::SymbolInfoTool;
use crate::terminal_tool::TerminalTool;
@@ -77,7 +77,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(ContentsTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(GrepTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(RenameTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));

View File

@@ -43,7 +43,7 @@ pub struct BatchToolInput {
/// }
/// },
/// {
/// "name": "grep",
/// "name": "regex_search",
/// "input": {
/// "regex": "fn run\\("
/// }
@@ -91,7 +91,7 @@ pub struct BatchToolInput {
/// {
/// "invocations": [
/// {
/// "name": "grep",
/// "name": "regex_search",
/// "input": {
/// "regex": "impl Database"
/// }

View File

@@ -241,13 +241,10 @@ impl Tool for CodeActionTool {
format!("Completed code action: {}", title)
};
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
log.save_edited_buffer(buffer.clone(), cx)
})?.await?;
Ok(response)
} else {

View File

@@ -97,14 +97,11 @@ impl Tool for CreateFileTool {
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
action_log.update(cx, |action_log, cx| {
action_log.will_create_buffer(buffer.clone(), cx)
});
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
action_log.save_new_buffer(buffer.clone(), cx)
})
})?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}"))
})

View File

@@ -81,7 +81,7 @@ impl Tool for DiagnosticsTool {
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
match serde_json::from_value::<DiagnosticsToolInput>(input)
@@ -152,10 +152,6 @@ impl Tool for DiagnosticsTool {
}
}
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
});
if has_diagnostics {
Task::ready(Ok(output)).into()
} else {

View File

@@ -1,11 +1,14 @@
use crate::{replace::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 language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
@@ -160,24 +163,22 @@ impl Tool for EditFileTool {
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
snapshot
})?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
action_log.update(cx, |log, cx| {
log.save_edited_buffer(buffer.clone(), cx)
})?.await?;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
let diff_str = cx.background_spawn({
let snapshot = snapshot.clone();
async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}
}).await;
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
}).into()
}
}

View File

@@ -1,424 +0,0 @@
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 language::OffsetRangeExt;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
Project,
search::{SearchQuery, SearchResult},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
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.
pub regex: String,
/// A glob pattern for the paths of files to include in the search.
/// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts".
/// If omitted, all files in the project will be searched.
pub include_pattern: Option<String>,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: u32,
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
#[serde(default)]
pub case_sensitive: bool,
}
impl GrepToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 20;
pub struct GrepTool;
impl Tool for GrepTool {
fn name(&self) -> String {
"grep".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./grep_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Regex
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<GrepToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<GrepToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
let regex_str = MarkdownString::inline_code(&input.regex);
let case_info = if input.case_sensitive {
" (case-sensitive)"
} else {
""
};
if page > 1 {
format!("Get page {page} of search results for regex {regex_str}{case_info}")
} else {
format!("Search files for regex {regex_str}{case_info}")
}
}
Err(_) => "Search with regex".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
let input = match serde_json::from_value::<GrepToolInput>(input) {
Ok(input) => input,
Err(error) => {
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
}
};
let include_matcher = match PathMatcher::new(
input
.include_pattern
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
}
};
let query = match SearchQuery::regex(
&input.regex,
false,
input.case_sensitive,
false,
false,
include_matcher,
PathMatcher::default(), // For now, keep it simple and don't enable an exclude pattern.
true, // Always match file include pattern against *full project paths* that start with a project root.
None,
) {
Ok(query) => query,
Err(error) => return Task::ready(Err(error)).into(),
};
let results = project.update(cx, |project, cx| project.search(query, cx));
cx.spawn(async move|cx| {
futures::pin_mut!(results);
let mut output = String::new();
let mut skips_remaining = input.offset;
let mut matches_found = 0;
let mut has_more_matches = false;
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
if ranges.is_empty() {
continue;
}
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
let mut file_header_written = false;
let mut ranges = ranges
.into_iter()
.map(|range| {
let mut point_range = range.to_point(buffer);
point_range.start.row =
point_range.start.row.saturating_sub(CONTEXT_LINES);
point_range.start.column = 0;
point_range.end.row = cmp::min(
buffer.max_point().row,
point_range.end.row + CONTEXT_LINES,
);
point_range.end.column = buffer.line_len(point_range.end.row);
point_range
})
.peekable();
while let Some(mut range) = ranges.next() {
if skips_remaining > 0 {
skips_remaining -= 1;
continue;
}
// We'd already found a full page of matches, and we just found one more.
if matches_found >= RESULTS_PER_PAGE {
has_more_matches = true;
return Ok(());
}
while let Some(next_range) = ranges.peek() {
if range.end.row >= next_range.start.row {
range.end = next_range.end;
ranges.next();
} else {
break;
}
}
if !file_header_written {
writeln!(output, "\n## Matches in {}", path.display())?;
file_header_written = true;
}
let start_line = range.start.row + 1;
let end_line = range.end.row + 1;
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
output.extend(buffer.text_for_range(range));
output.push_str("\n```\n");
matches_found += 1;
}
}
Ok(())
})??;
}
if matches_found == 0 {
Ok("No matches found".to_string())
} else if has_more_matches {
Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
input.offset + 1,
input.offset + matches_found,
input.offset + RESULTS_PER_PAGE,
))
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
}).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_tool::Tool;
use gpui::{AppContext, TestAppContext};
use project::{FakeFs, Project};
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
serde_json::json!({
"src": {
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
"utils": {
"helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}",
},
},
"tests": {
"test_main.rs": "fn test_main() {\n assert!(true);\n}",
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
// Test with include pattern for Rust files inside the root of the project
let input = serde_json::to_value(GrepToolInput {
regex: "println".to_string(),
include_pattern: Some("root/**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(result.contains("main.rs"), "Should find matches in main.rs");
assert!(
result.contains("helper.rs"),
"Should find matches in helper.rs"
);
assert!(
!result.contains("test_main.rs"),
"Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)"
);
// Test with include pattern for src directory only
let input = serde_json::to_value(GrepToolInput {
regex: "fn".to_string(),
include_pattern: Some("root/**/src/**".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("main.rs"),
"Should find matches in src/main.rs"
);
assert!(
result.contains("helper.rs"),
"Should find matches in src/utils/helper.rs"
);
assert!(
!result.contains("test_main.rs"),
"Should not include test_main.rs as it's not in src directory"
);
// Test with empty include pattern (should default to all files)
let input = serde_json::to_value(GrepToolInput {
regex: "fn".to_string(),
include_pattern: None,
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(result.contains("main.rs"), "Should find matches in main.rs");
assert!(
result.contains("helper.rs"),
"Should find matches in helper.rs"
);
assert!(
result.contains("test_main.rs"),
"Should include test_main.rs"
);
}
#[gpui::test]
async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
serde_json::json!({
"case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
// Test case-insensitive search (default)
let input = serde_json::to_value(GrepToolInput {
regex: "uppercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("UPPERCASE"),
"Case-insensitive search should match uppercase"
);
// Test case-sensitive search
let input = serde_json::to_value(GrepToolInput {
regex: "uppercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
!result.contains("UPPERCASE"),
"Case-sensitive search should not match uppercase"
);
// Test case-sensitive search
let input = serde_json::to_value(GrepToolInput {
regex: "LOWERCASE".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
!result.contains("lowercase"),
"Case-sensitive search should match lowercase"
);
// Test case-sensitive search for lowercase pattern
let input = serde_json::to_value(GrepToolInput {
regex: "lowercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("lowercase"),
"Case-sensitive search should match lowercase text"
);
}
async fn run_grep_tool(
input: serde_json::Value,
project: Entity<Project>,
cx: &mut TestAppContext,
) -> 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));
match task.output.await {
Ok(result) => result,
Err(e) => panic!("Failed to run grep tool: {}", e),
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -1,8 +0,0 @@
Searches the contents of files in the project with a regular expression
- Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in.
- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
- Pass an `include_pattern` if you know how to narrow your search on the files system
- Never use this tool to search for paths. Only search file contents with this tool.
- Use this tool when you need to find files containing specific patterns
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.

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 `regex_search` or `path_search` tools when searching the codebase.

View File

@@ -2,6 +2,6 @@ Fast file pattern matching tool that works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted alphabetically
- Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths.
- Prefer the `regex_search` tool to this tool when searching for symbols unless you have specific information about paths.
- Use this tool when you need to find files by name patterns
- Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.

View File

@@ -186,7 +186,7 @@ mod test {
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
let input = json!({
@@ -216,7 +216,7 @@ mod test {
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
let input = json!({
@@ -245,7 +245,7 @@ mod test {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
@@ -314,7 +314,7 @@ mod test {
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
let input = json!({

View File

@@ -0,0 +1,206 @@
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 language::OffsetRangeExt;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
Project,
search::{SearchQuery, SearchResult},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RegexSearchToolInput {
/// A regex pattern to search for in the entire project. Note that the regex
/// will be parsed by the Rust `regex` crate.
pub regex: String,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: u32,
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
#[serde(default)]
pub case_sensitive: bool,
}
impl RegexSearchToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 20;
pub struct RegexSearchTool;
impl Tool for RegexSearchTool {
fn name(&self) -> String {
"regex_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./regex_search_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Regex
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<RegexSearchToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
let regex_str = MarkdownString::inline_code(&input.regex);
let case_info = if input.case_sensitive {
" (case-sensitive)"
} else {
""
};
if page > 1 {
format!("Get page {page} of search results for regex {regex_str}{case_info}")
} else {
format!("Search files for regex {regex_str}{case_info}")
}
}
Err(_) => "Search with regex".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
let (offset, regex, case_sensitive) =
match serde_json::from_value::<RegexSearchToolInput>(input) {
Ok(input) => (input.offset, input.regex, input.case_sensitive),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let query = match SearchQuery::regex(
&regex,
false,
case_sensitive,
false,
false,
PathMatcher::default(),
PathMatcher::default(),
None,
) {
Ok(query) => query,
Err(error) => return Task::ready(Err(error)).into(),
};
let results = project.update(cx, |project, cx| project.search(query, cx));
cx.spawn(async move|cx| {
futures::pin_mut!(results);
let mut output = String::new();
let mut skips_remaining = offset;
let mut matches_found = 0;
let mut has_more_matches = false;
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
if ranges.is_empty() {
continue;
}
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
let mut file_header_written = false;
let mut ranges = ranges
.into_iter()
.map(|range| {
let mut point_range = range.to_point(buffer);
point_range.start.row =
point_range.start.row.saturating_sub(CONTEXT_LINES);
point_range.start.column = 0;
point_range.end.row = cmp::min(
buffer.max_point().row,
point_range.end.row + CONTEXT_LINES,
);
point_range.end.column = buffer.line_len(point_range.end.row);
point_range
})
.peekable();
while let Some(mut range) = ranges.next() {
if skips_remaining > 0 {
skips_remaining -= 1;
continue;
}
// We'd already found a full page of matches, and we just found one more.
if matches_found >= RESULTS_PER_PAGE {
has_more_matches = true;
return Ok(());
}
while let Some(next_range) = ranges.peek() {
if range.end.row >= next_range.start.row {
range.end = next_range.end;
ranges.next();
} else {
break;
}
}
if !file_header_written {
writeln!(output, "\n## Matches in {}", path.display())?;
file_header_written = true;
}
let start_line = range.start.row + 1;
let end_line = range.end.row + 1;
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
output.extend(buffer.text_for_range(range));
output.push_str("\n```\n");
matches_found += 1;
}
}
Ok(())
})??;
}
if matches_found == 0 {
Ok("No matches found".to_string())
} else if has_more_matches {
Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
offset + 1,
offset + matches_found,
offset + RESULTS_PER_PAGE,
))
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
}).into()
}
}

View File

@@ -0,0 +1,6 @@
Searches the entire project for the given regular expression.
- Prefer this tool when searching for files containing symbols in the project.
- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
- Use this tool when you need to find files containing specific patterns
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.

View File

@@ -129,13 +129,9 @@ impl Tool for RenameTool {
})?
.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
log.save_edited_buffer(buffer.clone(), cx)
})?.await?;
Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
}).into()

View File

@@ -14,7 +14,6 @@ pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> O
true,
PathMatcher::new(iter::empty::<&str>()).ok()?,
PathMatcher::new(iter::empty::<&str>()).ok()?,
false,
None,
)
.log_err()?;
@@ -59,8 +58,10 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
let max_row = buffer.max_point().row;
'windows: for start_row in 0..max_row + 1 {
let end_row = start_row + old_lines.len().saturating_sub(1) as u32;
'windows: for start_row in 0..max_row.saturating_sub(old_lines.len() as u32 - 1) {
let mut common_leading = None;
let end_row = start_row + old_lines.len() as u32 - 1;
if end_row > max_row {
// The buffer ends before fully matching the pattern
@@ -75,14 +76,6 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
let mut window_lines = window_text.lines();
let mut old_lines_iter = old_lines.iter();
let mut common_mismatch = None;
#[derive(Eq, PartialEq)]
enum Mismatch {
OverIndented(String),
UnderIndented(String),
}
while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())
{
let line_trimmed = window_line.trim_start();
@@ -95,24 +88,18 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
continue;
}
let line_mismatch = if window_line.len() > old_line.len() {
let prefix = window_line[..window_line.len() - old_line.len()].to_string();
Mismatch::UnderIndented(prefix)
} else {
let prefix = old_line[..old_line.len() - window_line.len()].to_string();
Mismatch::OverIndented(prefix)
};
let line_leading = &window_line[..window_line.len() - old_line.len()];
match &common_mismatch {
Some(common_mismatch) if common_mismatch != &line_mismatch => {
match &common_leading {
Some(common_leading) if common_leading != line_leading => {
continue 'windows;
}
Some(_) => (),
None => common_mismatch = Some(line_mismatch),
None => common_leading = Some(line_leading.to_string()),
}
}
if let Some(common_mismatch) = &common_mismatch {
if let Some(common_leading) = common_leading {
let line_ending = buffer.line_ending();
let replacement = new_lines
.iter()
@@ -120,13 +107,7 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
if new_line.trim().is_empty() {
new_line.to_string()
} else {
match common_mismatch {
Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line,
Mismatch::OverIndented(prefix) => new_line
.strip_prefix(prefix)
.unwrap_or(new_line)
.to_string(),
}
common_leading.to_string() + new_line
}
})
.collect::<Vec<_>>()
@@ -168,123 +149,14 @@ fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
}
#[cfg(test)]
mod replace_exact_tests {
use super::*;
use gpui::TestAppContext;
use gpui::prelude::*;
#[gpui::test]
async fn basic(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
assert_eq!(result, Some("let x = 42;".to_string()));
}
#[gpui::test]
async fn no_match(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn multi_line(cx: &mut TestAppContext) {
let whole = "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}";
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
let result = test_replace_exact(cx, whole, old_text, new_text).await;
assert_eq!(
result,
Some("fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}".to_string())
);
}
#[gpui::test]
async fn multiple_occurrences(cx: &mut TestAppContext) {
let whole = "let x = 41;\nlet y = 41;\nlet z = 41;";
let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await;
assert_eq!(
result,
Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string())
);
}
#[gpui::test]
async fn empty_buffer(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn partial_match(cx: &mut TestAppContext) {
let whole = "let x = 41; let y = 42;";
let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await;
assert_eq!(result, Some("let x = 42; let y = 42;".to_string()));
}
#[gpui::test]
async fn whitespace_sensitive(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn entire_buffer(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
assert_eq!(result, Some("let x = 42;".to_string()));
}
async fn test_replace_exact(
cx: &mut TestAppContext,
whole: &str,
old: &str,
new: &str,
) -> Option<String> {
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact(old, new, &buffer_snapshot).await;
diff.map(|diff| {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
})
})
}
}
#[cfg(test)]
mod flexible_indent_tests {
mod tests {
use super::*;
use gpui::TestAppContext;
use gpui::prelude::*;
use unindent::Unindent;
#[gpui::test]
fn test_underindented_single_line(cx: &mut TestAppContext) {
let cur = " let a = 41;".to_string();
let old = " let a = 41;".to_string();
let new = " let a = 42;".to_string();
let exp = " let a = 42;".to_string();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(exp.to_string()))
}
#[gpui::test]
fn test_overindented_single_line(cx: &mut TestAppContext) {
let cur = " let a = 41;".to_string();
let old = " let a = 41;".to_string();
let new = " let a = 42;".to_string();
let exp = " let a = 42;".to_string();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(exp.to_string()))
}
#[gpui::test]
fn test_underindented_multi_line(cx: &mut TestAppContext) {
fn test_replace_consistent_indentation(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
@@ -321,33 +193,6 @@ mod flexible_indent_tests {
);
}
#[gpui::test]
fn test_overindented_multi_line(cx: &mut TestAppContext) {
let cur = r#"
fn foo() {
let a = 41;
let b = 3.13;
}
"#
.unindent();
// 6 space indent instead of 4
let old = " let a = 41;\n let b = 3.13;";
let new = " let a = 42;\n let b = 3.14;";
let expected = r#"
fn foo() {
let a = 42;
let b = 3.14;
}
"#
.unindent();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(expected.to_string()))
}
#[gpui::test]
fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {
let whole = r#"
@@ -420,6 +265,7 @@ mod flexible_indent_tests {
#[gpui::test]
fn test_replace_no_match(cx: &mut TestAppContext) {
// Test with no match
let whole = r#"
fn test() {
let x = 5;
@@ -470,71 +316,6 @@ mod flexible_indent_tests {
);
}
#[gpui::test]
fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) {
let whole = r#"
let x = 5;
"#
.unindent();
let old = r#"
let x = 5;
let y = 10;
"#
.unindent();
let new = r#"
let x = 5;
let y = 20;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_old_is_empty(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
}
"#
.unindent();
let old = "";
let new = r#"
let y = 10;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_whole_is_empty(cx: &mut TestAppContext) {
let whole = "";
let old = r#"
let x = 5;
"#
.unindent();
let new = r#"
let x = 10;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[test]
fn test_lines_with_min_indent() {
// Empty string
@@ -722,133 +503,6 @@ mod flexible_indent_tests {
);
}
#[gpui::test]
async fn test_replace_exact_basic(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
assert_eq!(diff.edits.len(), 1);
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;");
}
#[gpui::test]
async fn test_replace_exact_no_match(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_multi_line(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| {
language::Buffer::local(
"fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}",
cx,
)
});
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
let diff = replace_exact(old_text, new_text, &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(
result,
"fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}"
);
}
#[gpui::test]
async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) {
let buffer =
cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Should replace only the first occurrence
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;");
}
#[gpui::test]
async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_partial_match(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Verify substring replacement actually works
let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42; let y = 42;");
}
#[gpui::test]
async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;");
}
fn test_replace_with_flexible_indent(
cx: &mut TestAppContext,
whole: &str,

View File

@@ -84,10 +84,6 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Self {
Self::Claude3_5Haiku
}
pub fn from_id(id: &str) -> anyhow::Result<Self> {
if id.starts_with("claude-3-5-sonnet-v2") {
Ok(Self::Claude3_5SonnetV2)

View File

@@ -5091,7 +5091,6 @@ async fn test_project_search(
false,
Default::default(),
Default::default(),
false,
None,
)
.unwrap(),

View File

@@ -882,7 +882,6 @@ impl RandomizedTest for ProjectCollaborationTest {
false,
Default::default(),
Default::default(),
false,
None,
)
.unwrap(),

View File

@@ -1,7 +1,7 @@
use crate::tests::TestServer;
use call::ActiveCall;
use collections::{HashMap, HashSet};
use dap::DapRegistry;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _;
@@ -86,6 +86,7 @@ async fn test_sharing_an_ssh_remote_project(
http_client: remote_http_client,
node_runtime: node,
languages,
debug_adapters: Arc::new(DapRegistry::fake()),
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
@@ -253,6 +254,7 @@ async fn test_ssh_collaboration_git_branches(
http_client: remote_http_client,
node_runtime: node,
languages,
debug_adapters: Arc::new(DapRegistry::fake()),
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
@@ -458,6 +460,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
http_client: remote_http_client,
node_runtime: NodeRuntime::unavailable(),
languages,
debug_adapters: Arc::new(DapRegistry::fake()),
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,

View File

@@ -14,7 +14,7 @@ use client::{
use clock::FakeSystemClock;
use collab_ui::channel_view::ChannelView;
use collections::{HashMap, HashSet};
use dap::DapRegistry;
use fs::FakeFs;
use futures::{StreamExt as _, channel::oneshot};
use git::GitHostingProviderRegistry;
@@ -275,12 +275,14 @@ impl TestServer {
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let debug_adapters = Arc::new(DapRegistry::default());
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
languages: language_registry,
debug_adapters,
fs: fs.clone(),
build_window_options: |_, _| Default::default(),
node_runtime: NodeRuntime::unavailable(),
@@ -796,6 +798,7 @@ impl TestClient {
self.app_state.node_runtime.clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.debug_adapters.clone(),
self.app_state.fs.clone(),
None,
cx,

View File

@@ -37,7 +37,6 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
false,
Default::default(),
Default::default(),
false,
None,
)
.unwrap()

View File

@@ -166,9 +166,6 @@ impl ComponentPreview {
component_preview.update_component_list(cx);
let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);
window.focus(&focus_handle);
component_preview
}
@@ -782,13 +779,10 @@ impl Item for ComponentPreview {
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
self.workspace_id = workspace.database_id();
let focus_handle = self.filter_editor.read(cx).focus_handle(cx);
window.focus(&focus_handle);
}
}

View File

@@ -61,10 +61,6 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Self {
Self::Claude3_7Sonnet
}
pub fn uses_streaming(&self) -> bool {
match self {
Self::Gpt4o

View File

@@ -39,7 +39,6 @@ log.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
paths.workspace = true
proto.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -3,8 +3,7 @@ use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use collections::HashMap;
use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
use dap_types::StartDebuggingRequestArguments;
use futures::io::BufReader;
use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
@@ -14,10 +13,16 @@ use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::{self, fs::File, lock::Mutex};
use std::{
borrow::Borrow, collections::HashSet, ffi::OsStr, fmt::Debug, net::Ipv4Addr, ops::Deref,
path::PathBuf, sync::Arc,
borrow::Borrow,
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
fmt::Debug,
net::Ipv4Addr,
ops::Deref,
path::PathBuf,
sync::Arc,
};
use task::{DebugTaskDefinition, TcpArgumentsTemplate};
use task::DebugTaskDefinition;
use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -88,91 +93,17 @@ pub struct TcpArguments {
pub port: u16,
pub timeout: Option<u64>,
}
impl TcpArguments {
pub fn from_proto(proto: proto::TcpHost) -> anyhow::Result<Self> {
let host = TcpArgumentsTemplate::from_proto(proto)?;
Ok(TcpArguments {
host: host.host.ok_or_else(|| anyhow!("missing host"))?,
port: host.port.ok_or_else(|| anyhow!("missing port"))?,
timeout: host.timeout,
})
}
pub fn to_proto(&self) -> proto::TcpHost {
TcpArgumentsTemplate {
host: Some(self.host),
port: Some(self.port),
timeout: self.timeout,
}
.to_proto()
}
}
#[derive(Debug, Clone)]
pub struct DebugAdapterBinary {
pub adapter_name: DebugAdapterName,
pub command: String,
pub arguments: Vec<String>,
pub envs: HashMap<String, String>,
pub arguments: Option<Vec<OsString>>,
pub envs: Option<HashMap<String, String>>,
pub cwd: Option<PathBuf>,
pub connection: Option<TcpArguments>,
pub request_args: StartDebuggingRequestArguments,
}
impl DebugAdapterBinary {
pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
let request = match binary.launch_type() {
proto::debug_adapter_binary::LaunchType::Launch => {
StartDebuggingRequestArgumentsRequest::Launch
}
proto::debug_adapter_binary::LaunchType::Attach => {
StartDebuggingRequestArgumentsRequest::Attach
}
};
Ok(DebugAdapterBinary {
command: binary.command,
arguments: binary.arguments,
envs: binary.envs.into_iter().collect(),
connection: binary
.connection
.map(TcpArguments::from_proto)
.transpose()?,
request_args: StartDebuggingRequestArguments {
configuration: serde_json::from_str(&binary.configuration)?,
request,
},
cwd: binary.cwd.map(|cwd| cwd.into()),
})
}
pub fn to_proto(&self) -> proto::DebugAdapterBinary {
proto::DebugAdapterBinary {
command: self.command.clone(),
arguments: self.arguments.clone(),
envs: self
.envs
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
cwd: self
.cwd
.as_ref()
.map(|cwd| cwd.to_string_lossy().to_string()),
connection: self.connection.as_ref().map(|c| c.to_proto()),
launch_type: match self.request_args.request {
StartDebuggingRequestArgumentsRequest::Launch => {
proto::debug_adapter_binary::LaunchType::Launch.into()
}
StartDebuggingRequestArgumentsRequest::Attach => {
proto::debug_adapter_binary::LaunchType::Attach.into()
}
},
configuration: self.request_args.configuration.to_string(),
}
}
}
#[derive(Debug)]
pub struct AdapterVersion {
pub tag_name: String,
@@ -387,22 +318,22 @@ impl FakeAdapter {
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
use serde_json::json;
use task::DebugRequest;
use task::DebugRequestType;
let value = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
"process_id": if let DebugRequest::Attach(attach_config) = &config.request {
"process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
},
});
let request = match config.request {
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
DebugRequestType::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequestType::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
};
StartDebuggingRequestArguments {
configuration: value,
@@ -426,10 +357,11 @@ impl DebugAdapter for FakeAdapter {
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
Ok(DebugAdapterBinary {
adapter_name: Self::ADAPTER_NAME.into(),
command: "command".into(),
arguments: vec![],
arguments: None,
connection: None,
envs: HashMap::default(),
envs: None,
cwd: None,
request_args: self.request_args(config),
})

View File

@@ -1,5 +1,5 @@
use crate::{
adapters::DebugAdapterBinary,
adapters::{DebugAdapterBinary, DebugAdapterName},
transport::{IoKind, LogKind, TransportDelegate},
};
use anyhow::{Result, anyhow};
@@ -88,6 +88,7 @@ impl DebugAdapterClient {
) -> Result<Self> {
let binary = match self.transport_delegate.transport() {
crate::transport::Transport::Tcp(tcp_transport) => DebugAdapterBinary {
adapter_name: binary.adapter_name,
command: binary.command,
arguments: binary.arguments,
envs: binary.envs,
@@ -218,6 +219,9 @@ impl DebugAdapterClient {
self.id
}
pub fn name(&self) -> DebugAdapterName {
self.binary.adapter_name.clone()
}
pub fn binary(&self) -> &DebugAdapterBinary {
&self.binary
}
@@ -318,6 +322,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
adapter_name: "adapter".into(),
command: "command".into(),
arguments: Default::default(),
envs: Default::default(),
@@ -388,6 +393,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
adapter_name: "adapter".into(),
command: "command".into(),
arguments: Default::default(),
envs: Default::default(),
@@ -441,6 +447,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
adapter_name: "test-adapter".into(),
command: "command".into(),
arguments: Default::default(),
envs: Default::default(),

View File

@@ -7,7 +7,7 @@ pub mod transport;
pub use dap_types::*;
pub use registry::DapRegistry;
pub use task::DebugRequest;
pub use task::DebugRequestType;
pub type ScopeId = u64;
pub type VariableReference = u64;

View File

@@ -1,4 +1,3 @@
use gpui::{App, Global};
use parking_lot::RwLock;
use crate::adapters::{DebugAdapter, DebugAdapterName};
@@ -12,20 +11,8 @@ struct DapRegistryState {
#[derive(Clone, Default)]
/// Stores available debug adapters.
pub struct DapRegistry(Arc<RwLock<DapRegistryState>>);
impl Global for DapRegistry {}
impl DapRegistry {
pub fn global(cx: &mut App) -> &mut Self {
let ret = cx.default_global::<Self>();
#[cfg(any(test, feature = "test-support"))]
if ret.adapter(crate::FakeAdapter::ADAPTER_NAME).is_none() {
ret.add_adapter(Arc::new(crate::FakeAdapter::new()));
}
ret
}
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
let name = adapter.name();
let _previous_value = self.0.write().adapters.insert(name, adapter);
@@ -34,12 +21,19 @@ impl DapRegistry {
"Attempted to insert a new debug adapter when one is already registered"
);
}
pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> {
self.0.read().adapters.get(name).cloned()
}
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
self.0.read().adapters.keys().cloned().collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake() -> Self {
use crate::FakeAdapter;
let register = Self::default();
register.add_adapter(Arc::new(FakeAdapter::new()));
register
}
}

View File

@@ -21,7 +21,7 @@ use std::{
sync::Arc,
time::Duration,
};
use task::TcpArgumentsTemplate;
use task::TCPHost;
use util::ResultExt as _;
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
@@ -74,14 +74,16 @@ pub enum Transport {
}
impl Transport {
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
#[cfg(any(test, feature = "test-support"))]
async fn start(_: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
#[cfg(any(test, feature = "test-support"))]
if cfg!(any(test, feature = "test-support")) {
return FakeTransport::start(cx)
.await
.map(|(transports, fake)| (transports, Self::Fake(fake)));
}
return FakeTransport::start(cx)
.await
.map(|(transports, fake)| (transports, Self::Fake(fake)));
}
#[cfg(not(any(test, feature = "test-support")))]
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
if binary.connection.is_some() {
TcpTransport::start(binary, cx)
.await
@@ -518,21 +520,18 @@ pub struct TcpTransport {
impl TcpTransport {
/// Get an open port to use with the tcp client when not supplied by debug config
pub async fn port(host: &TcpArgumentsTemplate) -> Result<u16> {
pub async fn port(host: &TCPHost) -> Result<u16> {
if let Some(port) = host.port {
Ok(port)
} else {
Self::unused_port(host.host()).await
Ok(TcpListener::bind(SocketAddrV4::new(host.host(), 0))
.await?
.local_addr()?
.port())
}
}
pub async fn unused_port(host: Ipv4Addr) -> Result<u16> {
Ok(TcpListener::bind(SocketAddrV4::new(host, 0))
.await?
.local_addr()?
.port())
}
#[allow(dead_code, reason = "This is used in non test builds of Zed")]
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
let Some(connection_args) = binary.connection.as_ref() else {
return Err(anyhow!("No connection arguments provided"));
@@ -547,8 +546,13 @@ impl TcpTransport {
command.current_dir(cwd);
}
command.args(&binary.arguments);
command.envs(&binary.envs);
if let Some(args) = &binary.arguments {
command.args(args);
}
if let Some(envs) = &binary.envs {
command.envs(envs);
}
command
.stdin(Stdio::null())
@@ -631,8 +635,13 @@ impl StdioTransport {
command.current_dir(cwd);
}
command.args(&binary.arguments);
command.envs(&binary.envs);
if let Some(args) = &binary.arguments {
command.args(args);
}
if let Some(envs) = &binary.envs {
command.envs(envs);
}
command
.stdin(Stdio::piped())

View File

@@ -1,10 +1,10 @@
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use std::{path::PathBuf, sync::OnceLock};
use anyhow::{Result, bail};
use async_trait::async_trait;
use dap::adapters::latest_github_release;
use gpui::AsyncApp;
use task::{DebugRequest, DebugTaskDefinition};
use task::{DebugRequestType, DebugTaskDefinition};
use crate::*;
@@ -19,8 +19,8 @@ impl CodeLldbDebugAdapter {
fn request_args(&self, config: &DebugTaskDefinition) -> dap::StartDebuggingRequestArguments {
let mut configuration = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
});
let map = configuration.as_object_mut().unwrap();
@@ -28,10 +28,10 @@ impl CodeLldbDebugAdapter {
map.insert("name".into(), Value::String(config.label.clone()));
let request = config.request.to_dap();
match &config.request {
DebugRequest::Attach(attach) => {
DebugRequestType::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
@@ -140,13 +140,16 @@ impl DebugAdapter for CodeLldbDebugAdapter {
.ok_or_else(|| anyhow!("Adapter path is expected to be valid UTF-8"))?;
Ok(DebugAdapterBinary {
command,
cwd: None,
arguments: vec![
cwd: Some(adapter_dir),
arguments: Some(vec![
"--settings".into(),
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
],
json!({"sourceLanguages": ["cpp", "rust"]})
.to_string()
.into(),
]),
request_args: self.request_args(config),
envs: HashMap::default(),
adapter_name: "test".into(),
envs: None,
connection: None,
})
}

View File

@@ -11,7 +11,7 @@ use anyhow::{Result, anyhow};
use async_trait::async_trait;
use codelldb::CodeLldbDebugAdapter;
use dap::{
DapRegistry, DebugRequest,
DapRegistry, DebugRequestType,
adapters::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
@@ -19,26 +19,23 @@ use dap::{
};
use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
use gpui::{App, BorrowAppContext};
use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use serde_json::{Value, json};
use task::TcpArgumentsTemplate;
use task::TCPHost;
pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
registry.add_adapter(Arc::from(PythonDebugAdapter));
registry.add_adapter(Arc::from(PhpDebugAdapter));
registry.add_adapter(Arc::from(JsDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter));
})
pub fn init(registry: Arc<DapRegistry>) {
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
registry.add_adapter(Arc::from(PythonDebugAdapter));
registry.add_adapter(Arc::from(PhpDebugAdapter));
registry.add_adapter(Arc::from(JsDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter));
}
pub(crate) async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
tcp_connection: TCPHost,
) -> Result<(Ipv4Addr, u16, Option<u64>)> {
let host = tcp_connection.host();
let timeout = tcp_connection.timeout;
@@ -56,7 +53,7 @@ trait ToDap {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest;
}
impl ToDap for DebugRequest {
impl ToDap for DebugRequestType {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest {
match self {
Self::Launch(_) => dap::StartDebuggingRequestArgumentsRequest::Launch,

View File

@@ -1,10 +1,10 @@
use std::{collections::HashMap, ffi::OsStr};
use std::ffi::OsStr;
use anyhow::{Result, bail};
use async_trait::async_trait;
use dap::StartDebuggingRequestArguments;
use gpui::AsyncApp;
use task::{DebugRequest, DebugTaskDefinition};
use task::{DebugRequestType, DebugTaskDefinition};
use crate::*;
@@ -17,18 +17,18 @@ impl GdbDebugAdapter {
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
DebugRequestType::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
@@ -82,9 +82,10 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
Ok(DebugAdapterBinary {
adapter_name: Self::ADAPTER_NAME.into(),
command: gdb_path,
arguments: vec!["-i=dap".into()],
envs: HashMap::default(),
arguments: Some(vec!["-i=dap".into()]),
envs: None,
cwd: None,
connection: None,
request_args: self.request_args(config),

View File

@@ -1,6 +1,6 @@
use dap::StartDebuggingRequestArguments;
use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use std::{ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
use crate::*;
@@ -12,12 +12,12 @@ impl GoDebugAdapter {
const ADAPTER_NAME: &'static str = "Delve";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = match &config.request {
dap::DebugRequest::Attach(attach_config) => {
dap::DebugRequestType::Attach(attach_config) => {
json!({
"processId": attach_config.process_id,
})
}
dap::DebugRequest::Launch(launch_config) => json!({
dap::DebugRequestType::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args
@@ -92,14 +92,15 @@ impl DebugAdapter for GoDebugAdapter {
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
adapter_name: self.name(),
command: delve_path,
arguments: vec![
arguments: Some(vec![
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
],
format!("{}:{}", host, port).into(),
]),
cwd: None,
envs: HashMap::default(),
envs: None,
connection: Some(adapters::TcpArguments {
host,
port,

View File

@@ -1,8 +1,8 @@
use adapters::latest_github_release;
use dap::StartDebuggingRequestArguments;
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf};
use task::{DebugRequest, DebugTaskDefinition};
use std::path::PathBuf;
use task::{DebugRequestType, DebugTaskDefinition};
use crate::*;
@@ -18,16 +18,16 @@ impl JsDebugAdapter {
let mut args = json!({
"type": "pwa-node",
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
DebugRequestType::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
@@ -106,22 +106,20 @@ impl DebugAdapter for JsDebugAdapter {
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
adapter_name: self.name(),
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
port.to_string(),
host.to_string(),
],
arguments: Some(vec![
adapter_path.join(Self::ADAPTER_PATH).into(),
port.to_string().into(),
host.to_string().into(),
]),
cwd: None,
envs: HashMap::default(),
envs: None,
connection: Some(adapters::TcpArguments {
host,
port,

View File

@@ -1,7 +1,7 @@
use adapters::latest_github_release;
use dap::adapters::TcpArguments;
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf};
use std::path::PathBuf;
use task::DebugTaskDefinition;
use crate::*;
@@ -19,18 +19,20 @@ impl PhpDebugAdapter {
config: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
match &config.request {
dap::DebugRequest::Attach(_) => {
dap::DebugRequestType::Attach(_) => {
anyhow::bail!("php adapter does not support attaching")
}
dap::DebugRequest::Launch(launch_config) => Ok(dap::StartDebuggingRequestArguments {
configuration: json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
}),
request: config.request.to_dap(),
}),
dap::DebugRequestType::Launch(launch_config) => {
Ok(dap::StartDebuggingRequestArguments {
configuration: json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
}),
request: config.request.to_dap(),
})
}
}
}
}
@@ -92,26 +94,24 @@ impl DebugAdapter for PhpDebugAdapter {
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
adapter_name: self.name(),
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
format!("--server={}", port),
],
arguments: Some(vec![
adapter_path.join(Self::ADAPTER_PATH).into(),
format!("--server={}", port).into(),
]),
connection: Some(TcpArguments {
port,
host,
timeout,
}),
cwd: None,
envs: HashMap::default(),
envs: None,
request_args: self.request_args(config)?,
})
}

View File

@@ -1,7 +1,7 @@
use crate::*;
use dap::{DebugRequest, StartDebuggingRequestArguments};
use dap::{DebugRequestType, StartDebuggingRequestArguments};
use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use std::{ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
#[derive(Default)]
@@ -16,18 +16,18 @@ impl PythonDebugAdapter {
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
DebugRequestType::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
@@ -141,22 +141,20 @@ impl DebugAdapter for PythonDebugAdapter {
};
Ok(DebugAdapterBinary {
adapter_name: self.name(),
command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
arguments: vec![
debugpy_dir
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
format!("--port={}", port),
format!("--host={}", host),
],
arguments: Some(vec![
debugpy_dir.join(Self::ADAPTER_PATH).into(),
format!("--port={}", port).into(),
format!("--host={}", host).into(),
]),
connection: Some(adapters::TcpArguments {
host,
port,
timeout,
}),
cwd: None,
envs: HashMap::default(),
envs: None,
request_args: self.request_args(config),
})
}

View File

@@ -566,13 +566,11 @@ impl DapLogView {
.dap_store()
.read(cx)
.sessions()
.filter_map(|session| {
let session = session.read(cx);
session.adapter_name();
let client = session.adapter_client()?;
.filter_map(|client| {
let client = client.read(cx).adapter_client()?;
Some(DapMenuItem {
client_id: client.id(),
client_name: session.adapter_name().to_string(),
client_name: client.name().0.as_ref().into(),
has_adapter_logs: client.has_adapter_logs(),
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
})

View File

@@ -1,4 +1,4 @@
use dap::DebugRequest;
use dap::DebugRequestType;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::Subscription;
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
@@ -216,10 +216,10 @@ impl PickerDelegate for AttachModalDelegate {
};
match &mut self.debug_config.request {
DebugRequest::Attach(config) => {
DebugRequestType::Attach(config) => {
config.process_id = Some(candidate.pid);
}
DebugRequest::Launch(_) => {
DebugRequestType::Launch(_) => {
debug_panic!("Debugger attach modal used on launch debug config");
return;
}

View File

@@ -5,7 +5,7 @@ use std::{
};
use anyhow::{Result, anyhow};
use dap::{DapRegistry, DebugRequest};
use dap::DebugRequestType;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
@@ -13,7 +13,7 @@ use gpui::{
};
use project::Project;
use settings::Settings;
use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest};
use task::{DebugTaskDefinition, LaunchConfig};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@@ -37,9 +37,9 @@ pub(super) struct NewSessionModal {
last_selected_profile_name: Option<SharedString>,
}
fn suggested_label(request: &DebugRequest, debugger: &str) -> String {
fn suggested_label(request: &DebugRequestType, debugger: &str) -> String {
match request {
DebugRequest::Launch(config) => {
DebugRequestType::Launch(config) => {
let last_path_component = Path::new(&config.program)
.file_name()
.map(|name| name.to_string_lossy())
@@ -47,7 +47,7 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> String {
format!("{} ({debugger})", last_path_component)
}
DebugRequest::Attach(config) => format!(
DebugRequestType::Attach(config) => format!(
"pid: {} ({debugger})",
config.process_id.unwrap_or(u32::MAX)
),
@@ -71,7 +71,7 @@ impl NewSessionModal {
.and_then(|def| def.stop_on_entry);
let launch_config = match past_debug_definition.map(|def| def.request) {
Some(DebugRequest::Launch(launch_config)) => Some(launch_config),
Some(DebugRequestType::Launch(launch_config)) => Some(launch_config),
_ => None,
};
@@ -96,6 +96,7 @@ impl NewSessionModal {
request,
initialize_args: self.initialize_args.clone(),
tcp_connection: None,
locator: None,
stop_on_entry: match self.stop_on_entry {
ToggleState::Selected => Some(true),
_ => None,
@@ -130,16 +131,20 @@ impl NewSessionModal {
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
let task = project.update(cx, |this, cx| {
let template = DebugTaskTemplate {
locator: None,
definition: config.clone(),
};
if let Some(debug_config) = template
.to_zed_format()
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
if let Some(debug_config) =
config
.clone()
.to_zed_format()
.ok()
.and_then(|task_template| {
task_template
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| {
resolved_task.resolved_debug_adapter_config()
})
})
{
this.start_debug_session(debug_config.definition, cx)
this.start_debug_session(debug_config, cx)
} else {
this.start_debug_session(config, cx)
}
@@ -209,7 +214,12 @@ impl NewSessionModal {
};
let available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.update(cx, |this, cx| {
this.project()
.read(cx)
.debug_adapters()
.enumerate_adapters()
})
.ok()
.unwrap_or_default();
@@ -241,14 +251,14 @@ impl NewSessionModal {
this.debugger = Some(task.adapter.clone().into());
this.initialize_args = task.initialize_args.clone();
match &task.request {
DebugRequest::Launch(launch_config) => {
DebugRequestType::Launch(launch_config) => {
this.mode = NewSessionMode::launch(
Some(launch_config.clone()),
window,
cx,
);
}
DebugRequest::Attach(_) => {
DebugRequestType::Attach(_) => {
let Ok(project) = this
.workspace
.read_with(cx, |this, _| this.project().clone())
@@ -275,7 +285,7 @@ impl NewSessionModal {
}
};
let available_adapters: Vec<DebugTaskTemplate> = workspace
let available_adapters: Vec<DebugTaskDefinition> = workspace
.update(cx, |this, cx| {
this.project()
.read(cx)
@@ -293,9 +303,9 @@ impl NewSessionModal {
for debug_definition in available_adapters {
menu = menu.entry(
debug_definition.definition.label.clone(),
debug_definition.label.clone(),
None,
setter_for_name(debug_definition.definition),
setter_for_name(debug_definition),
);
}
menu
@@ -312,7 +322,7 @@ struct LaunchMode {
impl LaunchMode {
fn new(
past_launch_config: Option<LaunchRequest>,
past_launch_config: Option<LaunchConfig>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
@@ -338,9 +348,9 @@ impl LaunchMode {
cx.new(|_| Self { program, cwd })
}
fn debug_task(&self, cx: &App) -> task::LaunchRequest {
fn debug_task(&self, cx: &App) -> task::LaunchConfig {
let path = self.cwd.read(cx).text(cx);
task::LaunchRequest {
task::LaunchConfig {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
@@ -363,9 +373,10 @@ impl AttachMode {
) -> Entity<Self> {
let debug_definition = DebugTaskDefinition {
label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
request: dap::DebugRequestType::Attach(task::AttachConfig { process_id: None }),
tcp_connection: None,
adapter: debugger.clone().unwrap_or_default().into(),
locator: None,
initialize_args: None,
stop_on_entry: Some(false),
};
@@ -380,8 +391,8 @@ impl AttachMode {
attach_picker,
})
}
fn debug_task(&self) -> task::AttachRequest {
task::AttachRequest { process_id: None }
fn debug_task(&self) -> task::AttachConfig {
task::AttachConfig { process_id: None }
}
}
@@ -395,7 +406,7 @@ enum NewSessionMode {
}
impl NewSessionMode {
fn debug_task(&self, cx: &App) -> DebugRequest {
fn debug_task(&self, cx: &App) -> DebugRequestType {
match self {
NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
@@ -477,7 +488,7 @@ impl NewSessionMode {
Self::Attach(AttachMode::new(debugger, project, window, cx))
}
fn launch(
past_launch_config: Option<LaunchRequest>,
past_launch_config: Option<LaunchConfig>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {

View File

@@ -5,7 +5,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm;
use project::{FakeFs, Project};
use serde_json::json;
use task::{AttachRequest, DebugTaskDefinition, TcpArgumentsTemplate};
use task::{AttachConfig, DebugTaskDefinition, TCPHost};
use tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -31,12 +31,13 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
cx,
DebugTaskDefinition {
adapter: "fake-adapter".to_string(),
request: dap::DebugRequest::Attach(AttachRequest {
request: dap::DebugRequestType::Attach(AttachConfig {
process_id: Some(10),
}),
label: "label".to_string(),
initialize_args: None,
tcp_connection: None,
locator: None,
stop_on_entry: None,
},
|client| {
@@ -104,10 +105,11 @@ async fn test_show_attach_modal_and_select_process(
project.clone(),
DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()),
request: dap::DebugRequestType::Attach(AttachConfig::default()),
label: "attach example".into(),
initialize_args: None,
tcp_connection: Some(TcpArgumentsTemplate::default()),
tcp_connection: Some(TCPHost::default()),
locator: None,
stop_on_entry: None,
},
vec![

View File

@@ -64,10 +64,6 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Self {
Model::Chat
}
pub fn from_id(id: &str) -> Result<Self> {
match id {
"deepseek-chat" => Ok(Self::Chat),

View File

@@ -215,7 +215,6 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024;
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -812,8 +811,7 @@ pub struct Editor {
next_completion_id: CompletionId,
available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>,
code_actions_task: Option<Task<Result<()>>>,
quick_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
debounced_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
selection_highlight_task: Option<Task<()>>,
document_highlights_task: Option<Task<()>>,
linked_editing_range_task: Option<Task<Option<()>>>,
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
@@ -1592,8 +1590,7 @@ impl Editor {
code_action_providers,
available_code_actions: Default::default(),
code_actions_task: Default::default(),
quick_selection_highlight_task: Default::default(),
debounced_selection_highlight_task: Default::default(),
selection_highlight_task: Default::default(),
document_highlights_task: Default::default(),
linked_editing_range_task: Default::default(),
pending_rename: Default::default(),
@@ -1723,7 +1720,6 @@ impl Editor {
new_anchor.offset,
);
});
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
}
}
EditorEvent::Edited { .. } => {
@@ -5111,21 +5107,44 @@ impl Editor {
CodeActionsItem::Task(task_source_kind, resolved_task) => {
match resolved_task.task_type() {
task::TaskType::Script => workspace.update(cx, |workspace, cx| {
workspace.schedule_resolved_task(
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
resolved_task,
false,
window,
cx,
);
Some(Task::ready(Ok(())))
}),
task::TaskType::Debug(_) => {
workspace.update(cx, |workspace, cx| {
workspace.schedule_debug_task(resolved_task, window, cx);
});
Some(Task::ready(Ok(())))
task::TaskType::Debug(debug_args) => {
if debug_args.locator.is_some() {
workspace.update(cx, |workspace, cx| {
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
resolved_task,
false,
cx,
);
});
return Some(Task::ready(Ok(())));
}
if let Some(project) = self.project.as_ref() {
project
.update(cx, |project, cx| {
project.start_debug_session(
resolved_task.resolved_debug_adapter_config().unwrap(),
cx,
)
})
.detach_and_log_err(cx);
Some(Task::ready(Ok(())))
} else {
Some(Task::ready(Ok(())))
}
}
}
}
@@ -5456,169 +5475,111 @@ impl Editor {
None
}
fn prepare_highlight_query_from_selection(
pub fn refresh_selected_text_highlights(
&mut self,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<(String, Range<Anchor>)> {
) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
return None;
return;
}
self.selection_highlight_task.take();
if !EditorSettings::get_global(cx).selection_highlight {
return None;
self.clear_background_highlights::<SelectedTextHighlight>(cx);
return;
}
if self.selections.count() != 1 || self.selections.line_mode {
return None;
self.clear_background_highlights::<SelectedTextHighlight>(cx);
return;
}
let selection = self.selections.newest::<Point>(cx);
if selection.is_empty() || selection.start.row != selection.end.row {
return None;
self.clear_background_highlights::<SelectedTextHighlight>(cx);
return;
}
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot);
let query = multi_buffer_snapshot
.text_for_range(selection_anchor_range.clone())
.collect::<String>();
if query.trim().is_empty() {
return None;
}
Some((query, selection_anchor_range))
}
fn update_selection_occurrence_highlights(
&mut self,
query_text: String,
query_range: Range<Anchor>,
multi_buffer_range_to_query: Range<Point>,
use_debounce: bool,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<()> {
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
cx.spawn_in(window, async move |editor, cx| {
if use_debounce {
cx.background_executor()
.timer(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT)
.await;
}
let match_task = cx.background_spawn(async move {
let buffer_ranges = multi_buffer_snapshot
.range_to_buffer_ranges(multi_buffer_range_to_query)
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty());
let mut match_ranges = Vec::new();
for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges {
match_ranges.extend(
project::search::SearchQuery::text(
query_text.clone(),
false,
false,
false,
Default::default(),
Default::default(),
false,
None,
)
.unwrap()
.search(&buffer_snapshot, Some(search_range.clone()))
.await
.into_iter()
.filter_map(|match_range| {
let match_start = buffer_snapshot
.anchor_after(search_range.start + match_range.start);
let match_end =
buffer_snapshot.anchor_before(search_range.start + match_range.end);
let match_anchor_range = Anchor::range_in_buffer(
excerpt_id,
buffer_snapshot.remote_id(),
match_start..match_end,
);
(match_anchor_range != query_range).then_some(match_anchor_range)
}),
);
}
match_ranges
});
let match_ranges = match_task.await;
let debounce = EditorSettings::get_global(cx).selection_highlight_debounce;
self.selection_highlight_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(Duration::from_millis(debounce))
.await;
let Some(Some(matches_task)) = editor
.update_in(cx, |editor, _, cx| {
if editor.selections.count() != 1 || editor.selections.line_mode {
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
return None;
}
let selection = editor.selections.newest::<Point>(cx);
if selection.is_empty() || selection.start.row != selection.end.row {
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
return None;
}
let buffer = editor.buffer().read(cx).snapshot(cx);
let query = buffer.text_for_range(selection.range()).collect::<String>();
if query.trim().is_empty() {
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
return None;
}
Some(cx.background_spawn(async move {
let mut ranges = Vec::new();
let selection_anchors = selection.range().to_anchors(&buffer);
for range in [buffer.anchor_before(0)..buffer.anchor_after(buffer.len())] {
for (search_buffer, search_range, excerpt_id) in
buffer.range_to_buffer_ranges(range)
{
ranges.extend(
project::search::SearchQuery::text(
query.clone(),
false,
false,
false,
Default::default(),
Default::default(),
None,
)
.unwrap()
.search(search_buffer, Some(search_range.clone()))
.await
.into_iter()
.filter_map(
|match_range| {
let start = search_buffer.anchor_after(
search_range.start + match_range.start,
);
let end = search_buffer.anchor_before(
search_range.start + match_range.end,
);
let range = Anchor::range_in_buffer(
excerpt_id,
search_buffer.remote_id(),
start..end,
);
(range != selection_anchors).then_some(range)
},
),
);
}
}
ranges
}))
})
.log_err()
else {
return;
};
let matches = matches_task.await;
editor
.update_in(cx, |editor, _, cx| {
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
if !match_ranges.is_empty() {
if !matches.is_empty() {
editor.highlight_background::<SelectedTextHighlight>(
&match_ranges,
&matches,
|theme| theme.editor_document_highlight_bracket_background,
cx,
)
}
})
.log_err();
})
}
fn refresh_selected_text_highlights(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx)
else {
self.clear_background_highlights::<SelectedTextHighlight>(cx);
self.quick_selection_highlight_task.take();
self.debounced_selection_highlight_task.take();
return;
};
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
if self
.quick_selection_highlight_task
.as_ref()
.map_or(true, |(prev_anchor_range, _)| {
prev_anchor_range != &query_range
})
{
let multi_buffer_visible_start = self
.scroll_manager
.anchor()
.anchor
.to_point(&multi_buffer_snapshot);
let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
multi_buffer_visible_start
+ Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
Bias::Left,
);
let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
self.quick_selection_highlight_task = Some((
query_range.clone(),
self.update_selection_occurrence_highlights(
query_text.clone(),
query_range.clone(),
multi_buffer_visible_range,
false,
window,
cx,
),
));
}
if self
.debounced_selection_highlight_task
.as_ref()
.map_or(true, |(prev_anchor_range, _)| {
prev_anchor_range != &query_range
})
{
let multi_buffer_start = multi_buffer_snapshot
.anchor_before(0)
.to_point(&multi_buffer_snapshot);
let multi_buffer_end = multi_buffer_snapshot
.anchor_after(multi_buffer_snapshot.len())
.to_point(&multi_buffer_snapshot);
let multi_buffer_full_range = multi_buffer_start..multi_buffer_end;
self.debounced_selection_highlight_task = Some((
query_range.clone(),
self.update_selection_occurrence_highlights(
query_text,
query_range,
multi_buffer_full_range,
true,
window,
cx,
),
));
}
}));
}
pub fn refresh_inline_completion(
@@ -6822,12 +6783,12 @@ impl Editor {
resolved.reveal = reveal_strategy;
workspace
.update_in(cx, |workspace, window, cx| {
workspace.schedule_resolved_task(
.update(cx, |workspace, cx| {
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
resolved_task,
false,
window,
cx,
);
})

View File

@@ -10,6 +10,7 @@ pub struct EditorSettings {
pub cursor_shape: Option<CursorShape>,
pub current_line_highlight: CurrentLineHighlight,
pub selection_highlight: bool,
pub selection_highlight_debounce: u64,
pub lsp_highlight_debounce: u64,
pub hover_popover_enabled: bool,
pub hover_popover_delay: u64,
@@ -262,6 +263,10 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub selection_highlight: Option<bool>,
/// The debounce delay before querying highlights based on the selected text.
///
/// Default: 75
pub selection_highlight_debounce: Option<u64>,
/// The debounce delay before querying highlights from the language
/// server based on the current cursor location.
///

View File

@@ -12,8 +12,8 @@ use crate::{
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
use futures::StreamExt;
use gpui::{
BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
VisualTestContext, WindowBounds, WindowOptions, div,
BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
WindowBounds, WindowOptions, div,
};
use indoc::indoc;
use language::{
@@ -19549,64 +19549,6 @@ println!("5");
});
}
#[gpui::test]
async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
struct EmptyModalView {
focus_handle: gpui::FocusHandle,
}
impl EventEmitter<DismissEvent> for EmptyModalView {}
impl Render for EmptyModalView {
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
div()
}
}
impl Focusable for EmptyModalView {
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl workspace::ModalView for EmptyModalView {}
fn new_empty_modal_view(cx: &App) -> EmptyModalView {
EmptyModalView {
focus_handle: cx.focus_handle(),
}
}
init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let editor = cx.new_window_entity(|window, cx| {
Editor::new(
EditorMode::full(),
buffer,
Some(project.clone()),
window,
cx,
)
});
workspace
.update(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
})
.unwrap();
editor.update_in(cx, |editor, window, cx| {
editor.open_context_menu(&OpenContextMenu, window, cx);
assert!(editor.mouse_context_menu.is_some());
});
workspace
.update(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
})
.unwrap();
cx.read(|cx| {
assert!(editor.read(cx).mouse_context_menu.is_none());
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -928,17 +928,9 @@ impl Item for Editor {
&mut self,
workspace: &mut Workspace,
_window: &mut Window,
cx: &mut Context<Self>,
_: &mut Context<Self>,
) {
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
if let Some(workspace) = &workspace.weak_handle().upgrade() {
cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| {
if matches!(event, workspace::Event::ModalOpened) {
editor.mouse_context_menu.take();
}
})
.detach();
}
}
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {

View File

@@ -15,6 +15,7 @@ clap.workspace = true
client.workspace = true
collections.workspace = true
context_server.workspace = true
dap.workspace = true
dirs = "5.0"
env_logger.workspace = true
extension.workspace = true

View File

@@ -1,3 +1,3 @@
url = "https://github.com/zed-industries/zed.git"
revision = "03ecb88fe30794873f191ddb728f597935b3101c"
revision = "38fcadf9481d018543c65f36ac3bafeba190179b"
language_extension = "rs"

View File

@@ -1,3 +1,3 @@
1. The first tool call should be to path search including "find_replace_file_tool.rs" in the string. (*Not* grep, for example, or reading the file based on a guess at the path.) This is because we gave the model a filename and it needs to turn that into a real path.
1. The first tool call should be to path search including "find_replace_file_tool.rs" in the string. (*Not* regex_search, for example, or reading the file based on a guess at the path.) This is because we gave the model a filename and it needs to turn that into a real path.
2. After obtaining the correct path of "zed/crates/assistant_tools/src/find_replace_file_tool.rs", it should read the contents of that path.
3. When trying to find information about the Render trait, it should *not* begin with a path search, because it doesn't yet have any information on what path the Render trait might be in.

View File

@@ -1,15 +1,13 @@
mod example;
mod ids;
mod tool_metrics;
use client::{Client, ProxySettings, UserStore};
pub(crate) use example::*;
pub(crate) use tool_metrics::*;
use telemetry;
use ::fs::RealFs;
use anyhow::{Result, anyhow};
use clap::Parser;
use client::{Client, ProxySettings, UserStore};
use collections::HashSet;
use extension::ExtensionHostProxy;
use futures::{StreamExt, future};
use gpui::http_client::{Uri, read_proxy_from_env};
@@ -24,6 +22,7 @@ use prompt_store::PromptBuilder;
use release_channel::AppVersion;
use reqwest_client::ReqwestClient;
use settings::{Settings, SettingsStore};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::usize;
@@ -93,8 +92,6 @@ fn main() {
.telemetry()
.start(system_id, installation_id, session_id, cx);
let mut cumulative_tool_metrics = ToolMetrics::default();
let model_registry = LanguageModelRegistry::read_global(cx);
let model = find_model("claude-3-7-sonnet-latest", model_registry, cx).unwrap();
let model_provider_id = model.provider_id();
@@ -180,7 +177,7 @@ fn main() {
return cx.update(|cx| cx.quit());
}
let mut repo_urls = HashSet::default();
let mut repo_urls = HashSet::new();
let mut clone_tasks = Vec::new();
for (i, example) in examples.iter_mut().enumerate() {
@@ -247,24 +244,9 @@ fn main() {
let model = model.clone();
let example = example.clone();
cx.spawn(async move |cx| {
let result = async {
let run_output = cx
.update(|cx| example.run(model.clone(), app_state.clone(), cx))?
.await?;
let judge_tasks = (0..judge_repetitions).map(|round| {
run_judge_repetition(
example.clone(),
model.clone(),
&run_output,
round,
cx,
)
});
let judge_outputs = future::join_all(judge_tasks).await;
anyhow::Ok((run_output, judge_outputs))
}
.await;
(example, result)
let result =
run_example(&example, model, app_state, judge_repetitions, cx).await;
(result, example)
})
});
@@ -274,58 +256,52 @@ fn main() {
.await;
println!("\n\n");
print_header("EVAL RESULTS");
println!("========================================");
println!(" EVAL RESULTS ");
println!("========================================");
println!("");
let mut diff_scores = Vec::new();
let mut thread_scores = Vec::new();
let mut error_count = 0;
for (example, result) in results {
print_header(&example.name);
for (result, example) in results {
match result {
Err(err) => {
println!("💥 {}{:?}", example.log_prefix, err);
error_count += 1;
}
Ok((run_output, judge_results)) => {
cumulative_tool_metrics.merge(&run_output.tool_metrics);
println!("┌───────┬──────┬────────┐");
println!("│ Judge │ Diff │ Thread │");
println!("├───────┼──────┼────────┤");
for (i, judge_result) in judge_results.iter().enumerate() {
Ok(judge_results) => {
for judge_result in judge_results {
match judge_result {
Ok(judge_output) => {
let diff_score = judge_output.diff.score;
diff_scores.push(diff_score);
let thread_display = if let Some(thread) = &judge_output.thread
{
let thread_score = thread.score;
thread_scores.push(thread_score);
format!("{}", thread_score)
} else {
"N/A".to_string()
};
const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
let diff_score: u32 = judge_output.diff.score;
let score_index = (diff_score.min(5)) as usize;
println!(
"|{:^7}{:^6}{:^8}",
i + 1,
diff_score,
thread_display
"{} {}{} (Diff)",
SCORES[score_index],
example.log_prefix,
judge_output.diff.score,
);
diff_scores.push(judge_output.diff.score);
if let Some(thread) = judge_output.thread {
let process_score: u32 = thread.score;
let score_index = (process_score.min(5)) as usize;
println!(
"{} {}{} (Thread)",
SCORES[score_index], example.log_prefix, thread.score,
);
thread_scores.push(thread.score);
}
}
Err(err) => {
println!("|{:^7}{:^6}{:^8}{:?}", i + 1, "N/A", "N/A", err);
println!("💥 {}{:?}", example.log_prefix, err);
}
}
}
println!("└───────┴──────┴────────┘");
println!("{}", run_output.tool_metrics);
}
}
println!(
@@ -365,9 +341,6 @@ fn main() {
}
}
print_header("CUMULATIVE TOOL METRICS");
println!("{}", cumulative_tool_metrics);
std::thread::sleep(std::time::Duration::from_secs(2));
app_state.client.telemetry().flush_events();
@@ -378,6 +351,27 @@ fn main() {
});
}
async fn run_example(
example: &Example,
model: Arc<dyn LanguageModel>,
app_state: Arc<AgentAppState>,
judge_repetitions: u32,
cx: &mut AsyncApp,
) -> Result<Vec<Result<JudgeOutput>>> {
let run_output = cx
.update(|cx| example.run(model.clone(), app_state.clone(), cx))?
.await?;
let judge_tasks = (0..judge_repetitions)
.map(|round| run_judge_repetition(example.clone(), model.clone(), &run_output, round, cx));
let results = future::join_all(judge_tasks).await;
app_state.client.telemetry().flush_events();
Ok(results)
}
fn list_all_examples() -> Result<Vec<PathBuf>> {
let path = std::fs::canonicalize(EXAMPLES_DIR).unwrap();
let entries = std::fs::read_dir(path).unwrap();
@@ -572,7 +566,7 @@ async fn run_judge_repetition(
diff_analysis = judge_output.diff.analysis,
thread_score = thread.score,
thread_analysis = thread.analysis,
tool_metrics = run_output.tool_metrics,
tool_use_counts = run_output.tool_use_counts,
response_count = run_output.response_count,
token_usage = run_output.token_usage,
model = model.telemetry_id(),
@@ -591,7 +585,7 @@ async fn run_judge_repetition(
round = round,
diff_score = judge_output.diff.score,
diff_analysis = judge_output.diff.analysis,
tool_metrics = run_output.tool_metrics,
tool_use_counts = run_output.tool_use_counts,
response_count = run_output.response_count,
token_usage = run_output.token_usage,
model = model.telemetry_id(),
@@ -607,9 +601,3 @@ async fn run_judge_repetition(
judge_result
}
fn print_header(header: &str) {
println!("\n========================================");
println!("{:^40}", header);
println!("========================================\n");
}

View File

@@ -1,18 +1,19 @@
use crate::{AgentAppState, ToolMetrics};
use agent::{ThreadEvent, ThreadStore};
use agent::{RequestKind, ThreadEvent, ThreadStore};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::ToolWorkingSet;
use client::proto::LspWorkProgress;
use collections::HashMap;
use dap::DapRegistry;
use futures::channel::mpsc;
use futures::{FutureExt, StreamExt as _, select_biased};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task};
use handlebars::Handlebars;
use language::{Buffer, DiagnosticSeverity, OffsetRangeExt};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
MessageContent, Role, StopReason, TokenUsage,
};
use project::{Project, ProjectPath};
use project::{LspStore, Project, ProjectPath};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::fmt::Write as _;
@@ -31,6 +32,8 @@ use util::command::new_smol_command;
use util::markdown::MarkdownString;
use util::serde::default_true;
use crate::AgentAppState;
pub const EXAMPLES_DIR: &str = "./crates/eval/examples";
pub const REPOS_DIR: &str = "./crates/eval/repos";
pub const WORKTREES_DIR: &str = "./crates/eval/worktrees";
@@ -85,7 +88,7 @@ pub struct RunOutput {
pub diagnostics_after: Option<String>,
pub response_count: usize,
pub token_usage: TokenUsage,
pub tool_metrics: ToolMetrics,
pub tool_use_counts: HashMap<Arc<str>, u32>,
pub last_request: LanguageModelRequest,
}
@@ -242,6 +245,7 @@ impl Example {
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
Arc::new(DapRegistry::default()),
app_state.fs.clone(),
None,
cx,
@@ -267,7 +271,7 @@ impl Example {
})?
.await;
let lsp = if this.base.require_lsp {
let lsp_open_handle_and_store = if this.base.require_lsp {
let language_extension = this.base.language_extension.as_deref().context(
"language_extension field is required in base.toml when `require_lsp == true`",
)?;
@@ -297,13 +301,39 @@ impl Example {
let language_file_buffer = open_language_file_buffer_task.await?;
let lsp_open_handle = project.update(cx, |project, cx| {
project.register_buffer_with_language_servers(&language_file_buffer, cx)
let (lsp_open_handle, lsp_store) = project.update(cx, |project, cx| {
(
project.register_buffer_with_language_servers(&language_file_buffer, cx),
project.lsp_store().clone(),
)
})?;
wait_for_lang_server(&project, &language_file_buffer, this.log_prefix.clone(), cx).await?;
// TODO: remove this once the diagnostics tool waits for new diagnostics
cx.background_executor().timer(Duration::new(5, 0)).await;
wait_for_lang_server(&lsp_store, this.log_prefix.clone(), cx).await?;
Some((lsp_open_handle, language_file_buffer))
lsp_store.update(cx, |lsp_store, cx| {
lsp_open_handle.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, cx| {
let has_language_server = lsp_store
.language_servers_for_local_buffer(buffer, cx)
.next()
.is_some();
if has_language_server {
Ok(())
} else {
Err(anyhow!(
"`{:?}` was opened to cause the language server to start, \
but no language servers are registered for its buffer. \
Set `require_lsp = false` in `base.toml` to skip this.",
language_file
))
}
})
})
})??;
Some((lsp_open_handle, lsp_store))
} else {
None
};
@@ -347,7 +377,8 @@ impl Example {
});
})?;
let tool_metrics = Arc::new(Mutex::new(ToolMetrics::default()));
let tool_use_counts: Arc<Mutex<HashMap<Arc<str>, u32>>> =
Mutex::new(HashMap::default()).into();
let (thread_event_tx, mut thread_event_rx) = mpsc::unbounded();
@@ -357,7 +388,7 @@ impl Example {
let event_handler_task = cx.spawn({
let log_prefix = this.log_prefix.clone();
let tool_metrics = tool_metrics.clone();
let tool_use_counts = tool_use_counts.clone();
let thread = thread.downgrade();
async move |cx| {
loop {
@@ -400,7 +431,6 @@ impl Example {
} => {
thread.update(cx, |thread, _cx| {
if let Some(tool_use) = pending_tool_use {
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)
@@ -408,11 +438,13 @@ impl Example {
format!("TOOL FINISHED: {}", tool_use.name)
};
println!("{log_prefix}{message}");
tool_metrics.insert(tool_result.tool_name.clone(), !tool_result.is_error);
let mut tool_use_counts = tool_use_counts.lock().unwrap();
*tool_use_counts
.entry(tool_result.tool_name.clone())
.or_insert(0) += 1;
} else {
let message = format!("TOOL FINISHED WITHOUT RESULT: {}", tool_use.name);
println!("{log_prefix}{message}");
tool_metrics.insert(tool_use.name.clone(), true);
}
}
})?;
@@ -440,15 +472,15 @@ impl Example {
thread.update(cx, |thread, cx| {
let context = vec![];
thread.insert_user_message(this.prompt.clone(), context, None, cx);
thread.send_to_model(model, cx);
thread.send_to_model(model, RequestKind::Chat, cx);
})?;
event_handler_task.await?;
println!("{}Stopped", this.log_prefix);
if let Some((_, language_file_buffer)) = lsp.as_ref() {
wait_for_lang_server(&project, &language_file_buffer, this.log_prefix.clone(), cx).await?;
if let Some((_, lsp_store)) = lsp_open_handle_and_store.as_ref() {
wait_for_lang_server(lsp_store, this.log_prefix.clone(), cx).await?;
}
println!("{}Getting repository diff", this.log_prefix);
@@ -472,7 +504,7 @@ impl Example {
};
drop(subscription);
drop(lsp);
drop(lsp_open_handle_and_store);
if let Some(diagnostics_before) = &diagnostics_before {
fs::write(example_output_dir.join("diagnostics_before.txt"), diagnostics_before)?;
@@ -495,7 +527,7 @@ impl Example {
diagnostics_after,
response_count,
token_usage: thread.cumulative_token_usage(),
tool_metrics: tool_metrics.lock().unwrap().clone(),
tool_use_counts: tool_use_counts.lock().unwrap().clone(),
last_request,
}
})
@@ -642,42 +674,27 @@ impl Example {
}
fn wait_for_lang_server(
project: &Entity<Project>,
buffer: &Entity<Buffer>,
lsp_store: &Entity<LspStore>,
log_prefix: String,
cx: &mut AsyncApp,
) -> Task<Result<()>> {
if cx
.update(|cx| !has_pending_lang_server_work(lsp_store, cx))
.unwrap()
|| std::env::var("ZED_EVAL_SKIP_LS_WAIT").is_ok()
{
return Task::ready(anyhow::Ok(()));
}
println!("{}⏵ Waiting for language server", log_prefix);
let (mut tx, mut rx) = mpsc::channel(1);
let lsp_store = project
.update(cx, |project, _| project.lsp_store())
.unwrap();
let has_lang_server = buffer
.update(cx, |buffer, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.language_servers_for_local_buffer(&buffer, cx)
.next()
.is_some()
})
})
.unwrap_or(false);
if has_lang_server {
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.unwrap()
.detach();
}
let subscriptions =
[
cx.subscribe(&lsp_store, {
let log_prefix = log_prefix.clone();
move |_, event, _| match event {
let subscription =
cx.subscribe(&lsp_store, {
let log_prefix = log_prefix.clone();
move |lsp_store, event, cx| {
match event {
project::LspStoreEvent::LanguageServerUpdate {
message:
client::proto::update_language_server::Variant::WorkProgress(
@@ -690,23 +707,12 @@ fn wait_for_lang_server(
} => println!("{}{message}", log_prefix),
_ => {}
}
}),
cx.subscribe(&project, {
let buffer = buffer.clone();
move |project, event, cx| match event {
project::Event::LanguageServerAdded(_, _, _) => {
let buffer = buffer.clone();
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))
.detach();
}
project::Event::DiskBasedDiagnosticsFinished { .. } => {
tx.try_send(()).ok();
}
_ => {}
if !has_pending_lang_server_work(&lsp_store, cx) {
tx.try_send(()).ok();
}
}),
];
}
});
cx.spawn(async move |cx| {
let timeout = cx.background_executor().timer(Duration::new(60 * 5, 0));
@@ -719,11 +725,18 @@ fn wait_for_lang_server(
Err(anyhow!("LSP wait timed out after 5 minutes"))
}
};
drop(subscriptions);
drop(subscription);
result
})
}
fn has_pending_lang_server_work(lsp_store: &Entity<LspStore>, cx: &App) -> bool {
lsp_store
.read(cx)
.language_server_statuses()
.any(|(_, status)| !status.pending_work.is_empty())
}
async fn query_lsp_diagnostics(
project: Entity<Project>,
cx: &mut AsyncApp,
@@ -903,20 +916,6 @@ impl RequestMarkdown {
MessageContent::Image(_) => {
messages.push_str("[IMAGE DATA]\n\n");
}
MessageContent::Thinking { text, signature } => {
messages.push_str("**Thinking**:\n\n");
if let Some(sig) = signature {
messages.push_str(&format!("Signature: {}\n\n", sig));
}
messages.push_str(text);
messages.push_str("\n");
}
MessageContent::RedactedThinking(items) => {
messages.push_str(&format!(
"**Redacted Thinking**: {} item(s)\n\n",
items.len()
));
}
MessageContent::ToolUse(tool_use) => {
messages.push_str(&format!(
"**Tool Use**: {} (ID: {})\n",
@@ -935,7 +934,7 @@ impl RequestMarkdown {
if tool_result.is_error {
messages.push_str("**ERROR:**\n");
}
messages.push_str(&format!("{}\n\n", tool_result.content));
messages.push_str(&format!("{}\n", tool_result.content));
}
}
}
@@ -971,7 +970,7 @@ fn response_events_to_markdown(
Ok(LanguageModelCompletionEvent::Text(text)) => {
text_buffer.push_str(text);
}
Ok(LanguageModelCompletionEvent::Thinking { text, .. }) => {
Ok(LanguageModelCompletionEvent::Thinking(text)) => {
thinking_buffer.push_str(text);
}
Ok(LanguageModelCompletionEvent::Stop(reason)) => {

View File

@@ -1,102 +0,0 @@
use collections::HashMap;
use serde::{Deserialize, Serialize};
use std::{fmt::Display, sync::Arc};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ToolMetrics {
pub use_counts: HashMap<Arc<str>, u32>,
pub failure_counts: HashMap<Arc<str>, u32>,
}
impl ToolMetrics {
pub fn insert(&mut self, tool_name: Arc<str>, succeeded: bool) {
*self.use_counts.entry(tool_name.clone()).or_insert(0) += 1;
if !succeeded {
*self.failure_counts.entry(tool_name).or_insert(0) += 1;
}
}
pub fn merge(&mut self, other: &ToolMetrics) {
for (tool_name, use_count) in &other.use_counts {
*self.use_counts.entry(tool_name.clone()).or_insert(0) += use_count;
}
for (tool_name, failure_count) in &other.failure_counts {
*self.failure_counts.entry(tool_name.clone()).or_insert(0) += failure_count;
}
}
}
impl Display for ToolMetrics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut failure_rates: Vec<(Arc<str>, f64)> = Vec::new();
for (tool_name, use_count) in &self.use_counts {
let failure_count = self.failure_counts.get(tool_name).cloned().unwrap_or(0);
if *use_count > 0 {
let failure_rate = failure_count as f64 / *use_count as f64;
failure_rates.push((tool_name.clone(), failure_rate));
}
}
// Sort by failure rate descending
failure_rates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
// Table dimensions
let tool_width = 30;
let count_width = 10;
let rate_width = 10;
// Write table top border
writeln!(
f,
"┌{}┬{}┬{}┬{}┐",
"".repeat(tool_width),
"".repeat(count_width),
"".repeat(count_width),
"".repeat(rate_width)
)?;
// Write header row
writeln!(
f,
"│{:^30}│{:^10}│{:^10}│{:^10}│",
"Tool", "Uses", "Failures", "Rate"
)?;
// Write header-data separator
writeln!(
f,
"├{}┼{}┼{}┼{}┤",
"".repeat(tool_width),
"".repeat(count_width),
"".repeat(count_width),
"".repeat(rate_width)
)?;
// Write data rows
for (tool_name, failure_rate) in failure_rates {
let use_count = self.use_counts.get(&tool_name).cloned().unwrap_or(0);
let failure_count = self.failure_counts.get(&tool_name).cloned().unwrap_or(0);
writeln!(
f,
"│{:^30}│{:^10}│{:^10}│{:^10}│",
tool_name,
use_count,
failure_count,
format!("{}%", (failure_rate * 100.0).round())
)?;
}
// Write table bottom border
writeln!(
f,
"└{}┴{}┴{}┴{}┘",
"".repeat(tool_width),
"".repeat(count_width),
"".repeat(count_width),
"".repeat(rate_width)
)?;
Ok(())
}
}

View File

@@ -412,10 +412,6 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Model {
Model::Gemini15Flash
}
pub fn id(&self) -> &str {
match self {
Model::Gemini15Pro => "gemini-1.5-pro",

View File

@@ -66,7 +66,7 @@ x11 = [
"x11-clipboard",
"filedescriptor",
"open",
"scap",
"scap"
]
@@ -220,7 +220,6 @@ rand.workspace = true
windows.workspace = true
windows-core = "0.61"
windows-numerics = "0.2"
windows-registry = "0.5"
[dev-dependencies]
backtrace = "0.3"

View File

@@ -635,7 +635,7 @@ impl Render for InputExample {
.flex()
.flex_row()
.justify_between()
.child(format!("Keyboard {}", cx.keyboard_layout().name()))
.child(format!("Keyboard {}", cx.keyboard_layout()))
.child(
div()
.border_1()

View File

@@ -35,10 +35,10 @@ use crate::{
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,
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId,
WindowInvalidator, current_platform, hash, init_app_menus,
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, current_platform, hash,
init_app_menus,
};
mod async_context;
@@ -248,7 +248,7 @@ pub struct App {
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) keyboard_layout: SharedString,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
pending_effects: VecDeque<Effect>,
@@ -289,7 +289,7 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_layout = platform.keyboard_layout();
let keyboard_layout = SharedString::from(platform.keyboard_layout());
let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(App {
@@ -345,7 +345,7 @@ impl App {
move || {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_layout = cx.platform.keyboard_layout();
cx.keyboard_layout = SharedString::from(cx.platform.keyboard_layout());
cx.keyboard_layout_observers
.clone()
.retain(&(), move |callback| (callback)(cx));
@@ -387,8 +387,8 @@ impl App {
}
/// Get the id of the current keyboard layout
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
self.keyboard_layout.as_ref()
pub fn keyboard_layout(&self) -> &SharedString {
&self.keyboard_layout
}
/// Invokes a handler when the current keyboard layout changes

View File

@@ -113,7 +113,7 @@ impl AppContext for TestAppContext {
impl TestAppContext {
/// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you.
pub fn build(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self {
pub fn new(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self {
let arc_dispatcher = Arc::new(dispatcher.clone());
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
@@ -146,7 +146,7 @@ impl TestAppContext {
/// returns a new `TestAppContext` re-using the same executors to interleave tasks.
pub fn new_app(&self) -> TestAppContext {
Self::build(self.dispatcher.clone(), self.fn_name)
Self::new(self.dispatcher.clone(), self.fn_name)
}
/// Called by the test helper to end the test.
@@ -178,11 +178,6 @@ impl TestAppContext {
&self.foreground_executor
}
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
let mut cx = self.app.borrow_mut();
cx.new(build_entity)
}
/// Gives you an `&mut App` for the duration of the closure
pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
let mut cx = self.app.borrow_mut();

View File

@@ -214,7 +214,7 @@ pub(crate) trait Platform: 'static {
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn keyboard_layout(&self) -> String;
fn compositor_name(&self) -> &'static str {
""
@@ -1634,11 +1634,3 @@ impl From<String> for ClipboardString {
}
}
}
/// A trait for platform-specific keyboard layouts
pub trait PlatformKeyboardLayout {
/// Get the keyboard layout ID, which should be unique to the layout
fn id(&self) -> &str;
/// Get the keyboard layout display name
fn name(&self) -> &str;
}

View File

@@ -9,8 +9,7 @@ use util::ResultExt;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
AnyWindowHandle, CursorStyle, DisplayId, LinuxKeyboardLayout, PlatformDisplay,
PlatformKeyboardLayout, ScreenCaptureSource, WindowParams,
AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, ScreenCaptureSource, WindowParams,
};
pub struct HeadlessClientState {
@@ -51,8 +50,8 @@ impl LinuxClient for HeadlessClient {
f(&mut self.0.borrow_mut().common)
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(LinuxKeyboardLayout::new("unknown".to_string()))
fn keyboard_layout(&self) -> String {
"unknown".to_string()
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {

View File

@@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
Point, Result, ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
Pixels, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Point, Result,
ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
@@ -46,7 +46,7 @@ const FILE_PICKER_PORTAL_MISSING: &str =
pub trait LinuxClient {
fn compositor_name(&self) -> &'static str;
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn keyboard_layout(&self) -> String;
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
#[allow(unused)]
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
@@ -138,7 +138,7 @@ impl<P: LinuxClient + 'static> Platform for P {
self.with_common(|common| common.text_system.clone())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
fn keyboard_layout(&self) -> String {
self.keyboard_layout()
}
@@ -858,26 +858,6 @@ impl crate::Modifiers {
}
}
pub(crate) struct LinuxKeyboardLayout {
id: String,
}
impl PlatformKeyboardLayout for LinuxKeyboardLayout {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.id
}
}
impl LinuxKeyboardLayout {
pub(crate) fn new(id: String) -> Self {
Self { id }
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -66,10 +66,8 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
use super::{
display::WaylandDisplay,
window::{ImeInput, WaylandWindowStatePtr},
};
use super::display::WaylandDisplay;
use super::window::{ImeInput, WaylandWindowStatePtr};
use crate::platform::linux::{
LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
@@ -85,11 +83,11 @@ use crate::platform::linux::{
use crate::platform::{PlatformWindow, blade::BladeContext};
use crate::{
AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource,
ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, SCROLL_LINES,
ScaledPixels, ScreenCaptureSource, ScrollDelta, ScrollWheelEvent, Size, TouchPhase,
WindowParams, point, px, size,
};
/// Used to convert evdev scancode to xkb scancode
@@ -589,9 +587,9 @@ impl WaylandClient {
}
impl LinuxClient for WaylandClient {
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
fn keyboard_layout(&self) -> String {
let state = self.0.borrow();
let id = if let Some(keymap_state) = &state.keymap_state {
if let Some(keymap_state) = &state.keymap_state {
let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
keymap_state
.get_keymap()
@@ -599,8 +597,7 @@ impl LinuxClient for WaylandClient {
.to_string()
} else {
"unknown".to_string()
};
Box::new(LinuxKeyboardLayout::new(id))
}
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {

View File

@@ -59,10 +59,9 @@ use crate::platform::{
};
use crate::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform,
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions,
ScaledPixels, ScreenCaptureSource, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
modifiers_from_xinput_info, point, px,
Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform, PlatformDisplay,
PlatformInput, Point, RequestFrameOptions, ScaledPixels, ScreenCaptureSource, ScrollDelta,
Size, TouchPhase, WindowParams, X11Window, modifiers_from_xinput_info, point, px,
};
/// Value for DeviceId parameters which selects all devices.
@@ -1283,16 +1282,14 @@ impl LinuxClient for X11Client {
f(&mut self.0.borrow_mut().common)
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
fn keyboard_layout(&self) -> String {
let state = self.0.borrow();
let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
Box::new(LinuxKeyboardLayout::new(
state
.xkb
.get_keymap()
.layout_get_name(layout_idx)
.to_string(),
))
state
.xkb
.get_keymap()
.layout_get_name(layout_idx)
.to_string()
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {

View File

@@ -7,9 +7,9 @@ use super::{
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, Keymap, MacDispatcher, MacDisplay,
MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource,
SemanticVersion, Task, WindowAppearance, WindowParams, hash,
MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem,
PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, WindowAppearance,
WindowParams, hash,
};
use anyhow::{Context as _, anyhow};
use block::ConcreteBlock;
@@ -825,8 +825,20 @@ impl Platform for MacPlatform {
self.0.lock().validate_menu_command = Some(callback);
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(MacKeyboardLayout::new())
fn keyboard_layout(&self) -> String {
unsafe {
let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
let input_source_id: *mut Object = TISGetInputSourceProperty(
current_keyboard,
kTISPropertyInputSourceID as *const c_void,
);
let input_source_id: *const std::os::raw::c_char =
msg_send![input_source_id, UTF8String];
let input_source_id = CStr::from_ptr(input_source_id).to_str().unwrap();
input_source_id.to_string()
}
}
fn app_path(&self) -> Result<PathBuf> {
@@ -1489,7 +1501,6 @@ unsafe extern "C" {
pub(super) fn LMGetKbdType() -> u16;
pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
pub(super) static kTISPropertyInputSourceID: CFStringRef;
pub(super) static kTISPropertyLocalizedName: CFStringRef;
}
mod security {
@@ -1579,45 +1590,6 @@ impl UTType {
}
}
struct MacKeyboardLayout {
id: String,
name: String,
}
impl PlatformKeyboardLayout for MacKeyboardLayout {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
}
impl MacKeyboardLayout {
fn new() -> Self {
unsafe {
let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
let id: *mut Object = TISGetInputSourceProperty(
current_keyboard,
kTISPropertyInputSourceID as *const c_void,
);
let id: *const std::os::raw::c_char = msg_send![id, UTF8String];
let id = CStr::from_ptr(id).to_str().unwrap().to_string();
let name: *mut Object = TISGetInputSourceProperty(
current_keyboard,
kTISPropertyLocalizedName as *const c_void,
);
let name: *const std::os::raw::c_char = msg_send![name, UTF8String];
let name = CStr::from_ptr(name).to_str().unwrap().to_string();
Self { id, name }
}
}
}
#[cfg(test)]
mod tests {
use crate::ClipboardItem;

View File

@@ -1,8 +1,8 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Size, Task,
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformTextSystem,
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Size, Task, TestDisplay,
TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -223,8 +223,8 @@ impl Platform for TestPlatform {
self.text_system.clone()
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(TestKeyboardLayout)
fn keyboard_layout(&self) -> String {
"zed.keyboard.example".to_string()
}
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
@@ -431,15 +431,3 @@ impl Drop for TestPlatform {
}
}
}
struct TestKeyboardLayout;
impl PlatformKeyboardLayout for TestKeyboardLayout {
fn id(&self) -> &str {
"zed.keyboard.example"
}
fn name(&self) -> &str {
"zed.keyboard.example"
}
}

View File

@@ -39,7 +39,6 @@ pub(crate) fn handle_msg(
WM_CREATE => handle_create_msg(handle, state_ptr),
WM_MOVE => handle_move_msg(handle, lparam, state_ptr),
WM_SIZE => handle_size_msg(wparam, lparam, state_ptr),
WM_GETMINMAXINFO => handle_get_min_max_info_msg(lparam, state_ptr),
WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle),
WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle),
WM_TIMER => handle_timer_msg(handle, wparam, state_ptr),
@@ -141,29 +140,6 @@ fn handle_move_msg(
Some(0)
}
fn handle_get_min_max_info_msg(
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let lock = state_ptr.state.borrow();
if let Some(min_size) = lock.min_size {
let scale_factor = lock.scale_factor;
let boarder_offset = lock.border_offset;
drop(lock);
unsafe {
let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO);
minmax_info.ptMinTrackSize.x =
min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset;
minmax_info.ptMinTrackSize.y =
min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset;
}
Some(0)
} else {
None
}
}
fn handle_size_msg(
wparam: WPARAM,
lparam: LPARAM,

View File

@@ -297,12 +297,8 @@ impl Platform for WindowsPlatform {
self.text_system.clone()
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(
KeyboardLayout::new()
.log_err()
.unwrap_or(KeyboardLayout::unknown()),
)
fn keyboard_layout(&self) -> String {
"unknown".into()
}
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
@@ -840,42 +836,6 @@ fn should_auto_hide_scrollbars() -> Result<bool> {
Ok(ui_settings.AutoHideScrollBars()?)
}
struct KeyboardLayout {
id: String,
name: String,
}
impl PlatformKeyboardLayout for KeyboardLayout {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
}
impl KeyboardLayout {
fn new() -> Result<Self> {
let mut buffer = [0u16; KL_NAMELENGTH as usize];
unsafe { GetKeyboardLayoutNameW(&mut buffer)? };
let id = HSTRING::from_wide(&buffer).to_string();
let entry = windows_registry::LOCAL_MACHINE.open(format!(
"System\\CurrentControlSet\\Control\\Keyboard Layouts\\{}",
id
))?;
let name = entry.get_hstring("Layout Text")?.to_string();
Ok(Self { id, name })
}
fn unknown() -> Self {
Self {
id: "unknown".to_string(),
name: "unknown".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use crate::{ClipboardItem, read_from_clipboard, write_to_clipboard};

View File

@@ -34,7 +34,6 @@ pub(crate) struct WindowsWindow(pub Rc<WindowsWindowStatePtr>);
pub struct WindowsWindowState {
pub origin: Point<Pixels>,
pub logical_size: Size<Pixels>,
pub min_size: Option<Size<Pixels>>,
pub fullscreen_restore_bounds: Bounds<Pixels>,
pub border_offset: WindowBorderOffset,
pub scale_factor: f32,
@@ -80,7 +79,6 @@ impl WindowsWindowState {
current_cursor: Option<HCURSOR>,
display: WindowsDisplay,
gpu_context: &BladeContext,
min_size: Option<Size<Pixels>>,
) -> Result<Self> {
let scale_factor = {
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
@@ -115,7 +113,6 @@ impl WindowsWindowState {
border_offset,
scale_factor,
restore_from_minimized,
min_size,
callbacks,
input_handler,
system_key_handled,
@@ -232,7 +229,6 @@ impl WindowsWindowStatePtr {
context.current_cursor,
context.display,
context.gpu_context,
context.min_size,
)?);
Ok(Rc::new_cyclic(|this| Self {
@@ -354,7 +350,6 @@ struct WindowCreateContext<'a> {
display: WindowsDisplay,
transparent: bool,
is_movable: bool,
min_size: Option<Size<Pixels>>,
executor: ForegroundExecutor,
current_cursor: Option<HCURSOR>,
windows_version: WindowsVersion,
@@ -417,7 +412,6 @@ impl WindowsWindow {
display,
transparent: true,
is_movable: params.is_movable,
min_size: params.window_min_size,
executor,
current_cursor,
windows_version,
@@ -1031,8 +1025,8 @@ type Color = (u8, u8, u8, u8);
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct WindowBorderOffset {
pub(crate) width_offset: i32,
pub(crate) height_offset: i32,
width_offset: i32,
height_offset: i32,
}
impl WindowBorderOffset {

View File

@@ -329,7 +329,7 @@ mod tests {
fn build_wrapper() -> LineWrapper {
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
let cx = TestAppContext::build(dispatcher, None);
let cx = TestAppContext::new(dispatcher, None);
let id = cx.text_system().font_id(&font("Zed Plex Mono")).unwrap();
LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
}

View File

@@ -104,7 +104,7 @@ fn try_test(args: Vec<NestedMeta>, function: TokenStream) -> Result<TokenStream,
{
let cx_varname = format_ident!("cx_{}", ix);
cx_vars.extend(quote!(
let mut #cx_varname = gpui::TestAppContext::build(
let mut #cx_varname = gpui::TestAppContext::new(
dispatcher.clone(),
Some(stringify!(#outer_fn_name)),
);
@@ -166,7 +166,7 @@ fn try_test(args: Vec<NestedMeta>, function: TokenStream) -> Result<TokenStream,
let cx_varname = format_ident!("cx_{}", ix);
let cx_varname_lock = format_ident!("cx_{}_lock", ix);
cx_vars.extend(quote!(
let mut #cx_varname = gpui::TestAppContext::build(
let mut #cx_varname = gpui::TestAppContext::new(
dispatcher.clone(),
Some(stringify!(#outer_fn_name))
);
@@ -184,7 +184,7 @@ fn try_test(args: Vec<NestedMeta>, function: TokenStream) -> Result<TokenStream,
Some("TestAppContext") => {
let cx_varname = format_ident!("cx_{}", ix);
cx_vars.extend(quote!(
let mut #cx_varname = gpui::TestAppContext::build(
let mut #cx_varname = gpui::TestAppContext::new(
dispatcher.clone(),
Some(stringify!(#outer_fn_name))
);

View File

@@ -230,6 +230,23 @@ pub struct Diagnostic {
pub data: Option<Value>,
}
impl Ord for Diagnostic {
fn cmp(&self, other: &Self) -> Ordering {
other
.is_primary
.cmp(&self.is_primary)
.then_with(|| self.is_disk_based.cmp(&other.is_disk_based))
.then_with(|| self.severity.cmp(&other.severity))
.then_with(|| self.message.cmp(&other.message))
}
}
impl PartialOrd for Diagnostic {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// An operation used to synchronize this buffer with its other replicas.
#[derive(Clone, Debug, PartialEq)]
pub enum Operation {

View File

@@ -9,7 +9,7 @@ use std::{
ops::Range,
};
use sum_tree::{self, Bias, SumTree};
use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
use text::{Anchor, AnchorRangeExt, FromAnchor, PointUtf16, ToOffset, Unclipped};
/// A set of diagnostics associated with a given buffer, provided
/// by a single language server.
@@ -246,6 +246,12 @@ impl sum_tree::Item for DiagnosticEntry<Anchor> {
}
impl DiagnosticEntry<Anchor> {
pub fn cmp(&self, other: &Self, buffer: &text::BufferSnapshot) -> Ordering {
self.range
.cmp(&other.range, buffer)
.then_with(|| self.diagnostic.cmp(&other.diagnostic))
}
/// Converts the [DiagnosticEntry] to a different buffer coordinate type.
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticEntry<O> {
DiagnosticEntry {
@@ -256,6 +262,20 @@ impl DiagnosticEntry<Anchor> {
}
}
impl DiagnosticEntry<Unclipped<PointUtf16>> {
pub fn cmp(&self, other: &Self) -> Ordering {
self.range
.start
.0
.row
.cmp(&other.range.start.0.row)
.then_with(|| self.range.start.0.column.cmp(&other.range.start.0.column))
.then_with(|| self.range.end.0.row.cmp(&other.range.end.0.row))
.then_with(|| self.range.end.0.column.cmp(&other.range.end.0.column))
.then_with(|| self.diagnostic.cmp(&other.diagnostic))
}
}
impl Default for Summary {
fn default() -> Self {
Self {

View File

@@ -1913,13 +1913,12 @@ impl CodeLabel {
let runs = highlight_id
.map(|highlight_id| vec![(0..label_length, highlight_id)])
.unwrap_or_default();
let text = if let Some(detail) = item.detail.as_deref().filter(|detail| detail != label) {
let text = if let Some(detail) = &item.detail {
format!("{label} {detail}")
} else if let Some(description) = item
.label_details
.as_ref()
.and_then(|label_details| label_details.description.as_deref())
.filter(|description| description != label)
.and_then(|label_details| label_details.description.as_ref())
{
format!("{label} {description}")
} else {
@@ -2214,100 +2213,4 @@ mod tests {
// Loading an unknown language returns an error.
assert!(languages.language_for_name("Unknown").await.is_err());
}
#[gpui::test]
async fn test_completion_label_omits_duplicate_data() {
let regular_completion_item_1 = lsp::CompletionItem {
label: "regular1".to_string(),
detail: Some("detail1".to_string()),
label_details: Some(lsp::CompletionItemLabelDetails {
detail: None,
description: Some("description 1".to_string()),
}),
..lsp::CompletionItem::default()
};
let regular_completion_item_2 = lsp::CompletionItem {
label: "regular2".to_string(),
label_details: Some(lsp::CompletionItemLabelDetails {
detail: None,
description: Some("description 2".to_string()),
}),
..lsp::CompletionItem::default()
};
let completion_item_with_duplicate_detail_and_proper_description = lsp::CompletionItem {
detail: Some(regular_completion_item_1.label.clone()),
..regular_completion_item_1.clone()
};
let completion_item_with_duplicate_detail = lsp::CompletionItem {
detail: Some(regular_completion_item_1.label.clone()),
label_details: None,
..regular_completion_item_1.clone()
};
let completion_item_with_duplicate_description = lsp::CompletionItem {
label_details: Some(lsp::CompletionItemLabelDetails {
detail: None,
description: Some(regular_completion_item_2.label.clone()),
}),
..regular_completion_item_2.clone()
};
assert_eq!(
CodeLabel::fallback_for_completion(&regular_completion_item_1, None).text,
format!(
"{} {}",
regular_completion_item_1.label,
regular_completion_item_1.detail.unwrap()
),
"LSP completion items with both detail and label_details.description should prefer detail"
);
assert_eq!(
CodeLabel::fallback_for_completion(&regular_completion_item_2, None).text,
format!(
"{} {}",
regular_completion_item_2.label,
regular_completion_item_2
.label_details
.as_ref()
.unwrap()
.description
.as_ref()
.unwrap()
),
"LSP completion items without detail but with label_details.description should use that"
);
assert_eq!(
CodeLabel::fallback_for_completion(
&completion_item_with_duplicate_detail_and_proper_description,
None
)
.text,
format!(
"{} {}",
regular_completion_item_1.label,
regular_completion_item_1
.label_details
.as_ref()
.unwrap()
.description
.as_ref()
.unwrap()
),
"LSP completion items with both detail and label_details.description should prefer description only if the detail duplicates the completion label"
);
assert_eq!(
CodeLabel::fallback_for_completion(&completion_item_with_duplicate_detail, None).text,
regular_completion_item_1.label,
"LSP completion items with duplicate label and detail, should omit the detail"
);
assert_eq!(
CodeLabel::fallback_for_completion(&completion_item_with_duplicate_description, None)
.text,
regular_completion_item_2.label,
"LSP completion items with duplicate label and detail, should omit the detail"
);
}
}

View File

@@ -49,10 +49,6 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
Some(Arc::new(FakeLanguageModel::default()))
}
fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(Arc::new(FakeLanguageModel::default()))
}
fn provided_models(&self, _: &App) -> Vec<Arc<dyn LanguageModel>> {
vec![Arc::new(FakeLanguageModel::default())]
}

View File

@@ -65,14 +65,9 @@ pub struct LanguageModelCacheConfiguration {
pub enum LanguageModelCompletionEvent {
Stop(StopReason),
Text(String),
Thinking {
text: String,
signature: Option<String>,
},
Thinking(String),
ToolUse(LanguageModelToolUse),
StartMessage {
message_id: String,
},
StartMessage { message_id: String },
UsageUpdate(TokenUsage),
}
@@ -307,7 +302,7 @@ pub trait LanguageModel: Send + Sync {
match result {
Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None,
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
Ok(LanguageModelCompletionEvent::Thinking { .. }) => None,
Ok(LanguageModelCompletionEvent::Thinking(_)) => None,
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
@@ -370,7 +365,6 @@ pub trait LanguageModelProvider: 'static {
IconName::ZedAssistant
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>>;
fn recommended_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
Vec::new()

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