Merge remote-tracking branch 'origin/main' into review-only-assistant-changes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
name: "Close Stale Issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 7,9,11 * * 2"
|
||||
- cron: "0 7,9,11 * * 3"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
3
.github/workflows/release_nightly.yml
vendored
3
.github/workflows/release_nightly.yml
vendored
@@ -182,8 +182,7 @@ jobs:
|
||||
runner: buildjet-16vcpu-ubuntu-2204
|
||||
install_nix: true
|
||||
- os: arm Mac
|
||||
# TODO: once other macs are provisioned for nix, remove that constraint from the runner
|
||||
runner: [macOS, ARM64, nix]
|
||||
runner: [macOS, ARM64, test]
|
||||
install_nix: false
|
||||
- os: arm Linux
|
||||
runner: buildjet-16vcpu-ubuntu-2204-arm
|
||||
|
||||
120
Cargo.lock
generated
120
Cargo.lock
generated
@@ -491,8 +491,8 @@ dependencies = [
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"scripting_tool",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -699,6 +699,7 @@ dependencies = [
|
||||
"collections",
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"icons",
|
||||
"language",
|
||||
"language_model",
|
||||
"parking_lot",
|
||||
@@ -3198,6 +3199,7 @@ dependencies = [
|
||||
"extension",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"icons",
|
||||
"language_model",
|
||||
"log",
|
||||
"parking_lot",
|
||||
@@ -3244,9 +3246,7 @@ name = "copilot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tar",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
@@ -3274,7 +3274,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"strum",
|
||||
"task",
|
||||
"theme",
|
||||
@@ -4521,12 +4520,6 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
@@ -5646,7 +5639,6 @@ dependencies = [
|
||||
"askpass",
|
||||
"assistant_settings",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
@@ -6499,6 +6491,14 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icons"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
@@ -7312,6 +7312,7 @@ dependencies = [
|
||||
"google_ai",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_ai",
|
||||
@@ -7324,7 +7325,6 @@ dependencies = [
|
||||
"strum",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -7931,25 +7931,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
version = "547.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "luajit-src"
|
||||
version = "210.5.12+a4f56a4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"which 7.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lyon"
|
||||
version = "1.0.1"
|
||||
@@ -8365,34 +8346,6 @@ dependencies = [
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"either",
|
||||
"futures-util",
|
||||
"mlua-sys",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua-sys"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"lua-src",
|
||||
"luajit-src",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msvc_spectre_libs"
|
||||
version = "0.1.2"
|
||||
@@ -9675,7 +9628,7 @@ name = "perplexity"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.3.0",
|
||||
"zed_extension_api 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12201,30 +12154,6 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
|
||||
|
||||
[[package]]
|
||||
name = "scripting_tool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"buffer_diff",
|
||||
"clock",
|
||||
"collections",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"mlua",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.11.0"
|
||||
@@ -14276,10 +14205,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"auto_update",
|
||||
"call",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"git_ui",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"notifications",
|
||||
@@ -14300,7 +14230,6 @@ dependencies = [
|
||||
"windows 0.61.1",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15008,6 +14937,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"component",
|
||||
"gpui",
|
||||
"icons",
|
||||
"itertools 0.14.0",
|
||||
"linkme",
|
||||
"menu",
|
||||
@@ -16186,18 +16116,6 @@ dependencies = [
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "7.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283"
|
||||
dependencies = [
|
||||
"either",
|
||||
"env_home",
|
||||
"rustix",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.2"
|
||||
@@ -17464,6 +17382,7 @@ dependencies = [
|
||||
"theme_extension",
|
||||
"theme_selector",
|
||||
"time",
|
||||
"title_bar",
|
||||
"toolchain_selector",
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-rust",
|
||||
@@ -17513,7 +17432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -17571,7 +17490,7 @@ dependencies = [
|
||||
name = "zed_test_extension"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.3.0",
|
||||
"zed_extension_api 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17722,7 +17641,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"call",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
|
||||
@@ -70,6 +70,7 @@ members = [
|
||||
"crates/html_to_markdown",
|
||||
"crates/http_client",
|
||||
"crates/http_client_tls",
|
||||
"crates/icons",
|
||||
"crates/image_viewer",
|
||||
"crates/indexed_docs",
|
||||
"crates/inline_completion",
|
||||
@@ -124,7 +125,6 @@ members = [
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/schema_generator",
|
||||
"crates/scripting_tool",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
@@ -275,6 +275,7 @@ gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
http_client_tls = { path = "crates/http_client_tls" }
|
||||
icons = { path = "crates/icons" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion = { path = "crates/inline_completion" }
|
||||
@@ -329,7 +330,6 @@ reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
scripting_tool = { path = "crates/scripting_tool" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
|
||||
1
assets/icons/arrow_right_left.svg
Normal file
1
assets/icons/arrow_right_left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right-left"><path d="m16 3 4 4-4 4"/><path d="M20 7H4"/><path d="m8 21-4-4 4-4"/><path d="M4 17h16"/></svg>
|
||||
|
After Width: | Height: | Size: 316 B |
1
assets/icons/clipboard.svg
Normal file
1
assets/icons/clipboard.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>
|
||||
|
After Width: | Height: | Size: 358 B |
1
assets/icons/cog.svg
Normal file
1
assets/icons/cog.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cog"><path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/></svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@@ -195,7 +195,7 @@
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "assistant::DeployPromptLibrary",
|
||||
"ctrl-k l": "assistant::OpenPromptLibrary",
|
||||
"new": "assistant::NewChat",
|
||||
"ctrl-t": "assistant::NewChat",
|
||||
"ctrl-n": "assistant::NewChat"
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "assistant::DeployPromptLibrary",
|
||||
"cmd-k l": "assistant::OpenPromptLibrary",
|
||||
"cmd-t": "assistant::NewChat",
|
||||
"cmd-n": "assistant::NewChat"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ You should only perform actions that modify the user’s system if explicitly re
|
||||
|
||||
Be concise and direct in your responses.
|
||||
|
||||
The user has opened a project that contains the following root directories/files:
|
||||
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}}` (absolute path: `{{abs_path}}`)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
// Features that can be globally enabled or disabled
|
||||
"features": {
|
||||
// Which edit prediction provider to use.
|
||||
"edit_prediction_provider": "copilot"
|
||||
"edit_prediction_provider": "zed"
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Plex Mono",
|
||||
@@ -184,6 +184,11 @@
|
||||
// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": false,
|
||||
// What to do when go to definition yields no results.
|
||||
//
|
||||
// 1. Do nothing: `none`
|
||||
// 2. Find references for the same symbol: `find_all_references` (default)
|
||||
"go_to_definition_fallback": "find_all_references",
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
@@ -633,11 +638,15 @@
|
||||
"name": "Code Writer",
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"copy-path": true,
|
||||
"create-file": true,
|
||||
"delete-path": true,
|
||||
"diagnostics": true,
|
||||
"edit-files": true,
|
||||
"find-replace-file": true,
|
||||
"edit-files": false,
|
||||
"fetch": true,
|
||||
"list-directory": true,
|
||||
"move-path": true,
|
||||
"now": true,
|
||||
"path-search": true,
|
||||
"read-file": true,
|
||||
@@ -645,7 +654,8 @@
|
||||
"thinking": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notify_when_agent_waiting": true
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -1255,6 +1265,14 @@
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"LaTeX": {
|
||||
"format_on_save": "on",
|
||||
"formatter": "language_server",
|
||||
"language_servers": ["texlab", "..."],
|
||||
"prettier": {
|
||||
"allowed": false
|
||||
}
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false,
|
||||
|
||||
@@ -38,7 +38,7 @@ use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
|
||||
};
|
||||
use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};
|
||||
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
|
||||
@@ -259,7 +259,7 @@ impl AssistantPanel {
|
||||
menu.context(focus_handle.clone())
|
||||
.action("New Chat", Box::new(NewChat))
|
||||
.action("History", Box::new(DeployHistory))
|
||||
.action("Prompt Library", Box::new(DeployPromptLibrary))
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action("Configure", Box::new(ShowConfiguration))
|
||||
.action(zoom_label, Box::new(ToggleZoom))
|
||||
}))
|
||||
@@ -1028,7 +1028,7 @@ impl AssistantPanel {
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
_: &DeployPromptLibrary,
|
||||
_: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
||||
@@ -61,8 +61,8 @@ project.workspace = true
|
||||
prompt_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
scripting_tool.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -3,8 +3,10 @@ use crate::thread::{
|
||||
ThreadEvent, ThreadFeedback,
|
||||
};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolType, ToolUse, ToolUseStatus};
|
||||
use crate::ui::ContextPill;
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
@@ -12,11 +14,11 @@ use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
|
||||
Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||
WindowHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use scripting_tool::{ScriptingTool, ScriptingToolInput};
|
||||
use settings::Settings as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -37,12 +39,12 @@ pub struct ActiveThread {
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
|
||||
rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
|
||||
last_error: Option<ThreadError>,
|
||||
pop_ups: Vec<WindowHandle<ToolReadyPopUp>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -233,7 +235,6 @@ impl ActiveThread {
|
||||
save_thread_task: None,
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
rendered_scripting_tool_uses: HashMap::default(),
|
||||
rendered_tool_use_labels: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
expanded_thinking_segments: HashMap::default(),
|
||||
@@ -246,6 +247,7 @@ impl ActiveThread {
|
||||
}),
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
pop_ups: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
@@ -260,26 +262,6 @@ impl ActiveThread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
for tool_use in thread
|
||||
.read(cx)
|
||||
.scripting_tool_uses_for_message(message.id, cx)
|
||||
{
|
||||
this.render_tool_use_label_markdown(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
this.render_scripting_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.as_ref(),
|
||||
tool_use.input.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
@@ -360,36 +342,6 @@ impl ActiveThread {
|
||||
self.rendered_messages_by_id.remove(id);
|
||||
}
|
||||
|
||||
/// Renders the input of a scripting tool use to Markdown.
|
||||
///
|
||||
/// Does nothing if the tool use does not correspond to the scripting tool.
|
||||
fn render_scripting_tool_use_markdown(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: &str,
|
||||
tool_input: serde_json::Value,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if tool_name != ScriptingTool::NAME {
|
||||
return;
|
||||
}
|
||||
|
||||
let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
|
||||
.map(|input| input.lua_script)
|
||||
.unwrap_or_default();
|
||||
|
||||
let lua_script = render_markdown(
|
||||
format!("```lua\n{lua_script}\n```").into(),
|
||||
self.language_registry.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.rendered_scripting_tool_uses
|
||||
.insert(tool_use_id, lua_script);
|
||||
}
|
||||
|
||||
fn render_tool_use_label_markdown(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
@@ -422,7 +374,14 @@ impl ActiveThread {
|
||||
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
|
||||
self.save_thread(cx);
|
||||
}
|
||||
ThreadEvent::DoneStreaming => {}
|
||||
ThreadEvent::DoneStreaming => {
|
||||
if !self.thread().read(cx).is_generating() {
|
||||
self.show_notification("Your changes have been applied.", window, cx);
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
self.show_notification("There's a tool confirmation needed.", window, cx);
|
||||
}
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
rendered_message.append_text(text, window, cx);
|
||||
@@ -476,13 +435,6 @@ impl ActiveThread {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.render_scripting_tool_use_markdown(
|
||||
tool_use.id,
|
||||
tool_use.name.as_ref(),
|
||||
tool_use.input.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished {
|
||||
@@ -556,6 +508,59 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_notification(
|
||||
&mut self,
|
||||
caption: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, ActiveThread>,
|
||||
) {
|
||||
if !window.is_window_active()
|
||||
&& self.pop_ups.is_empty()
|
||||
&& AssistantSettings::get_global(cx).notify_when_agent_waiting
|
||||
{
|
||||
let caption = caption.into();
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = ToolReadyPopUp::window_options(screen, cx);
|
||||
|
||||
if let Some(screen_window) = cx
|
||||
.open_window(options, |_, cx| {
|
||||
cx.new(|_| ToolReadyPopUp::new(caption.clone()))
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
if let Some(pop_up) = screen_window.entity(cx).log_err() {
|
||||
cx.subscribe_in(&pop_up, window, {
|
||||
|this, _, event, window, cx| match event {
|
||||
ToolReadyPopupEvent::Accepted => {
|
||||
let handle = window.window_handle();
|
||||
cx.activate(true); // Switch back to the Zed application
|
||||
|
||||
// If there are multiple Zed windows, activate the correct one.
|
||||
cx.defer(move |cx| {
|
||||
handle
|
||||
.update(cx, |_view, window, _cx| {
|
||||
window.activate_window();
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
|
||||
this.dismiss_notifications(cx);
|
||||
}
|
||||
ToolReadyPopupEvent::Dismissed => {
|
||||
this.dismiss_notifications(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.pop_ups.push(screen_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a task to save the active thread.
|
||||
///
|
||||
/// Only one task to save the thread will be in flight at a time.
|
||||
@@ -725,13 +730,9 @@ impl ActiveThread {
|
||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||
let context = thread.context_for_message(message_id);
|
||||
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
|
||||
|
||||
// Don't render user messages that are just there for returning tool results.
|
||||
if message.role == Role::User
|
||||
&& (thread.message_has_tool_results(message_id)
|
||||
|| thread.message_has_scripting_tool_results(message_id))
|
||||
{
|
||||
if message.role == Role::User && thread.message_has_tool_results(message_id) {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
@@ -996,32 +997,23 @@ impl ActiveThread {
|
||||
)
|
||||
.child(div().p_2().child(message_content)),
|
||||
),
|
||||
Role::Assistant => {
|
||||
v_flex()
|
||||
.id(("message-container", ix))
|
||||
.ml_2()
|
||||
.pl_2()
|
||||
.pr_4()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(message_content)
|
||||
.when(
|
||||
!tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
|
||||
|parent| {
|
||||
parent.child(
|
||||
v_flex()
|
||||
.children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
)
|
||||
.children(scripting_tool_uses.into_iter().map(|tool_use| {
|
||||
self.render_scripting_tool_use(tool_use, cx)
|
||||
})),
|
||||
)
|
||||
},
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
.ml_2()
|
||||
.pl_2()
|
||||
.pr_4()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(message_content)
|
||||
.when(!tool_uses.is_empty(), |parent| {
|
||||
parent.child(
|
||||
v_flex().children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
@@ -1095,6 +1087,7 @@ impl ActiveThread {
|
||||
|
||||
parent.child(
|
||||
h_flex()
|
||||
.pt_2p5()
|
||||
.px_2p5()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
@@ -1323,21 +1316,6 @@ impl ActiveThread {
|
||||
|
||||
let lighter_border = cx.theme().colors().border.opacity(0.5);
|
||||
|
||||
let tool_icon = match tool_use.name.as_ref() {
|
||||
"bash" => IconName::Terminal,
|
||||
"delete-path" => IconName::Trash,
|
||||
"diagnostics" => IconName::Warning,
|
||||
"edit-files" => IconName::Pencil,
|
||||
"fetch" => IconName::Globe,
|
||||
"list-directory" => IconName::Folder,
|
||||
"now" => IconName::Info,
|
||||
"path-search" => IconName::SearchCode,
|
||||
"read-file" => IconName::Eye,
|
||||
"regex-search" => IconName::Regex,
|
||||
"thinking" => IconName::Brain,
|
||||
_ => IconName::Terminal,
|
||||
};
|
||||
|
||||
div().py_2().child(
|
||||
v_flex()
|
||||
.rounded_lg()
|
||||
@@ -1363,7 +1341,7 @@ impl ActiveThread {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(tool_icon)
|
||||
Icon::new(tool_use.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
@@ -1537,145 +1515,6 @@ impl ActiveThread {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_scripting_tool_use(
|
||||
&self,
|
||||
tool_use: ToolUse,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
div().px_2p5().child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_0p5()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t_md()
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("tool-use-disclosure", is_open).on_click(
|
||||
cx.listener({
|
||||
let tool_use_id = tool_use.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_open = this
|
||||
.expanded_tool_uses
|
||||
.entry(tool_use_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Terminal)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
self.rendered_tool_use_labels
|
||||
.get(&tool_use.id)
|
||||
.cloned(),
|
||||
)
|
||||
.truncate(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(match tool_use.status {
|
||||
ToolUseStatus::Pending => "Pending",
|
||||
ToolUseStatus::Running => "Running",
|
||||
ToolUseStatus::Finished(_) => "Finished",
|
||||
ToolUseStatus::Error(_) => "Error",
|
||||
ToolUseStatus::NeedsConfirmation => "Asking Permission",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let lua_script_markdown =
|
||||
self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Label::new("Input:"))
|
||||
.map(|parent| {
|
||||
if let Some(markdown) = lua_script_markdown {
|
||||
parent.child(markdown)
|
||||
} else {
|
||||
parent.child(Label::new(
|
||||
"Failed to render script input to Markdown",
|
||||
))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|parent| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Result:"))
|
||||
.child(Label::new(output)),
|
||||
),
|
||||
ToolUseStatus::Error(err) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Error:"))
|
||||
.child(Label::new(err)),
|
||||
),
|
||||
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
|
||||
ToolUseStatus::NeedsConfirmation => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Asking Permission")),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
|
||||
let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
|
||||
else {
|
||||
@@ -1751,7 +1590,7 @@ impl ActiveThread {
|
||||
c.ui_text.clone(),
|
||||
c.input.clone(),
|
||||
&c.messages,
|
||||
c.tool_type.clone(),
|
||||
c.tool.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -1761,13 +1600,12 @@ impl ActiveThread {
|
||||
fn handle_deny_tool(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_type: ToolType,
|
||||
_: &ClickEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.deny_tool_use(tool_use_id, tool_type, cx);
|
||||
thread.deny_tool_use(tool_use_id, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1802,7 +1640,7 @@ impl ActiveThread {
|
||||
|
||||
thread
|
||||
.tools_needing_confirmation()
|
||||
.map(|(tool_type, tool)| {
|
||||
.map(|tool| {
|
||||
div()
|
||||
.m_3()
|
||||
.p_2()
|
||||
@@ -1844,7 +1682,6 @@ impl ActiveThread {
|
||||
move |this, event, window, cx| {
|
||||
this.handle_deny_tool(
|
||||
tool_id.clone(),
|
||||
tool_type.clone(),
|
||||
event,
|
||||
window,
|
||||
cx,
|
||||
@@ -1862,6 +1699,16 @@ impl ActiveThread {
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss_notifications(&mut self, cx: &mut Context<'_, ActiveThread>) {
|
||||
for window in self.pop_ups.drain(..) {
|
||||
window
|
||||
.update(cx, |_, window, _| {
|
||||
window.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
|
||||
@@ -32,6 +32,7 @@ use prompt_store::PromptBuilder;
|
||||
use settings::Settings as _;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::AddContextServerModal;
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
@@ -46,6 +47,7 @@ actions!(
|
||||
RemoveAllContext,
|
||||
OpenHistory,
|
||||
OpenConfiguration,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
ChatMode,
|
||||
@@ -86,6 +88,7 @@ pub fn init(
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
cx.observe_new(AddContextServerModal::register).detach();
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod add_context_server_modal;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
@@ -5,13 +7,14 @@ use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use ui::{
|
||||
prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip,
|
||||
};
|
||||
use ui::{prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::assistant::DeployPromptLibrary;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
pub(crate) use add_context_server_modal::AddContextServerModal;
|
||||
|
||||
use crate::AddContextServer;
|
||||
|
||||
pub struct AssistantConfiguration {
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
@@ -170,7 +173,6 @@ impl AssistantConfiguration {
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.mt_1()
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(
|
||||
@@ -309,8 +311,9 @@ impl AssistantConfiguration {
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(true)
|
||||
.tooltip(Tooltip::text("Not yet implemented")),
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(AddContextServer.boxed_clone(), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -352,33 +355,6 @@ impl Render for AssistantConfiguration {
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Create reusable prompts and tag which ones you want sent in every LLM interaction.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-prompt-library", "Open Prompt Library")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
.icon(IconName::Book)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(DeployPromptLibrary.boxed_clone(), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_context_servers_section(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, Modal, ModalFooter, ModalHeader, Section, Tooltip};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::AddContextServer;
|
||||
|
||||
pub struct AddContextServerModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
name_editor: Entity<Editor>,
|
||||
command_editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl AddContextServerModal {
|
||||
pub fn register(
|
||||
workspace: &mut Workspace,
|
||||
_window: Option<&mut Window>,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
Self::new(workspace_handle, window, cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
let command_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
|
||||
name_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text("Context server name", cx);
|
||||
});
|
||||
|
||||
command_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text("Command to run the context server", cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
name_editor,
|
||||
command_editor,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut Context<Self>) {
|
||||
let name = self.name_editor.read(cx).text(cx).trim().to_string();
|
||||
let command = self.command_editor.read(cx).text(cx).trim().to_string();
|
||||
|
||||
if name.is_empty() || command.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
|
||||
let Some(path) = command_parts.next() else {
|
||||
return;
|
||||
};
|
||||
let args = command_parts.collect::<Vec<_>>();
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ContextServerSettings>(fs.clone(), cx, |settings, _| {
|
||||
settings.context_servers.insert(
|
||||
name.into(),
|
||||
ServerConfig {
|
||||
command: Some(ServerCommand {
|
||||
path,
|
||||
args,
|
||||
env: None,
|
||||
}),
|
||||
settings: Some(json!({})),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AddContextServerModal {}
|
||||
|
||||
impl Focusable for AddContextServerModal {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.name_editor.focus_handle(cx).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AddContextServerModal {}
|
||||
|
||||
impl Render for AddContextServerModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_name_empty = self.name_editor.read(cx).text(cx).trim().is_empty();
|
||||
let is_command_empty = self.command_editor.read(cx).text(cx).trim().is_empty();
|
||||
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.key_context("AddContextServerModal")
|
||||
.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 Context Server"))
|
||||
.section(
|
||||
Section::new()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new("Name"))
|
||||
.child(self.name_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new("Command"))
|
||||
.child(self.command_editor.clone()),
|
||||
),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new()
|
||||
.start_slot(
|
||||
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)
|
||||
.map(|button| {
|
||||
if is_name_empty {
|
||||
button.tooltip(Tooltip::text("Name is required"))
|
||||
} else if is_command_empty {
|
||||
button.tooltip(Tooltip::text("Command is required"))
|
||||
} else {
|
||||
button
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ use client::zed_urls;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
|
||||
WeakEntity,
|
||||
action_with_deprecated_aliases, prelude::*, Action, AnyElement, App, AsyncWindowContext,
|
||||
Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
|
||||
Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
@@ -29,7 +29,7 @@ use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Ta
|
||||
use util::ResultExt as _;
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
|
||||
use zed_actions::assistant::ToggleFocus;
|
||||
|
||||
use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
|
||||
@@ -43,6 +43,12 @@ use crate::{
|
||||
OpenHistory,
|
||||
};
|
||||
|
||||
action_with_deprecated_aliases!(
|
||||
assistant,
|
||||
OpenPromptLibrary,
|
||||
["assistant::DeployPromptLibrary"]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
@@ -65,6 +71,14 @@ pub fn init(cx: &mut App) {
|
||||
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
@@ -303,7 +317,7 @@ impl AssistantPanel {
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
_: &DeployPromptLibrary,
|
||||
_: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
mod completion_provider;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::Editor;
|
||||
use editor::display_map::{Crease, FoldId};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||
use file_context_picker::render_file_context_entry;
|
||||
use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use gpui::{
|
||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::ProjectPath;
|
||||
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
||||
use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
|
||||
};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
@@ -34,10 +43,31 @@ enum ContextPickerMode {
|
||||
Thread,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ContextPickerMode {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"file" => Ok(Self::File),
|
||||
"fetch" => Ok(Self::Fetch),
|
||||
"thread" => Ok(Self::Thread),
|
||||
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextPickerMode {
|
||||
pub fn mention_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
Self::File => "file",
|
||||
Self::Fetch => "fetch",
|
||||
Self::Thread => "thread",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::File => "File/Directory",
|
||||
Self::File => "Files & Directories",
|
||||
Self::Fetch => "Fetch",
|
||||
Self::Thread => "Thread",
|
||||
}
|
||||
@@ -63,7 +93,6 @@ enum ContextPickerState {
|
||||
pub(super) struct ContextPicker {
|
||||
mode: ContextPickerState,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: WeakEntity<Editor>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
@@ -74,7 +103,6 @@ impl ContextPicker {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
editor: WeakEntity<Editor>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -88,7 +116,6 @@ impl ContextPicker {
|
||||
workspace,
|
||||
context_store,
|
||||
thread_store,
|
||||
editor,
|
||||
confirm_behavior,
|
||||
}
|
||||
}
|
||||
@@ -109,10 +136,7 @@ impl ContextPicker {
|
||||
.enumerate()
|
||||
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
||||
|
||||
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
|
||||
if self.allow_threads() {
|
||||
modes.push(ContextPickerMode::Thread);
|
||||
}
|
||||
let modes = supported_context_picker_modes(&self.thread_store);
|
||||
|
||||
let menu = menu
|
||||
.when(has_recent, |menu| {
|
||||
@@ -174,7 +198,6 @@ impl ContextPicker {
|
||||
FileContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.editor.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
@@ -278,7 +301,7 @@ impl ContextPicker {
|
||||
};
|
||||
|
||||
let task = context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_file_from_path(project_path.clone(), cx)
|
||||
context_store.add_file_from_path(project_path.clone(), true, cx)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
|
||||
@@ -308,7 +331,7 @@ impl ContextPicker {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let thread = open_thread_task.await?;
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_thread(thread, cx);
|
||||
context_store.add_thread(thread, true, cx);
|
||||
})?;
|
||||
|
||||
this.update(cx, |_this, cx| cx.notify())
|
||||
@@ -328,7 +351,7 @@ impl ContextPicker {
|
||||
|
||||
let mut current_files = context_store.file_paths(cx);
|
||||
|
||||
if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
|
||||
if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) {
|
||||
current_files.insert(active_path);
|
||||
}
|
||||
|
||||
@@ -384,16 +407,6 @@ impl ContextPicker {
|
||||
|
||||
recent
|
||||
}
|
||||
|
||||
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
||||
let active_item = workspace.active_item(cx)?;
|
||||
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let buffer = editor.buffer().read(cx).as_singleton()?;
|
||||
|
||||
let path = buffer.read(cx).file()?.path().to_path_buf();
|
||||
Some(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContextPicker {}
|
||||
@@ -429,3 +442,212 @@ enum RecentEntry {
|
||||
},
|
||||
Thread(ThreadContextEntry),
|
||||
}
|
||||
|
||||
fn supported_context_picker_modes(
|
||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||
) -> Vec<ContextPickerMode> {
|
||||
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
|
||||
if thread_store.is_some() {
|
||||
modes.push(ContextPickerMode::Thread);
|
||||
}
|
||||
modes
|
||||
}
|
||||
|
||||
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
||||
let active_item = workspace.active_item(cx)?;
|
||||
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let buffer = editor.buffer().read(cx).as_singleton()?;
|
||||
|
||||
let path = buffer.read(cx).file()?.path().to_path_buf();
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn recent_context_picker_entries(
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Vec<RecentEntry> {
|
||||
let mut recent = Vec::with_capacity(6);
|
||||
|
||||
let mut current_files = context_store.read(cx).file_paths(cx);
|
||||
|
||||
let workspace = workspace.read(cx);
|
||||
|
||||
if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
|
||||
current_files.insert(active_path);
|
||||
}
|
||||
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
recent.extend(
|
||||
workspace
|
||||
.recent_navigation_history_iter(cx)
|
||||
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
|
||||
.take(4)
|
||||
.filter_map(|(project_path, _)| {
|
||||
project
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
.map(|worktree| RecentEntry::File {
|
||||
project_path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
let mut current_threads = context_store.read(cx).thread_ids();
|
||||
|
||||
if let Some(active_thread) = workspace
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|panel| panel.read(cx).active_thread(cx))
|
||||
{
|
||||
current_threads.insert(active_thread.read(cx).id().clone());
|
||||
}
|
||||
|
||||
if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
|
||||
recent.extend(
|
||||
thread_store
|
||||
.read(cx)
|
||||
.threads()
|
||||
.into_iter()
|
||||
.filter(|thread| !current_threads.contains(&thread.id))
|
||||
.take(2)
|
||||
.map(|thread| {
|
||||
RecentEntry::Thread(ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
recent
|
||||
}
|
||||
|
||||
pub(crate) fn insert_crease_for_mention(
|
||||
excerpt_id: ExcerptId,
|
||||
crease_start: text::Anchor,
|
||||
content_len: usize,
|
||||
crease_label: SharedString,
|
||||
crease_icon_path: SharedString,
|
||||
editor_entity: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
editor_entity.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
|
||||
let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
crease_icon_path,
|
||||
crease_label,
|
||||
editor_entity.downgrade(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||
|
||||
let crease = Crease::inline(
|
||||
start..end,
|
||||
placeholder.clone(),
|
||||
fold_toggle("mention"),
|
||||
render_trailer,
|
||||
);
|
||||
|
||||
editor.insert_creases(vec![crease.clone()], cx);
|
||||
editor.fold_creases(vec![crease], false, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_fold_icon_button(
|
||||
icon_path: SharedString,
|
||||
label: SharedString,
|
||||
editor: WeakEntity<Editor>,
|
||||
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
||||
Arc::new({
|
||||
move |fold_id, fold_range, cx| {
|
||||
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor
|
||||
.buffer()
|
||||
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
|
||||
|
||||
let is_in_pending_selection = || {
|
||||
editor
|
||||
.selections
|
||||
.pending
|
||||
.as_ref()
|
||||
.is_some_and(|pending_selection| {
|
||||
pending_selection
|
||||
.selection
|
||||
.range()
|
||||
.includes(&fold_range, &snapshot)
|
||||
})
|
||||
};
|
||||
|
||||
let mut is_in_complete_selection = || {
|
||||
editor
|
||||
.selections
|
||||
.disjoint_in_range::<usize>(fold_range.clone(), cx)
|
||||
.into_iter()
|
||||
.any(|selection| {
|
||||
// This is needed to cover a corner case, if we just check for an existing
|
||||
// selection in the fold range, having a cursor at the start of the fold
|
||||
// marks it as selected. Non-empty selections don't cause this.
|
||||
let length = selection.end - selection.start;
|
||||
length > 0
|
||||
})
|
||||
};
|
||||
|
||||
is_in_pending_selection() || is_in_complete_selection()
|
||||
})
|
||||
});
|
||||
|
||||
ButtonLike::new(fold_id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(is_in_text_selection)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::from_path(icon_path.clone())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn fold_toggle(
|
||||
name: &'static str,
|
||||
) -> impl Fn(
|
||||
MultiBufferRow,
|
||||
bool,
|
||||
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
) -> AnyElement {
|
||||
move |row, is_folded, fold, _window, _cx| {
|
||||
Disclosure::new((name, row.0 as u64), !is_folded)
|
||||
.toggle_state(is_folded)
|
||||
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
1024
crates/assistant2/src/context_picker/completion_provider.rs
Normal file
1024
crates/assistant2/src/context_picker/completion_provider.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -81,77 +81,80 @@ impl FetchContextPickerDelegate {
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
|
||||
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
format!("https://{url}")
|
||||
} else {
|
||||
url
|
||||
};
|
||||
pub(crate) async fn fetch_url_content(
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
url: String,
|
||||
) -> Result<String> {
|
||||
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
format!("https://{url}")
|
||||
} else {
|
||||
url
|
||||
};
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading response body")?;
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading response body")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let Some(content_type) = response.headers().get("content-type") else {
|
||||
bail!("missing Content-Type header");
|
||||
};
|
||||
let content_type = content_type
|
||||
.to_str()
|
||||
.context("invalid Content-Type header")?;
|
||||
let content_type = match content_type {
|
||||
"text/html" => ContentType::Html,
|
||||
"text/plain" => ContentType::Plaintext,
|
||||
"application/json" => ContentType::Json,
|
||||
_ => ContentType::Html,
|
||||
};
|
||||
|
||||
match content_type {
|
||||
ContentType::Html => {
|
||||
let mut handlers: Vec<TagHandler> = vec![
|
||||
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
||||
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
||||
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
||||
Rc::new(RefCell::new(markdown::ListHandler)),
|
||||
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
||||
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
||||
];
|
||||
if url.contains("wikipedia.org") {
|
||||
use html_to_markdown::structure::wikipedia;
|
||||
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
||||
handlers.push(Rc::new(
|
||||
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
||||
));
|
||||
} else {
|
||||
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..], &mut handlers)
|
||||
}
|
||||
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
||||
ContentType::Json => {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
|
||||
let Some(content_type) = response.headers().get("content-type") else {
|
||||
bail!("missing Content-Type header");
|
||||
};
|
||||
let content_type = content_type
|
||||
.to_str()
|
||||
.context("invalid Content-Type header")?;
|
||||
let content_type = match content_type {
|
||||
"text/html" => ContentType::Html,
|
||||
"text/plain" => ContentType::Plaintext,
|
||||
"application/json" => ContentType::Json,
|
||||
_ => ContentType::Html,
|
||||
};
|
||||
|
||||
match content_type {
|
||||
ContentType::Html => {
|
||||
let mut handlers: Vec<TagHandler> = vec![
|
||||
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
||||
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
||||
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
||||
Rc::new(RefCell::new(markdown::ListHandler)),
|
||||
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
||||
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
||||
];
|
||||
if url.contains("wikipedia.org") {
|
||||
use html_to_markdown::structure::wikipedia;
|
||||
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
||||
handlers.push(Rc::new(
|
||||
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
||||
));
|
||||
} else {
|
||||
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..], &mut handlers)
|
||||
}
|
||||
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
||||
ContentType::Json => {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
|
||||
Ok(format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(&json)?
|
||||
))
|
||||
}
|
||||
Ok(format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(&json)?
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +211,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let text = cx
|
||||
.background_spawn(Self::build_message(http_client, url.clone()))
|
||||
.background_spawn(fetch_url_content(http_client, url.clone()))
|
||||
.await?;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::actions::FoldAt;
|
||||
use editor::display_map::{Crease, FoldId};
|
||||
use editor::scroll::Autoscroll;
|
||||
use editor::{Anchor, AnchorRangeExt, Editor, FoldPlaceholder, ToPoint};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful,
|
||||
Task, WeakEntity,
|
||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||
};
|
||||
use multi_buffer::{MultiBufferPoint, MultiBufferRow};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||
use rope::Point;
|
||||
use text::SelectionGoal;
|
||||
use ui::{prelude::*, ButtonLike, Disclosure, ListItem, TintColor, Tooltip};
|
||||
use ui::{prelude::*, ListItem, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
@@ -34,7 +24,6 @@ impl FileContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: WeakEntity<Editor>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
@@ -43,7 +32,6 @@ impl FileContextPicker {
|
||||
let delegate = FileContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
editor,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
@@ -68,7 +56,6 @@ impl Render for FileContextPicker {
|
||||
pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: WeakEntity<Editor>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
@@ -79,95 +66,18 @@ impl FileContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: WeakEntity<Editor>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
editor,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let recent_matches = workspace
|
||||
.recent_navigation_history(Some(10), cx)
|
||||
.into_iter()
|
||||
.filter_map(|(project_path, _)| {
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
});
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.entries(false, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: entry.is_dir(),
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(recent_matches.chain(file_matches).collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Entries,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileContextPickerDelegate {
|
||||
@@ -204,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
// TODO: This should be probably be run in the background.
|
||||
@@ -222,14 +132,6 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let file_name = mat
|
||||
.path
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy().into_owned())
|
||||
.unwrap_or(mat.path_prefix.to_string());
|
||||
|
||||
let full_path = mat.path.display().to_string();
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
@@ -237,106 +139,13 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
|
||||
let is_directory = mat.is_dir;
|
||||
|
||||
let Some(editor_entity) = self.editor.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
editor_entity.update(cx, |editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
// Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert.
|
||||
{
|
||||
let mut selections = editor.selections.all::<MultiBufferPoint>(cx);
|
||||
|
||||
for selection in selections.iter_mut() {
|
||||
if selection.is_empty() {
|
||||
let old_head = selection.head();
|
||||
let new_head = MultiBufferPoint::new(
|
||||
old_head.row,
|
||||
old_head.column.saturating_sub(1),
|
||||
);
|
||||
selection.set_head(new_head, SelectionGoal::None);
|
||||
}
|
||||
}
|
||||
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select(selections)
|
||||
});
|
||||
}
|
||||
|
||||
let start_anchors = {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor
|
||||
.selections
|
||||
.all::<Point>(cx)
|
||||
.into_iter()
|
||||
.map(|selection| snapshot.anchor_before(selection.start))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
editor.insert(&full_path, window, cx);
|
||||
|
||||
let end_anchors = {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor
|
||||
.selections
|
||||
.all::<Point>(cx)
|
||||
.into_iter()
|
||||
.map(|selection| snapshot.anchor_after(selection.end))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
editor.insert("\n", window, cx); // Needed to end the fold
|
||||
|
||||
let file_icon = if is_directory {
|
||||
FileIcons::get_folder_icon(false, cx)
|
||||
} else {
|
||||
FileIcons::get_icon(&Path::new(&full_path), cx)
|
||||
}
|
||||
.unwrap_or_else(|| SharedString::new(""));
|
||||
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
file_icon,
|
||||
file_name.into(),
|
||||
editor_entity.downgrade(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let mut rows_to_fold = BTreeSet::new();
|
||||
let crease_iter = start_anchors
|
||||
.into_iter()
|
||||
.zip(end_anchors)
|
||||
.map(|(start, end)| {
|
||||
rows_to_fold.insert(MultiBufferRow(start.to_point(&buffer).row));
|
||||
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder.clone(),
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
});
|
||||
|
||||
editor.insert_creases(crease_iter, cx);
|
||||
|
||||
for buffer_row in rows_to_fold {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let Some(task) = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
if is_directory {
|
||||
context_store.add_directory(project_path, cx)
|
||||
context_store.add_directory(project_path, true, cx)
|
||||
} else {
|
||||
context_store.add_file_from_path(project_path, cx)
|
||||
context_store.add_file_from_path(project_path, true, cx)
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
@@ -390,6 +199,80 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_paths(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let recent_matches = workspace
|
||||
.recent_navigation_history(Some(10), cx)
|
||||
.into_iter()
|
||||
.filter_map(|(project_path, _)| {
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
});
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.entries(false, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: entry.is_dir(),
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(recent_matches.chain(file_matches).collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Entries,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_file_context_entry(
|
||||
id: ElementId,
|
||||
path: &Path,
|
||||
@@ -484,85 +367,3 @@ pub fn render_file_context_entry(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_fold_icon_button(
|
||||
icon: SharedString,
|
||||
label: SharedString,
|
||||
editor: WeakEntity<Editor>,
|
||||
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
||||
Arc::new(move |fold_id, fold_range, cx| {
|
||||
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor
|
||||
.buffer()
|
||||
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
|
||||
|
||||
let is_in_pending_selection = || {
|
||||
editor
|
||||
.selections
|
||||
.pending
|
||||
.as_ref()
|
||||
.is_some_and(|pending_selection| {
|
||||
pending_selection
|
||||
.selection
|
||||
.range()
|
||||
.includes(&fold_range, &snapshot)
|
||||
})
|
||||
};
|
||||
|
||||
let mut is_in_complete_selection = || {
|
||||
editor
|
||||
.selections
|
||||
.disjoint_in_range::<usize>(fold_range.clone(), cx)
|
||||
.into_iter()
|
||||
.any(|selection| {
|
||||
// This is needed to cover a corner case, if we just check for an existing
|
||||
// selection in the fold range, having a cursor at the start of the fold
|
||||
// marks it as selected. Non-empty selections don't cause this.
|
||||
let length = selection.end - selection.start;
|
||||
length > 0
|
||||
})
|
||||
};
|
||||
|
||||
is_in_pending_selection() || is_in_complete_selection()
|
||||
})
|
||||
});
|
||||
|
||||
ButtonLike::new(fold_id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(is_in_text_selection)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::from_path(icon.clone())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
fn fold_toggle(
|
||||
name: &'static str,
|
||||
) -> impl Fn(
|
||||
MultiBufferRow,
|
||||
bool,
|
||||
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
) -> AnyElement {
|
||||
move |row, is_folded, fold, _window, _cx| {
|
||||
Disclosure::new((name, row.0 as u64), !is_folded)
|
||||
.toggle_state(is_folded)
|
||||
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,45 +110,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Ok(threads) = self.thread_store.update(cx, |this, _cx| {
|
||||
this.threads()
|
||||
.into_iter()
|
||||
.map(|thread| ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}) else {
|
||||
let Some(threads) = self.thread_store.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
let search_task = cx.background_spawn(async move {
|
||||
if query.is_empty() {
|
||||
threads
|
||||
} else {
|
||||
let candidates = threads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| threads[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
});
|
||||
|
||||
let search_task = search_threads(query, threads, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = search_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -176,7 +142,9 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| context_store.add_thread(thread, cx))
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_thread(thread, true, cx)
|
||||
})
|
||||
.ok();
|
||||
|
||||
match this.delegate.confirm_behavior {
|
||||
@@ -248,3 +216,46 @@ pub fn render_thread_context_entry(
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn search_threads(
|
||||
query: String,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<ThreadContextEntry>> {
|
||||
let threads = thread_store.update(cx, |this, _cx| {
|
||||
this.threads()
|
||||
.into_iter()
|
||||
.map(|thread| ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_spawn(async move {
|
||||
if query.is_empty() {
|
||||
threads
|
||||
} else {
|
||||
let candidates = threads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| threads[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ impl ContextStore {
|
||||
pub fn add_file_from_path(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -86,7 +87,9 @@ impl ContextStore {
|
||||
let already_included = this.update(cx, |this, _cx| {
|
||||
match this.will_include_buffer(buffer_id, &project_path.path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
this.remove_context(context_id);
|
||||
if remove_if_exists {
|
||||
this.remove_context(context_id);
|
||||
}
|
||||
true
|
||||
}
|
||||
Some(FileInclusion::InDirectory(_)) => true,
|
||||
@@ -157,6 +160,7 @@ impl ContextStore {
|
||||
pub fn add_directory(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -169,7 +173,9 @@ impl ContextStore {
|
||||
|
||||
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
|
||||
{
|
||||
self.remove_context(context_id);
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -256,9 +262,16 @@ impl ContextStore {
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn add_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
|
||||
pub fn add_thread(
|
||||
&mut self,
|
||||
thread: Entity<Thread>,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
|
||||
self.remove_context(context_id);
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id);
|
||||
}
|
||||
} else {
|
||||
self.insert_thread(thread, cx);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ impl ContextStrip {
|
||||
pub fn new(
|
||||
context_store: Entity<ContextStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: WeakEntity<Editor>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
suggest_context_kind: SuggestContextKind,
|
||||
@@ -51,7 +50,6 @@ impl ContextStrip {
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
context_store.downgrade(),
|
||||
editor.clone(),
|
||||
ConfirmBehavior::KeepOpen,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -861,7 +861,6 @@ impl PromptEditor<BufferCodegen> {
|
||||
ContextStrip::new(
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
prompt_editor.downgrade(),
|
||||
thread_store.clone(),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::Thread,
|
||||
@@ -1014,7 +1013,6 @@ impl PromptEditor<TerminalCodegen> {
|
||||
ContextStrip::new(
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
prompt_editor.downgrade(),
|
||||
thread_store.clone(),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::Thread,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
||||
use fs::Fs;
|
||||
use git::ExpandCommitEditor;
|
||||
use git_ui::git_panel;
|
||||
@@ -13,10 +13,8 @@ use gpui::{
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use text::Bias;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
@@ -25,7 +23,7 @@ use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
@@ -68,16 +66,30 @@ impl MessageEditor {
|
||||
let mut editor = Editor::auto_height(10, window, cx);
|
||||
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
min_entries_visible: 12,
|
||||
max_entries_visible: 12,
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
Some(thread_store.clone()),
|
||||
editor_entity,
|
||||
))));
|
||||
});
|
||||
|
||||
let inline_context_picker = cx.new(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_store.downgrade(),
|
||||
editor.downgrade(),
|
||||
ConfirmBehavior::Close,
|
||||
window,
|
||||
cx,
|
||||
@@ -88,7 +100,6 @@ impl MessageEditor {
|
||||
ContextStrip::new(
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
editor.downgrade(),
|
||||
Some(thread_store.clone()),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::File,
|
||||
@@ -98,7 +109,6 @@ impl MessageEditor {
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&editor, window, Self::handle_editor_event),
|
||||
cx.subscribe_in(
|
||||
&inline_context_picker,
|
||||
window,
|
||||
@@ -232,34 +242,6 @@ impl MessageEditor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let newest_cursor = editor.selections.newest::<Point>(cx).head();
|
||||
if newest_cursor.column > 0 {
|
||||
let behind_cursor = snapshot.clip_point(
|
||||
Point::new(newest_cursor.row, newest_cursor.column - 1),
|
||||
Bias::Left,
|
||||
);
|
||||
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
|
||||
if char_behind_cursor == Some('@') {
|
||||
self.inline_context_picker_menu_handle.show(window, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_inline_context_picker_event(
|
||||
&mut self,
|
||||
_inline_context_picker: &Entity<ContextPicker>,
|
||||
@@ -616,6 +598,7 @@ impl Render for MessageEditor {
|
||||
background: editor_bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -23,7 +23,6 @@ use project::{Project, Worktree};
|
||||
use prompt_store::{
|
||||
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
|
||||
};
|
||||
use scripting_tool::{ScriptingSession, ScriptingTool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _};
|
||||
@@ -34,7 +33,7 @@ use crate::thread_store::{
|
||||
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
|
||||
SerializedToolUse,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, PendingToolUseStatus, ToolType, ToolUse, ToolUseState};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
@@ -194,8 +193,6 @@ pub struct Thread {
|
||||
action_log: Entity<ActionLog>,
|
||||
last_restore_checkpoint: Option<LastRestoreCheckpoint>,
|
||||
pending_checkpoint: Option<ThreadCheckpoint>,
|
||||
scripting_session: Entity<ScriptingSession>,
|
||||
scripting_tool_use: ToolUseState,
|
||||
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
|
||||
cumulative_token_usage: TokenUsage,
|
||||
feedback: Option<ThreadFeedback>,
|
||||
@@ -230,8 +227,6 @@ impl Thread {
|
||||
last_restore_checkpoint: None,
|
||||
pending_checkpoint: None,
|
||||
tool_use: ToolUseState::new(tools.clone()),
|
||||
scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
|
||||
scripting_tool_use: ToolUseState::new(tools),
|
||||
action_log: cx.new(|_| ActionLog::new()),
|
||||
initial_project_snapshot: {
|
||||
let project_snapshot = Self::project_snapshot(project.clone(), cx);
|
||||
@@ -274,14 +269,7 @@ impl Thread {
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use =
|
||||
ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
|
||||
name != ScriptingTool::NAME
|
||||
});
|
||||
let scripting_tool_use =
|
||||
ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
|
||||
name == ScriptingTool::NAME
|
||||
});
|
||||
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||
ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |_| true);
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
@@ -320,8 +308,6 @@ impl Thread {
|
||||
tools,
|
||||
tool_use,
|
||||
action_log: cx.new(|_| ActionLog::new()),
|
||||
scripting_session,
|
||||
scripting_tool_use,
|
||||
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
|
||||
// TODO: persist token usage?
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
@@ -384,37 +370,17 @@ impl Thread {
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.find(|tool_use| &tool_use.id == id)
|
||||
.or_else(|| {
|
||||
self.scripting_tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.find(|tool_use| &tool_use.id == id)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tools_needing_confirmation(&self) -> impl Iterator<Item = (ToolType, &PendingToolUse)> {
|
||||
pub fn tools_needing_confirmation(&self) -> impl Iterator<Item = &PendingToolUse> {
|
||||
self.tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter_map(|tool_use| {
|
||||
if let PendingToolUseStatus::NeedsConfirmation(confirmation) = &tool_use.status {
|
||||
Some((confirmation.tool_type.clone(), tool_use))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(
|
||||
self.scripting_tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter_map(|tool_use| {
|
||||
if tool_use.status.needs_confirmation() {
|
||||
Some((ToolType::ScriptingTool, tool_use))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)
|
||||
.filter(|tool_use| tool_use.status.needs_confirmation())
|
||||
}
|
||||
|
||||
pub fn has_pending_tool_uses(&self) -> bool {
|
||||
!self.tool_use.pending_tool_uses().is_empty()
|
||||
}
|
||||
|
||||
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
|
||||
@@ -547,25 +513,18 @@ impl Thread {
|
||||
|
||||
/// Returns whether all of the tool uses have finished running.
|
||||
pub fn all_tools_finished(&self) -> bool {
|
||||
let mut all_pending_tool_uses = self
|
||||
.tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.chain(self.scripting_tool_use.pending_tool_uses());
|
||||
|
||||
// If the only pending tool uses left are the ones with errors, then
|
||||
// that means that we've finished running all of the pending tools.
|
||||
all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
|
||||
self.tool_use
|
||||
.pending_tool_uses()
|
||||
.iter()
|
||||
.all(|tool_use| tool_use.status.is_error())
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||
self.tool_use.tool_uses_for_message(id, cx)
|
||||
}
|
||||
|
||||
pub fn scripting_tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||
self.scripting_tool_use.tool_uses_for_message(id, cx)
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
self.tool_use.tool_results_for_message(id)
|
||||
}
|
||||
@@ -574,21 +533,10 @@ impl Thread {
|
||||
self.tool_use.tool_result(id)
|
||||
}
|
||||
|
||||
pub fn scripting_tool_results_for_message(
|
||||
&self,
|
||||
id: MessageId,
|
||||
) -> Vec<&LanguageModelToolResult> {
|
||||
self.scripting_tool_use.tool_results_for_message(id)
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.scripting_tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
@@ -709,7 +657,6 @@ impl Thread {
|
||||
tool_uses: this
|
||||
.tool_uses_for_message(message.id, cx)
|
||||
.into_iter()
|
||||
.chain(this.scripting_tool_uses_for_message(message.id, cx))
|
||||
.map(|tool_use| SerializedToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
@@ -719,7 +666,6 @@ impl Thread {
|
||||
tool_results: this
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(this.scripting_tool_results_for_message(message.id))
|
||||
.map(|tool_result| SerializedToolResult {
|
||||
tool_use_id: tool_result.tool_use_id.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
@@ -783,11 +729,12 @@ impl Thread {
|
||||
|
||||
// Note that Cline supports `.clinerules` being a directory, but that is not currently
|
||||
// supported. This doesn't seem to occur often in GitHub repositories.
|
||||
const RULES_FILE_NAMES: [&'static str; 5] = [
|
||||
const RULES_FILE_NAMES: [&'static str; 6] = [
|
||||
".rules",
|
||||
".cursorrules",
|
||||
".windsurfrules",
|
||||
".clinerules",
|
||||
".github/copilot-instructions.md",
|
||||
"CLAUDE.md",
|
||||
];
|
||||
let selected_rules_file = RULES_FILE_NAMES
|
||||
@@ -852,15 +799,6 @@ impl Thread {
|
||||
let mut request = self.to_completion_request(request_kind, cx);
|
||||
request.tools = {
|
||||
let mut tools = Vec::new();
|
||||
|
||||
if self.tools.is_scripting_tool_enabled() {
|
||||
tools.push(LanguageModelRequestTool {
|
||||
name: ScriptingTool::NAME.into(),
|
||||
description: ScriptingTool::DESCRIPTION.into(),
|
||||
input_schema: ScriptingTool::input_schema(),
|
||||
});
|
||||
}
|
||||
|
||||
tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| {
|
||||
LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
@@ -921,8 +859,6 @@ impl Thread {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
self.scripting_tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
@@ -939,8 +875,6 @@ impl Thread {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
self.scripting_tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
@@ -1087,19 +1021,11 @@ impl Thread {
|
||||
.iter()
|
||||
.rfind(|message| message.role == Role::Assistant)
|
||||
{
|
||||
if tool_use.name.as_ref() == ScriptingTool::NAME {
|
||||
thread.scripting_tool_use.request_tool_use(
|
||||
last_assistant_message.id,
|
||||
tool_use,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
thread.tool_use.request_tool_use(
|
||||
last_assistant_message.id,
|
||||
tool_use,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
thread.tool_use.request_tool_use(
|
||||
last_assistant_message.id,
|
||||
tool_use,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1264,15 +1190,16 @@ impl Thread {
|
||||
tool_use.ui_text.clone(),
|
||||
tool_use.input.clone(),
|
||||
messages.clone(),
|
||||
ToolType::NonScriptingTool(tool),
|
||||
tool,
|
||||
);
|
||||
cx.emit(ThreadEvent::ToolConfirmationNeeded);
|
||||
} else {
|
||||
self.run_tool(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
tool_use.input.clone(),
|
||||
&messages,
|
||||
ToolType::NonScriptingTool(tool),
|
||||
tool,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1282,33 +1209,13 @@ impl Thread {
|
||||
tool_use.ui_text.clone(),
|
||||
tool_use.input.clone(),
|
||||
&messages,
|
||||
ToolType::NonScriptingTool(tool),
|
||||
tool,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let pending_scripting_tool_uses = self
|
||||
.scripting_tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for scripting_tool_use in pending_scripting_tool_uses.iter() {
|
||||
self.scripting_tool_use.confirm_tool_use(
|
||||
scripting_tool_use.id.clone(),
|
||||
scripting_tool_use.ui_text.clone(),
|
||||
scripting_tool_use.input.clone(),
|
||||
messages.clone(),
|
||||
ToolType::ScriptingTool,
|
||||
);
|
||||
}
|
||||
|
||||
pending_tool_uses
|
||||
.into_iter()
|
||||
.chain(pending_scripting_tool_uses)
|
||||
}
|
||||
|
||||
pub fn run_tool(
|
||||
@@ -1317,21 +1224,12 @@ impl Thread {
|
||||
ui_text: impl Into<SharedString>,
|
||||
input: serde_json::Value,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
tool_type: ToolType,
|
||||
tool: Arc<dyn Tool>,
|
||||
cx: &mut Context<'_, Thread>,
|
||||
) {
|
||||
match tool_type {
|
||||
ToolType::ScriptingTool => {
|
||||
let task = self.spawn_scripting_tool_use(tool_use_id.clone(), input, cx);
|
||||
self.scripting_tool_use
|
||||
.run_pending_tool(tool_use_id, ui_text.into(), task);
|
||||
}
|
||||
ToolType::NonScriptingTool(tool) => {
|
||||
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
|
||||
self.tool_use
|
||||
.run_pending_tool(tool_use_id, ui_text.into(), task);
|
||||
}
|
||||
}
|
||||
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
|
||||
self.tool_use
|
||||
.run_pending_tool(tool_use_id, ui_text.into(), task);
|
||||
}
|
||||
|
||||
fn spawn_tool_use(
|
||||
@@ -1375,60 +1273,6 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_scripting_tool_use(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
input: serde_json::Value,
|
||||
cx: &mut Context<Thread>,
|
||||
) -> Task<()> {
|
||||
let task = match ScriptingTool::deserialize_input(input) {
|
||||
Err(err) => Task::ready(Err(err.into())),
|
||||
Ok(input) => {
|
||||
let (script_id, script_task) =
|
||||
self.scripting_session.update(cx, move |session, cx| {
|
||||
session.run_script(input.lua_script, cx)
|
||||
});
|
||||
|
||||
let session = self.scripting_session.clone();
|
||||
cx.spawn(async move |_, cx| {
|
||||
script_task.await;
|
||||
|
||||
let message = session.read_with(cx, |session, _cx| {
|
||||
// Using a id to get the script output seems impractical.
|
||||
// Why not just include it in the Task result?
|
||||
// This is because we'll later report the script state as it runs,
|
||||
session
|
||||
.get(script_id)
|
||||
.output_message_for_llm()
|
||||
.expect("Script shouldn't still be running")
|
||||
})?;
|
||||
|
||||
Ok(message)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
cx.spawn({
|
||||
let tool_use_id = tool_use_id.clone();
|
||||
async move |thread, cx| {
|
||||
let output = task.await;
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
let pending_tool_use = thread
|
||||
.scripting_tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), output);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
canceled: false,
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn attach_tool_results(
|
||||
&mut self,
|
||||
updated_context: Vec<ContextSnapshot>,
|
||||
@@ -1730,22 +1574,12 @@ impl Thread {
|
||||
self.cumulative_token_usage.clone()
|
||||
}
|
||||
|
||||
pub fn deny_tool_use(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_type: ToolType,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
pub fn deny_tool_use(&mut self, tool_use_id: LanguageModelToolUseId, cx: &mut Context<Self>) {
|
||||
let err = Err(anyhow::anyhow!(
|
||||
"Permission to run tool action denied by user"
|
||||
));
|
||||
|
||||
if let ToolType::ScriptingTool = tool_type {
|
||||
self.scripting_tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), err);
|
||||
} else {
|
||||
self.tool_use.insert_tool_output(tool_use_id.clone(), err);
|
||||
}
|
||||
self.tool_use.insert_tool_output(tool_use_id.clone(), err);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
@@ -1786,6 +1620,7 @@ pub enum ThreadEvent {
|
||||
canceled: bool,
|
||||
},
|
||||
CheckpointChanged,
|
||||
ToolConfirmationNeeded,
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadEvent> for Thread {}
|
||||
|
||||
@@ -4,7 +4,6 @@ use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use gpui::{Entity, Subscription};
|
||||
use indexmap::IndexMap;
|
||||
use scripting_tool::ScriptingTool;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
|
||||
@@ -51,8 +50,8 @@ impl ToolSelector {
|
||||
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, cx| {
|
||||
tools.disable_source(ToolSource::Native, cx);
|
||||
tools.disable_scripting_tool();
|
||||
tools.disable_all_tools(cx);
|
||||
|
||||
tools.enable(
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
@@ -62,8 +61,17 @@ impl ToolSelector {
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
if profile.tools.contains_key(ScriptingTool::NAME) {
|
||||
tools.enable_scripting_tool();
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -98,11 +106,6 @@ impl ToolSelector {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if ToolSource::Native == source {
|
||||
tools.push((
|
||||
ToolSource::Native,
|
||||
ScriptingTool::NAME.into(),
|
||||
tool_set.is_scripting_tool_enabled(),
|
||||
));
|
||||
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
|
||||
}
|
||||
|
||||
@@ -136,18 +139,10 @@ impl ToolSelector {
|
||||
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, _cx| {
|
||||
if name.as_ref() == ScriptingTool::NAME {
|
||||
if is_enabled {
|
||||
tools.disable_scripting_tool();
|
||||
} else {
|
||||
tools.enable_scripting_tool();
|
||||
}
|
||||
if is_enabled {
|
||||
tools.disable(source.clone(), &[name.clone()]);
|
||||
} else {
|
||||
if is_enabled {
|
||||
tools.disable(source.clone(), &[name.clone()]);
|
||||
} else {
|
||||
tools.enable(source.clone(), &[name.clone()]);
|
||||
}
|
||||
tools.enable(source.clone(), &[name.clone()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent, Role,
|
||||
};
|
||||
use scripting_tool::ScriptingTool;
|
||||
use ui::IconName;
|
||||
|
||||
use crate::thread::MessageId;
|
||||
use crate::thread_store::SerializedMessage;
|
||||
@@ -22,6 +22,7 @@ pub struct ToolUse {
|
||||
pub ui_text: SharedString,
|
||||
pub status: ToolUseStatus,
|
||||
pub input: serde_json::Value,
|
||||
pub icon: ui::IconName,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -180,12 +181,19 @@ impl ToolUseState {
|
||||
}
|
||||
})();
|
||||
|
||||
let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
tool.icon()
|
||||
} else {
|
||||
IconName::Cog
|
||||
};
|
||||
|
||||
tool_uses.push(ToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
|
||||
input: tool_use.input.clone(),
|
||||
status,
|
||||
icon,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -200,8 +208,6 @@ impl ToolUseState {
|
||||
) -> SharedString {
|
||||
if let Some(tool) = self.tools.tool(tool_name, cx) {
|
||||
tool.ui_text(input).into()
|
||||
} else if tool_name == ScriptingTool::NAME {
|
||||
"Run Lua Script".into()
|
||||
} else {
|
||||
"Unknown tool".into()
|
||||
}
|
||||
@@ -285,7 +291,7 @@ impl ToolUseState {
|
||||
ui_text: impl Into<Arc<str>>,
|
||||
input: serde_json::Value,
|
||||
messages: Arc<Vec<LanguageModelRequestMessage>>,
|
||||
tool_type: ToolType,
|
||||
tool: Arc<dyn Tool>,
|
||||
) {
|
||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
let ui_text = ui_text.into();
|
||||
@@ -294,7 +300,7 @@ impl ToolUseState {
|
||||
tool_use_id,
|
||||
input,
|
||||
messages,
|
||||
tool_type,
|
||||
tool,
|
||||
ui_text,
|
||||
};
|
||||
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
|
||||
@@ -398,19 +404,13 @@ pub struct PendingToolUse {
|
||||
pub status: PendingToolUseStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolType {
|
||||
ScriptingTool,
|
||||
NonScriptingTool(Arc<dyn Tool>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Confirmation {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub input: serde_json::Value,
|
||||
pub ui_text: Arc<str>,
|
||||
pub messages: Arc<Vec<LanguageModelRequestMessage>>,
|
||||
pub tool_type: ToolType,
|
||||
pub tool: Arc<dyn Tool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod context_pill;
|
||||
mod tool_ready_pop_up;
|
||||
|
||||
pub use context_pill::*;
|
||||
pub use tool_ready_pop_up::*;
|
||||
|
||||
115
crates/assistant2/src/ui/tool_ready_pop_up.rs
Normal file
115
crates/assistant2/src/ui/tool_ready_pop_up.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use gpui::{
|
||||
point, App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use std::rc::Rc;
|
||||
use theme;
|
||||
use ui::{prelude::*, Render};
|
||||
|
||||
pub struct ToolReadyPopUp {
|
||||
caption: SharedString,
|
||||
}
|
||||
|
||||
impl ToolReadyPopUp {
|
||||
pub fn new(caption: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
caption: caption.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_options(screen: Rc<dyn PlatformDisplay>, cx: &App) -> WindowOptions {
|
||||
let size = Size {
|
||||
width: px(440.),
|
||||
height: px(72.),
|
||||
};
|
||||
|
||||
let notification_margin_width = px(16.);
|
||||
let notification_margin_height = px(-48.);
|
||||
|
||||
let bounds = gpui::Bounds::<Pixels> {
|
||||
origin: screen.bounds().top_right()
|
||||
- point(
|
||||
size.width + notification_margin_width,
|
||||
notification_margin_height,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
let app_id = ReleaseChannel::global(cx).app_id();
|
||||
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: None,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
display_id: Some(screen.id()),
|
||||
window_background: WindowBackgroundAppearance::Transparent,
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: None,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ToolReadyPopupEvent {
|
||||
Accepted,
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolReadyPopupEvent> for ToolReadyPopUp {}
|
||||
|
||||
impl Render for ToolReadyPopUp {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
let line_height = window.line_height();
|
||||
|
||||
h_flex()
|
||||
.size_full()
|
||||
.p_3()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.elevation_3(cx)
|
||||
.text_ui(cx)
|
||||
.font(ui_font)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_xl()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex().h(line_height).justify_center().child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(Headline::new("Agent Panel").size(HeadlineSize::XSmall))
|
||||
.child(Label::new(self.caption.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Button::new("open", "View Panel")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click({
|
||||
cx.listener(move |_this, _event, _, cx| {
|
||||
cx.emit(ToolReadyPopupEvent::Accepted);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click({
|
||||
cx.listener(move |_, _event, _, cx| {
|
||||
cx.emit(ToolReadyPopupEvent::Dismissed);
|
||||
})
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use editor::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||
},
|
||||
scroll::{Autoscroll, AutoscrollStrategy},
|
||||
scroll::Autoscroll,
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
};
|
||||
@@ -414,12 +414,9 @@ impl ContextEditor {
|
||||
cursor..cursor
|
||||
};
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
|
||||
window,
|
||||
cx,
|
||||
|selections| selections.select_ranges([new_selection]),
|
||||
);
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
|
||||
selections.select_ranges([new_selection])
|
||||
});
|
||||
});
|
||||
// Avoid scrolling to the new cursor position so the assistant's output is stable.
|
||||
cx.defer_in(window, |this, _, _| this.scroll_position = None);
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::context_editor::ContextEditor;
|
||||
use anyhow::Result;
|
||||
pub use assistant_slash_command::SlashCommand;
|
||||
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
@@ -126,6 +126,7 @@ impl SlashCommandCompletionProvider {
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
icon_path: None,
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
@@ -223,6 +224,7 @@ impl SlashCommandCompletionProvider {
|
||||
last_argument_range.clone()
|
||||
},
|
||||
label: new_argument.label,
|
||||
icon_path: None,
|
||||
new_text,
|
||||
documentation: None,
|
||||
confirm,
|
||||
@@ -241,6 +243,7 @@ impl SlashCommandCompletionProvider {
|
||||
impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
_excerpt_id: ExcerptId,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
|
||||
@@ -9,12 +9,10 @@ pub struct AgentProfile {
|
||||
/// The name of the profile.
|
||||
pub name: SharedString,
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
#[allow(dead_code)]
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextServerPreset {
|
||||
#[allow(dead_code)]
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ pub struct AssistantSettings {
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: bool,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
@@ -175,6 +176,7 @@ impl AssistantSettingsContent {
|
||||
enable_experimental_live_diffs: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
@@ -198,6 +200,7 @@ impl AssistantSettingsContent {
|
||||
enable_experimental_live_diffs: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -329,6 +332,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
enable_experimental_live_diffs: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -372,6 +376,10 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: false
|
||||
always_allow_tool_actions: Option<bool>,
|
||||
/// Whether to show a popup notification when the agent is waiting for user input.
|
||||
///
|
||||
/// Default: true
|
||||
notify_when_agent_waiting: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
@@ -412,6 +420,13 @@ impl Default for LanguageModelSelection {
|
||||
pub struct AgentProfileContent {
|
||||
pub name: Arc<str>,
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
#[serde(default)]
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ContextServerPresetContent {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -512,6 +527,10 @@ impl Settings for AssistantSettings {
|
||||
&mut settings.always_allow_tool_actions,
|
||||
value.always_allow_tool_actions,
|
||||
);
|
||||
merge(
|
||||
&mut settings.notify_when_agent_waiting,
|
||||
value.notify_when_agent_waiting,
|
||||
);
|
||||
|
||||
if let Some(profiles) = value.profiles {
|
||||
settings
|
||||
@@ -522,7 +541,18 @@ impl Settings for AssistantSettings {
|
||||
AgentProfile {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
context_servers: IndexMap::default(),
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(context_server_id, preset)| {
|
||||
(
|
||||
context_server_id,
|
||||
ContextServerPreset {
|
||||
tools: preset.tools.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
}));
|
||||
@@ -593,6 +623,7 @@ mod tests {
|
||||
enable_experimental_live_diffs: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -13,10 +13,11 @@ path = "src/assistant_tool.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
icons.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
mod tool_registry;
|
||||
mod tool_working_set;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language::Buffer;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use crate::tool_registry::*;
|
||||
pub use crate::tool_working_set::*;
|
||||
@@ -33,6 +35,9 @@ pub trait Tool: 'static + Send + Sync {
|
||||
/// Returns the description of the tool.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the icon for the tool.
|
||||
fn icon(&self) -> IconName;
|
||||
|
||||
/// Returns the source of the tool.
|
||||
fn source(&self) -> ToolSource {
|
||||
ToolSource::Native
|
||||
|
||||
@@ -15,26 +15,14 @@ pub struct ToolWorkingSet {
|
||||
state: Mutex<WorkingSetState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WorkingSetState {
|
||||
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
||||
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
||||
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||
is_scripting_tool_disabled: bool,
|
||||
next_tool_id: ToolId,
|
||||
}
|
||||
|
||||
impl Default for WorkingSetState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
context_server_tools_by_id: HashMap::default(),
|
||||
context_server_tools_by_name: HashMap::default(),
|
||||
disabled_tools_by_source: HashMap::default(),
|
||||
is_scripting_tool_disabled: true,
|
||||
next_tool_id: ToolId::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolWorkingSet {
|
||||
pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
|
||||
self.state
|
||||
@@ -55,7 +43,7 @@ impl ToolWorkingSet {
|
||||
|
||||
pub fn are_all_tools_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
state.disabled_tools_by_source.is_empty() && !state.is_scripting_tool_disabled
|
||||
state.disabled_tools_by_source.is_empty()
|
||||
}
|
||||
|
||||
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
|
||||
@@ -70,7 +58,6 @@ impl ToolWorkingSet {
|
||||
pub fn enable_all_tools(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.disabled_tools_by_source.clear();
|
||||
state.enable_scripting_tool();
|
||||
}
|
||||
|
||||
pub fn disable_all_tools(&self, cx: &App) {
|
||||
@@ -124,21 +111,6 @@ impl ToolWorkingSet {
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
state.tools_changed();
|
||||
}
|
||||
|
||||
pub fn is_scripting_tool_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.is_scripting_tool_disabled
|
||||
}
|
||||
|
||||
pub fn enable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable_scripting_tool();
|
||||
}
|
||||
|
||||
pub fn disable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_scripting_tool();
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkingSetState {
|
||||
@@ -240,15 +212,5 @@ impl WorkingSetState {
|
||||
|
||||
self.disable(source, &tool_names);
|
||||
}
|
||||
|
||||
self.disable_scripting_tool();
|
||||
}
|
||||
|
||||
fn enable_scripting_tool(&mut self) {
|
||||
self.is_scripting_tool_disabled = false;
|
||||
}
|
||||
|
||||
fn disable_scripting_tool(&mut self) {
|
||||
self.is_scripting_tool_disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
mod bash_tool;
|
||||
mod copy_path_tool;
|
||||
mod create_file_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_files_tool;
|
||||
mod fetch_tool;
|
||||
mod find_replace_file_tool;
|
||||
mod list_directory_tool;
|
||||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod path_search_tool;
|
||||
mod read_file_tool;
|
||||
mod regex_search_tool;
|
||||
mod replace;
|
||||
mod thinking_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use move_path_tool::MovePathTool;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::create_file_tool::CreateFileTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_files_tool::EditFilesTool;
|
||||
use crate::fetch_tool::FetchTool;
|
||||
use crate::find_replace_file_tool::FindReplaceFileTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
@@ -36,7 +43,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(BashTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(EditFilesTool);
|
||||
|
||||
@@ -6,6 +6,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::command::new_smol_command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -31,6 +32,10 @@ impl Tool for BashTool {
|
||||
include_str!("./bash_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(BashToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
@@ -38,7 +43,7 @@ impl Tool for BashTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<BashToolInput>(input.clone()) {
|
||||
Ok(input) => format!("`$ {}`", input.command),
|
||||
Ok(input) => format!("`{}`", input.command),
|
||||
Err(_) => "Run bash command".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
119
crates/assistant_tools/src/copy_path_tool.rs
Normal file
119
crates/assistant_tools/src/copy_path_tool.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CopyPathToolInput {
|
||||
/// The source path of the file or directory to copy.
|
||||
/// If a directory is specified, its contents will be copied recursively (like `cp -r`).
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following files:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
/// - directory3/a/other.txt
|
||||
///
|
||||
/// You can copy the first file by providing a source_path of "directory1/a/something.txt"
|
||||
/// </example>
|
||||
pub source_path: String,
|
||||
|
||||
/// The destination path where the file or directory should be copied to.
|
||||
///
|
||||
/// <example>
|
||||
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
|
||||
/// provide a destination_path of "directory2/b/copy.txt"
|
||||
/// </example>
|
||||
pub destination_path: String,
|
||||
}
|
||||
|
||||
pub struct CopyPathTool;
|
||||
|
||||
impl Tool for CopyPathTool {
|
||||
fn name(&self) -> String {
|
||||
"copy-path".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./copy_path_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Clipboard
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CopyPathToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let src = input.source_path.as_str();
|
||||
let dest = input.destination_path.as_str();
|
||||
format!("Copy `{src}` to `{dest}`")
|
||||
}
|
||||
Err(_) => "Copy path".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let copy_task = project.update(cx, |project, cx| {
|
||||
match project
|
||||
.find_project_path(&input.source_path, cx)
|
||||
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
||||
{
|
||||
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
||||
Some(project_path) => {
|
||||
project.copy_entry(entity.id, None, project_path.path, cx)
|
||||
}
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Destination path {} was outside the project.",
|
||||
input.destination_path
|
||||
))),
|
||||
},
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Source path {} was not found in the project.",
|
||||
input.source_path
|
||||
))),
|
||||
}
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
match copy_task.await {
|
||||
Ok(_) => Ok(format!(
|
||||
"Copied {} to {}",
|
||||
input.source_path, input.destination_path
|
||||
)),
|
||||
Err(err) => Err(anyhow!(
|
||||
"Failed to copy {} to {}: {}",
|
||||
input.source_path,
|
||||
input.destination_path,
|
||||
err
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
6
crates/assistant_tools/src/copy_path_tool/description.md
Normal file
6
crates/assistant_tools/src/copy_path_tool/description.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Copies a file or directory in the project, and returns confirmation that the copy succeeded.
|
||||
Directory contents will be copied recursively (like `cp -r`).
|
||||
|
||||
This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
|
||||
It's much more efficient than doing this by separately reading and then writing the file or directory's contents,
|
||||
so this tool should be preferred over that approach whenever copying is the goal.
|
||||
111
crates/assistant_tools/src/create_file_tool.rs
Normal file
111
crates/assistant_tools/src/create_file_tool.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateFileToolInput {
|
||||
/// The path where the file should be created.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following structure:
|
||||
///
|
||||
/// - directory1/
|
||||
/// - directory2/
|
||||
///
|
||||
/// You can create a new file by providing a path of "directory1/new_file.txt"
|
||||
/// </example>
|
||||
pub path: String,
|
||||
|
||||
/// The text contents of the file to create.
|
||||
///
|
||||
/// <example>
|
||||
/// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
|
||||
/// </example>
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
pub struct CreateFileTool;
|
||||
|
||||
impl Tool for CreateFileTool {
|
||||
fn name(&self) -> String {
|
||||
"create-file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./create_file_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::File
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CreateFileToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = input.path.as_str();
|
||||
format!("Create file `{path}`")
|
||||
}
|
||||
Err(_) => "Create file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
|
||||
Some(project_path) => project_path,
|
||||
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
|
||||
};
|
||||
let contents: Arc<str> = input.contents.as_str().into();
|
||||
let destination_path: Arc<str> = input.path.as_str().into();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_entry(project_path.clone(), false, cx)
|
||||
})?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(contents, cx);
|
||||
})?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created file {destination_path}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Creates a new file at the specified path within the project, containing the given text content. Returns confirmation that the file was created.
|
||||
|
||||
This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it. In those cases, it's better to use another approach to edit the file.
|
||||
@@ -6,6 +6,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DeletePathToolInput {
|
||||
@@ -38,6 +39,10 @@ impl Tool for DeletePathTool {
|
||||
include_str!("./delete_path_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Trash
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DeletePathToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DiagnosticsToolInput {
|
||||
@@ -45,6 +46,10 @@ impl Tool for DiagnosticsTool {
|
||||
include_str!("./diagnostics_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Warning
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DiagnosticsToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
mod edit_action;
|
||||
pub mod log;
|
||||
mod replace;
|
||||
|
||||
use crate::replace::{replace_exact, replace_with_flexible_indent};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use collections::HashSet;
|
||||
use edit_action::{EditAction, EditActionParser};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
|
||||
};
|
||||
use log::{EditToolLog, EditToolRequestId};
|
||||
use project::Project;
|
||||
use replace::{replace_exact, replace_with_flexible_indent};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -87,6 +87,10 @@ impl Tool for EditFilesTool {
|
||||
include_str!("./edit_files_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(EditFilesToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
@@ -149,22 +153,36 @@ impl Tool for EditFilesTool {
|
||||
|
||||
struct EditToolRequest {
|
||||
parser: EditActionParser,
|
||||
output: String,
|
||||
changed_buffers: HashSet<Entity<language::Buffer>>,
|
||||
bad_searches: Vec<BadSearch>,
|
||||
editor_response: EditorResponse,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DiffResult {
|
||||
BadSearch(BadSearch),
|
||||
Diff(language::Diff),
|
||||
enum EditorResponse {
|
||||
/// The editor model hasn't produced any actions yet.
|
||||
/// If we don't have any by the end, we'll return its message to the architect model.
|
||||
Message(String),
|
||||
/// The editor model produced at least one action.
|
||||
Actions {
|
||||
applied: Vec<AppliedAction>,
|
||||
search_errors: Vec<SearchError>,
|
||||
},
|
||||
}
|
||||
|
||||
struct AppliedAction {
|
||||
source: String,
|
||||
buffer: Entity<language::Buffer>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum BadSearch {
|
||||
enum DiffResult {
|
||||
Diff(language::Diff),
|
||||
SearchError(SearchError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SearchError {
|
||||
NoMatch {
|
||||
file_path: String,
|
||||
search: String,
|
||||
@@ -225,24 +243,36 @@ impl EditToolRequest {
|
||||
temperature: Some(0.0),
|
||||
};
|
||||
|
||||
let (mut tx, mut rx) = mpsc::channel::<String>(32);
|
||||
let stream = model.stream_completion_text(llm_request, &cx);
|
||||
let mut chunks = stream.await?;
|
||||
let reader_task = cx.background_spawn(async move {
|
||||
let mut chunks = stream.await?;
|
||||
|
||||
while let Some(chunk) = chunks.stream.next().await {
|
||||
if let Some(chunk) = chunk.log_err() {
|
||||
// we don't process here because the API fails
|
||||
// if we take too long between reads
|
||||
tx.send(chunk).await?
|
||||
}
|
||||
}
|
||||
tx.close().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let mut request = Self {
|
||||
parser: EditActionParser::new(),
|
||||
// we start with the success header so we don't need to shift the output in the common case
|
||||
output: Self::SUCCESS_OUTPUT_HEADER.to_string(),
|
||||
changed_buffers: HashSet::default(),
|
||||
bad_searches: Vec::new(),
|
||||
editor_response: EditorResponse::Message(String::with_capacity(256)),
|
||||
action_log,
|
||||
project,
|
||||
tool_log,
|
||||
};
|
||||
|
||||
while let Some(chunk) = chunks.stream.next().await {
|
||||
request.process_response_chunk(&chunk?, cx).await?;
|
||||
while let Some(chunk) = rx.next().await {
|
||||
request.process_response_chunk(&chunk, cx).await?;
|
||||
}
|
||||
|
||||
reader_task.await?;
|
||||
|
||||
request.finalize(cx).await
|
||||
})
|
||||
}
|
||||
@@ -250,6 +280,12 @@ impl EditToolRequest {
|
||||
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
|
||||
let new_actions = self.parser.parse_chunk(chunk);
|
||||
|
||||
if let EditorResponse::Message(ref mut message) = self.editor_response {
|
||||
if new_actions.is_empty() {
|
||||
message.push_str(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((ref log, req_id)) = self.tool_log {
|
||||
log.update(cx, |log, cx| {
|
||||
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
|
||||
@@ -300,18 +336,45 @@ impl EditToolRequest {
|
||||
}?;
|
||||
|
||||
match result {
|
||||
DiffResult::BadSearch(invalid_replace) => {
|
||||
self.bad_searches.push(invalid_replace);
|
||||
DiffResult::SearchError(error) => {
|
||||
self.push_search_error(error);
|
||||
}
|
||||
DiffResult::Diff(diff) => {
|
||||
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
|
||||
|
||||
write!(&mut self.output, "\n\n{}", source)?;
|
||||
self.changed_buffers.insert(buffer);
|
||||
self.push_applied_action(AppliedAction { source, buffer });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn push_search_error(&mut self, error: SearchError) {
|
||||
match &mut self.editor_response {
|
||||
EditorResponse::Message(_) => {
|
||||
self.editor_response = EditorResponse::Actions {
|
||||
applied: Vec::new(),
|
||||
search_errors: vec![error],
|
||||
};
|
||||
}
|
||||
EditorResponse::Actions { search_errors, .. } => {
|
||||
search_errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_applied_action(&mut self, action: AppliedAction) {
|
||||
match &mut self.editor_response {
|
||||
EditorResponse::Message(_) => {
|
||||
self.editor_response = EditorResponse::Actions {
|
||||
applied: vec![action],
|
||||
search_errors: Vec::new(),
|
||||
};
|
||||
}
|
||||
EditorResponse::Actions { applied, .. } => {
|
||||
applied.push(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn replace_diff(
|
||||
@@ -325,152 +388,166 @@ impl EditToolRequest {
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
return Ok(DiffResult::BadSearch(BadSearch::EmptyBuffer {
|
||||
let error = SearchError::EmptyBuffer {
|
||||
file_path: file_path.display().to_string(),
|
||||
exists,
|
||||
search: old,
|
||||
}));
|
||||
};
|
||||
|
||||
return Ok(DiffResult::SearchError(error));
|
||||
}
|
||||
|
||||
let result =
|
||||
let replace_result =
|
||||
// Try to match exactly
|
||||
replace_exact(&old, &new, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
|
||||
|
||||
let Some(diff) = result else {
|
||||
return anyhow::Ok(DiffResult::BadSearch(BadSearch::NoMatch {
|
||||
let Some(diff) = replace_result else {
|
||||
let error = SearchError::NoMatch {
|
||||
search: old,
|
||||
file_path: file_path.display().to_string(),
|
||||
}));
|
||||
};
|
||||
|
||||
return Ok(DiffResult::SearchError(error));
|
||||
};
|
||||
|
||||
anyhow::Ok(DiffResult::Diff(diff))
|
||||
Ok(DiffResult::Diff(diff))
|
||||
}
|
||||
|
||||
const SUCCESS_OUTPUT_HEADER: &str = "Successfully applied. Here's a list of changes:";
|
||||
const ERROR_OUTPUT_HEADER_NO_EDITS: &str = "I couldn't apply any edits!";
|
||||
const ERROR_OUTPUT_HEADER_WITH_EDITS: &str =
|
||||
"Errors occurred. First, here's a list of the edits we managed to apply:";
|
||||
|
||||
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
|
||||
let changed_buffer_count = self.changed_buffers.len();
|
||||
match self.editor_response {
|
||||
EditorResponse::Message(message) => Err(anyhow!(
|
||||
"No edits were applied! You might need to provide more context.\n\n{}",
|
||||
message
|
||||
)),
|
||||
EditorResponse::Actions {
|
||||
applied,
|
||||
search_errors,
|
||||
} => {
|
||||
let mut output = String::with_capacity(1024);
|
||||
|
||||
// Save each buffer once at the end
|
||||
for buffer in &self.changed_buffers {
|
||||
self.project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
}
|
||||
let parse_errors = self.parser.errors();
|
||||
let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
|
||||
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx))
|
||||
.log_err();
|
||||
if has_errors {
|
||||
let error_count = search_errors.len() + parse_errors.len();
|
||||
|
||||
let errors = self.parser.errors();
|
||||
if applied.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"{} errors occurred! No edits were applied.",
|
||||
error_count,
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"{} errors occurred, but {} edits were correctly applied.",
|
||||
error_count,
|
||||
applied.len(),
|
||||
)?;
|
||||
|
||||
if errors.is_empty() && self.bad_searches.is_empty() {
|
||||
if changed_buffer_count == 0 {
|
||||
return Err(anyhow!(
|
||||
"The instructions didn't lead to any changes. You might need to consult the file contents first."
|
||||
));
|
||||
}
|
||||
writeln!(
|
||||
&mut output,
|
||||
"# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
|
||||
applied.len()
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
write!(
|
||||
&mut output,
|
||||
"Successfully applied! Here's a list of applied edits:"
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(self.output)
|
||||
} else {
|
||||
let mut output = self.output;
|
||||
let mut changed_buffers = HashSet::default();
|
||||
|
||||
if output.is_empty() {
|
||||
output.replace_range(
|
||||
0..Self::SUCCESS_OUTPUT_HEADER.len(),
|
||||
Self::ERROR_OUTPUT_HEADER_NO_EDITS,
|
||||
);
|
||||
} else {
|
||||
output.replace_range(
|
||||
0..Self::SUCCESS_OUTPUT_HEADER.len(),
|
||||
Self::ERROR_OUTPUT_HEADER_WITH_EDITS,
|
||||
);
|
||||
}
|
||||
for action in applied {
|
||||
changed_buffers.insert(action.buffer);
|
||||
write!(&mut output, "\n\n{}", action.source)?;
|
||||
}
|
||||
|
||||
if !self.bad_searches.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\n# {} SEARCH/REPLACE block(s) failed to match:\n",
|
||||
self.bad_searches.len()
|
||||
)?;
|
||||
for buffer in &changed_buffers {
|
||||
self.project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
}
|
||||
|
||||
for bad_search in self.bad_searches {
|
||||
match bad_search {
|
||||
BadSearch::NoMatch { file_path, search } => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"## No exact match in: `{}`\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
BadSearch::EmptyBuffer {
|
||||
file_path,
|
||||
exists: true,
|
||||
search,
|
||||
} => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"## No match because `{}` is empty:\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
BadSearch::EmptyBuffer {
|
||||
file_path,
|
||||
exists: false,
|
||||
search,
|
||||
} => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"## No match because `{}` does not exist:\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
|
||||
.log_err();
|
||||
|
||||
if !search_errors.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
|
||||
search_errors.len()
|
||||
)?;
|
||||
|
||||
for error in search_errors {
|
||||
match error {
|
||||
SearchError::NoMatch { file_path, search } => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"### No exact match in: `{}`\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
SearchError::EmptyBuffer {
|
||||
file_path,
|
||||
exists: true,
|
||||
search,
|
||||
} => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"### No match because `{}` is empty:\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
SearchError::EmptyBuffer {
|
||||
file_path,
|
||||
exists: false,
|
||||
search,
|
||||
} => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"### No match because `{}` does not exist:\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write!(&mut output,
|
||||
"The SEARCH section must exactly match an existing block of lines including all white \
|
||||
space, comments, indentation, docstrings, etc."
|
||||
)?;
|
||||
}
|
||||
|
||||
if !parse_errors.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\n## {} SEARCH/REPLACE blocks failed to parse:",
|
||||
parse_errors.len()
|
||||
)?;
|
||||
|
||||
for error in parse_errors {
|
||||
writeln!(&mut output, "- {}", error)?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(&mut output,
|
||||
"The SEARCH section must exactly match an existing block of lines including all white \
|
||||
space, comments, indentation, docstrings, etc."
|
||||
)?;
|
||||
}
|
||||
if has_errors {
|
||||
writeln!(&mut output,
|
||||
"\n\nYou can fix errors by running the tool again. You can include instructions, \
|
||||
but errors are part of the conversation so you don't need to repeat them.",
|
||||
)?;
|
||||
|
||||
if !errors.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\n# {} SEARCH/REPLACE blocks failed to parse:",
|
||||
errors.len()
|
||||
)?;
|
||||
|
||||
for error in errors {
|
||||
writeln!(&mut output, "- {}", error)?;
|
||||
}
|
||||
}
|
||||
|
||||
if changed_buffer_count > 0 {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\nThe other SEARCH/REPLACE blocks were applied successfully. Do not re-send them!",
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
&mut output,
|
||||
"{}You can fix errors by running the tool again. You can include instructions, \
|
||||
but errors are part of the conversation so you don't need to repeat them.",
|
||||
if changed_buffer_count == 0 {
|
||||
"\n\n"
|
||||
Err(anyhow!(output))
|
||||
} else {
|
||||
""
|
||||
Ok(output)
|
||||
}
|
||||
)?;
|
||||
|
||||
Err(anyhow!(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,7 @@ Edit files in the current project by specifying instructions in natural language
|
||||
When using this tool, you should suggest one coherent edit that can be made to the codebase.
|
||||
|
||||
When the set of edits you want to make is large or complex, feel free to invoke this tool multiple times, each time focusing on a specific change you wanna make.
|
||||
|
||||
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents, and you absolutely must never use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
|
||||
|
||||
DO NOT call this tool until the code to be edited appears in the conversation! You must use the `read-files` tool or ask the user to add it to context first.
|
||||
|
||||
@@ -120,7 +120,7 @@ Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each ch
|
||||
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
|
||||
Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
|
||||
|
||||
Only create *SEARCH/REPLACE* blocks for files that the user has added to the chat!
|
||||
Only create *SEARCH/REPLACE* blocks for files that have been read! Even though the conversation includes `read-file` tool results, you *CANNOT* issue your own reads. If the conversation doesn't include the code you need to edit, ask for it to be read explicitly.
|
||||
|
||||
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
@@ -121,6 +122,10 @@ impl Tool for FetchTool {
|
||||
include_str!("./fetch_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Globe
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(FetchToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
229
crates/assistant_tools/src/find_replace_file_tool.rs
Normal file
229
crates/assistant_tools/src/find_replace_file_tool.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
use crate::replace::replace_exact;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FindReplaceFileToolInput {
|
||||
/// The path of the file to modify.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - backend
|
||||
/// - frontend
|
||||
///
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// `frontend/db.js`
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A user-friendly description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year</example>
|
||||
pub display_description: String,
|
||||
|
||||
/// The unique string to find in the file. This string cannot be empty;
|
||||
/// if the string is empty, the tool call will fail. Remember, do not use this tool
|
||||
/// to create new files from scratch, or to overwrite existing files! Use a different
|
||||
/// approach if you want to do that.
|
||||
///
|
||||
/// If this string appears more than once in the file, this tool call will fail,
|
||||
/// so it is absolutely critical that you verify ahead of time that the string
|
||||
/// is unique. You can search within the file to verify this.
|
||||
///
|
||||
/// To make the string more likely to be unique, include a minimum of 3 lines of context
|
||||
/// before the string you actually want to find, as well as a minimum of 3 lines of
|
||||
/// context after the string you want to find. (These lines of context should appear
|
||||
/// in the `replace` string as well.) If 3 lines of context is not enough to obtain
|
||||
/// a string that appears only once in the file, then double the number of context lines
|
||||
/// until the string becomes unique. (Start with 3 lines before and 3 lines after
|
||||
/// though, because too much context is needlessly costly.)
|
||||
///
|
||||
/// Do not alter the context lines of code in any way, and make sure to preserve all
|
||||
/// whitespace and indentation for all lines of code. This string must be exactly as
|
||||
/// it appears in the file, because this tool will do a literal find/replace, and if
|
||||
/// even one character in this string is different in any way from how it appears
|
||||
/// in the file, then the tool call will fail.
|
||||
///
|
||||
/// <example>
|
||||
/// If a file contains this code:
|
||||
///
|
||||
/// ```rust
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Your find string should include at least 3 lines of context before and after the part
|
||||
/// you want to change:
|
||||
///
|
||||
/// ```
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// And your replace string might look like:
|
||||
///
|
||||
/// ```
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" || user.role == "superuser" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
/// </example>
|
||||
pub find: String,
|
||||
|
||||
/// The string to replace the one unique occurrence of the find string with.
|
||||
pub replace: String,
|
||||
}
|
||||
|
||||
pub struct FindReplaceFileTool;
|
||||
|
||||
impl Tool for FindReplaceFileTool {
|
||||
fn name(&self) -> String {
|
||||
"find-replace-file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("find_replace_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(FindReplaceFileToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<FindReplaceFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Edit file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.find.is_empty() {
|
||||
return Err(anyhow!("`find` string cannot be empty. Use a different tool if you want to create a file."));
|
||||
}
|
||||
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
replace_exact(&input.find, &input.replace, &snapshot).await
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(diff) = result {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
})?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.save_buffer(buffer.clone(), cx)
|
||||
})?.await?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
let mut buffers = HashSet::default();
|
||||
buffers.insert(buffer);
|
||||
log.buffer_edited(buffers, cx);
|
||||
})?;
|
||||
|
||||
Ok(format!("Edited {}", input.path.display()))
|
||||
} else {
|
||||
let err = buffer.read_with(cx, |buffer, _cx| {
|
||||
let file_exists = buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
if !file_exists {
|
||||
anyhow!("{} does not exist", input.path.display())
|
||||
} else if buffer.is_empty() {
|
||||
anyhow!(
|
||||
"{} is empty, so the provided `find` string wasn't found.",
|
||||
input.path.display()
|
||||
)
|
||||
} else {
|
||||
anyhow!("Failed to match the provided `find` string")
|
||||
}
|
||||
})?;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Find one unique part of a file in the project and replace that text with new text.
|
||||
|
||||
This tool is the preferred way to make edits to files. If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing multiple calls to this tool - one call for each find/replace operation.
|
||||
|
||||
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
|
||||
|
||||
DO NOT call this tool until the code to be edited appears in the conversation! You must use another tool to read the file's contents into the conversation, or ask the user to add it to context first.
|
||||
@@ -6,6 +6,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListDirectoryToolInput {
|
||||
@@ -49,6 +50,10 @@ impl Tool for ListDirectoryTool {
|
||||
include_str!("./list_directory_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Folder
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(ListDirectoryToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -6,6 +6,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MovePathToolInput {
|
||||
@@ -47,6 +48,10 @@ impl Tool for MovePathTool {
|
||||
include_str!("./move_path_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ArrowRightLeft
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(MovePathToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -8,6 +8,7 @@ use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -39,6 +40,10 @@ impl Tool for NowTool {
|
||||
"Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Info
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(NowToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -6,6 +6,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::paths::PathMatcher;
|
||||
use worktree::Snapshot;
|
||||
|
||||
@@ -47,6 +48,10 @@ impl Tool for PathSearchTool {
|
||||
include_str!("./path_search_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::SearchCode
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(PathSearchToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
@@ -71,7 +76,11 @@ impl Tool for PathSearchTool {
|
||||
Ok(input) => (input.offset.unwrap_or(0), input.glob),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
|
||||
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { &glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
@@ -52,6 +53,10 @@ impl Tool for ReadFileTool {
|
||||
include_str!("./read_file_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Eye
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(ReadFileToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -11,6 +11,7 @@ use project::{
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -49,6 +50,10 @@ impl Tool for RegexSearchTool {
|
||||
include_str!("./regex_search_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Regex
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(RegexSearchToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use language::{BufferSnapshot, Diff, Point, ToOffset};
|
||||
use project::search::SearchQuery;
|
||||
use std::iter;
|
||||
use util::{paths::PathMatcher, ResultExt as _};
|
||||
|
||||
/// Performs an exact string replacement in a buffer, requiring precise character-for-character matching.
|
||||
@@ -11,8 +12,8 @@ pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> O
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
PathMatcher::new(&[]).ok()?,
|
||||
PathMatcher::new(&[]).ok()?,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
None,
|
||||
)
|
||||
.log_err()?;
|
||||
@@ -7,6 +7,7 @@ use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ThinkingToolInput {
|
||||
@@ -30,6 +31,10 @@ impl Tool for ThinkingTool {
|
||||
include_str!("./thinking_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Brain
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(ThinkingToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
|
||||
@@ -7,8 +7,7 @@ use std::cmp::Ordering;
|
||||
use std::mem;
|
||||
use std::{future::Future, iter, ops::Range, sync::Arc};
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
|
||||
use text::{AnchorRangeExt, ToOffset as _};
|
||||
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct BufferDiff {
|
||||
@@ -189,7 +188,7 @@ impl BufferDiffSnapshot {
|
||||
|
||||
impl BufferDiffInner {
|
||||
/// Returns the new index text and new pending hunks.
|
||||
fn stage_or_unstage_hunks(
|
||||
fn stage_or_unstage_hunks_impl(
|
||||
&mut self,
|
||||
unstaged_diff: &Self,
|
||||
stage: bool,
|
||||
@@ -234,9 +233,6 @@ impl BufferDiffInner {
|
||||
}
|
||||
};
|
||||
|
||||
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
|
||||
let mut pending_hunks = SumTree::new(buffer);
|
||||
let mut old_pending_hunks = unstaged_diff
|
||||
.pending_hunks
|
||||
@@ -252,18 +248,16 @@ impl BufferDiffInner {
|
||||
{
|
||||
let preceding_pending_hunks =
|
||||
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
|
||||
|
||||
pending_hunks.append(preceding_pending_hunks, buffer);
|
||||
|
||||
// skip all overlapping old pending hunks
|
||||
while old_pending_hunks
|
||||
.item()
|
||||
.is_some_and(|preceding_pending_hunk_item| {
|
||||
preceding_pending_hunk_item
|
||||
.buffer_range
|
||||
.overlaps(&buffer_range, buffer)
|
||||
})
|
||||
{
|
||||
// Skip all overlapping or adjacent old pending hunks
|
||||
while old_pending_hunks.item().is_some_and(|old_hunk| {
|
||||
old_hunk
|
||||
.buffer_range
|
||||
.start
|
||||
.cmp(&buffer_range.end, buffer)
|
||||
.is_le()
|
||||
}) {
|
||||
old_pending_hunks.next(buffer);
|
||||
}
|
||||
|
||||
@@ -291,6 +285,9 @@ impl BufferDiffInner {
|
||||
// append the remainder
|
||||
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
|
||||
|
||||
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
|
||||
let mut prev_unstaged_hunk_buffer_offset = 0;
|
||||
let mut prev_unstaged_hunk_base_text_offset = 0;
|
||||
let mut edits = Vec::<(Range<usize>, String)>::new();
|
||||
@@ -357,7 +354,13 @@ impl BufferDiffInner {
|
||||
edits.push((index_range, replacement_text));
|
||||
}
|
||||
|
||||
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
|
||||
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted
|
||||
{
|
||||
for window in edits.windows(2) {
|
||||
let (range_a, range_b) = (&window[0].0, &window[1].0);
|
||||
debug_assert!(range_a.end < range_b.start);
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_index_text = Rope::new();
|
||||
let mut index_cursor = index_text.cursor(0);
|
||||
@@ -854,7 +857,7 @@ impl BufferDiff {
|
||||
file_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Rope> {
|
||||
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
|
||||
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks_impl(
|
||||
&self.secondary_diff.as_ref()?.read(cx).inner,
|
||||
stage,
|
||||
&hunks,
|
||||
@@ -1240,13 +1243,13 @@ impl DiffHunkStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Range (crossing new lines), old, new
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
diff_hunks: HunkIter,
|
||||
buffer: &text::BufferSnapshot,
|
||||
diff_base: &str,
|
||||
// Line range, deleted, added, status
|
||||
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
|
||||
) where
|
||||
HunkIter: Iterator<Item = DiffHunk>,
|
||||
@@ -1267,11 +1270,11 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
|
||||
let expected_hunks: Vec<_> = expected_hunks
|
||||
.iter()
|
||||
.map(|(r, old_text, new_text, status)| {
|
||||
.map(|(line_range, deleted_text, added_text, status)| {
|
||||
(
|
||||
Point::new(r.start, 0)..Point::new(r.end, 0),
|
||||
old_text.as_ref(),
|
||||
new_text.as_ref().to_string(),
|
||||
Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
|
||||
deleted_text.as_ref(),
|
||||
added_text.as_ref().to_string(),
|
||||
*status,
|
||||
)
|
||||
})
|
||||
@@ -1286,6 +1289,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use rand::{rngs::StdRng, Rng as _};
|
||||
use text::{Buffer, BufferId, Rope};
|
||||
use unindent::Unindent as _;
|
||||
@@ -1705,6 +1709,66 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
|
||||
let head_text = "
|
||||
one
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
let index_text = head_text.clone();
|
||||
let buffer_text = "
|
||||
one
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
|
||||
let unstaged = BufferDiff::build_sync(buffer.clone(), index_text, cx);
|
||||
let uncommitted = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
|
||||
let unstaged_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer, cx);
|
||||
diff.set_state(unstaged, &buffer);
|
||||
diff
|
||||
});
|
||||
let uncommitted_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer, cx);
|
||||
diff.set_state(uncommitted, &buffer);
|
||||
diff.set_secondary_diff(unstaged_diff.clone());
|
||||
diff
|
||||
});
|
||||
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunk = diff.hunks(&buffer, cx).next().unwrap();
|
||||
|
||||
let new_index_text = diff
|
||||
.stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert_eq!(new_index_text, buffer_text);
|
||||
|
||||
let hunk = diff.hunks(&buffer, &cx).next().unwrap();
|
||||
assert_eq!(
|
||||
hunk.secondary_status,
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
|
||||
);
|
||||
|
||||
let index_text = diff
|
||||
.stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert_eq!(index_text, head_text);
|
||||
|
||||
let hunk = diff.hunks(&buffer, &cx).next().unwrap();
|
||||
// optimistically unstaged (fine, could also be HasSecondaryHunk)
|
||||
assert_eq!(
|
||||
hunk.secondary_status,
|
||||
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
|
||||
let base_text = "
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::{Context as _, Result};
|
||||
use channel::{ChannelChat, ChannelStore, MessageParams};
|
||||
use client::{UserId, UserStore};
|
||||
use collections::HashSet;
|
||||
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AsyncApp, AsyncWindowContext, Context, Entity, Focusable, FontStyle, FontWeight,
|
||||
@@ -56,6 +56,7 @@ struct MessageEditorCompletionProvider(WeakEntity<MessageEditor>);
|
||||
impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
_excerpt_id: ExcerptId,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_: editor::CompletionContext,
|
||||
@@ -311,6 +312,7 @@ impl MessageEditor {
|
||||
old_range: range.clone(),
|
||||
new_text,
|
||||
label,
|
||||
icon_path: None,
|
||||
confirm: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::Custom,
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct CollaborationPanelSettings {
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Serialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Copy, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChatPanelButton {
|
||||
Never,
|
||||
@@ -20,50 +20,6 @@ pub enum ChatPanelButton {
|
||||
WhenInCall,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ChatPanelButton {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for Visitor {
|
||||
type Value = ChatPanelButton;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"a boolean or one of "never", "always", "when_in_call""#
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match b {
|
||||
false => Ok(ChatPanelButton::Never),
|
||||
true => Ok(ChatPanelButton::Always),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match s {
|
||||
"never" => Ok(ChatPanelButton::Never),
|
||||
"always" => Ok(ChatPanelButton::Always),
|
||||
"when_in_call" => Ok(ChatPanelButton::WhenInCall),
|
||||
_ => Err(E::unknown_variant(s, &["never", "always", "when_in_call"])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatPanelSettings {
|
||||
pub button: ChatPanelButton,
|
||||
|
||||
@@ -21,6 +21,7 @@ context_server_settings.workspace = true
|
||||
extension.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
icons.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use assistant_tool::{ActionLog, Tool, ToolSource};
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
|
||||
@@ -38,6 +39,10 @@ impl Tool for ContextServerTool {
|
||||
self.tool.description.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Cog
|
||||
}
|
||||
|
||||
fn source(&self) -> ToolSource {
|
||||
ToolSource::ContextServer {
|
||||
id: self.server_id.clone().into(),
|
||||
|
||||
@@ -26,8 +26,6 @@ test-support = [
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -49,7 +47,6 @@ schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
task.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
@@ -4,9 +4,7 @@ pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use ::fs::Fs;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
|
||||
@@ -14,7 +12,6 @@ use gpui::{
|
||||
actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use http_client::github::get_release_by_tag_name;
|
||||
use http_client::HttpClient;
|
||||
use language::language_settings::CopilotSettings;
|
||||
use language::{
|
||||
@@ -27,7 +24,6 @@ use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use request::StatusNotification;
|
||||
use settings::SettingsStore;
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
env,
|
||||
@@ -37,7 +33,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
use util::{fs::remove_matching, ResultExt};
|
||||
|
||||
pub use crate::copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification};
|
||||
@@ -65,7 +61,7 @@ pub fn init(
|
||||
|
||||
let copilot = cx.new({
|
||||
let node_runtime = node_runtime.clone();
|
||||
move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
|
||||
move |cx| Copilot::start(new_server_id, node_runtime, cx)
|
||||
});
|
||||
Copilot::set_global(copilot.clone(), cx);
|
||||
cx.observe(&copilot, |handle, cx| {
|
||||
@@ -305,7 +301,6 @@ pub struct Completion {
|
||||
}
|
||||
|
||||
pub struct Copilot {
|
||||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: NodeRuntime,
|
||||
server: CopilotServer,
|
||||
buffers: HashSet<WeakEntity<Buffer>>,
|
||||
@@ -337,13 +332,11 @@ impl Copilot {
|
||||
|
||||
fn start(
|
||||
new_server_id: LanguageServerId,
|
||||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: NodeRuntime,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
server_id: new_server_id,
|
||||
http,
|
||||
node_runtime,
|
||||
server: CopilotServer::Disabled,
|
||||
buffers: Default::default(),
|
||||
@@ -384,14 +377,12 @@ impl Copilot {
|
||||
return;
|
||||
}
|
||||
let server_id = self.server_id;
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||
let start_task = cx
|
||||
.spawn(async move |this, cx| {
|
||||
Self::start_language_server(
|
||||
server_id,
|
||||
http,
|
||||
node_runtime,
|
||||
env,
|
||||
this,
|
||||
@@ -445,11 +436,9 @@ impl Copilot {
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
let http = http_client::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
let this = cx.new(|cx| Self {
|
||||
server_id: LanguageServerId(0),
|
||||
http: http.clone(),
|
||||
node_runtime,
|
||||
server: CopilotServer::Running(RunningCopilotServer {
|
||||
lsp: Arc::new(server),
|
||||
@@ -464,7 +453,6 @@ impl Copilot {
|
||||
|
||||
async fn start_language_server(
|
||||
new_server_id: LanguageServerId,
|
||||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: NodeRuntime,
|
||||
env: Option<HashMap<String, String>>,
|
||||
this: WeakEntity<Self>,
|
||||
@@ -472,7 +460,7 @@ impl Copilot {
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
let start_language_server = async {
|
||||
let server_path = get_copilot_lsp(http).await?;
|
||||
let server_path = get_copilot_lsp(node_runtime.clone()).await?;
|
||||
let node_path = node_runtime.binary_path().await?;
|
||||
let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
|
||||
let binary = LanguageServerBinary {
|
||||
@@ -506,9 +494,23 @@ impl Copilot {
|
||||
let configuration = lsp::DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
|
||||
let editor_info = request::SetEditorInfoParams {
|
||||
editor_info: request::EditorInfo {
|
||||
name: "zed".into(),
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
},
|
||||
editor_plugin_info: request::EditorPluginInfo {
|
||||
name: "zed-copilot".into(),
|
||||
version: "0.0.1".into(),
|
||||
},
|
||||
};
|
||||
let editor_info_json = serde_json::to_value(&editor_info)?;
|
||||
|
||||
let server = cx
|
||||
.update(|cx| {
|
||||
let params = server.default_initialize_params(cx);
|
||||
let mut params = server.default_initialize_params(cx);
|
||||
params.initialization_options = Some(editor_info_json);
|
||||
server.initialize(params, configuration.into(), cx)
|
||||
})?
|
||||
.await?;
|
||||
@@ -520,16 +522,7 @@ impl Copilot {
|
||||
.await?;
|
||||
|
||||
server
|
||||
.request::<request::SetEditorInfo>(request::SetEditorInfoParams {
|
||||
editor_info: request::EditorInfo {
|
||||
name: "zed".into(),
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
},
|
||||
editor_plugin_info: request::EditorPluginInfo {
|
||||
name: "zed-copilot".into(),
|
||||
version: "0.0.1".into(),
|
||||
},
|
||||
})
|
||||
.request::<request::SetEditorInfo>(editor_info)
|
||||
.await?;
|
||||
|
||||
anyhow::Ok((server, status))
|
||||
@@ -668,13 +661,11 @@ impl Copilot {
|
||||
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||
let start_task = cx
|
||||
.spawn({
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
let server_id = self.server_id;
|
||||
async move |this, cx| {
|
||||
clear_copilot_dir().await;
|
||||
Self::start_language_server(server_id, http, node_runtime, env, this, false, cx)
|
||||
.await
|
||||
Self::start_language_server(server_id, node_runtime, env, this, false, cx).await
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
@@ -1056,73 +1047,31 @@ async fn clear_copilot_config_dir() {
|
||||
remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
|
||||
}
|
||||
|
||||
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
const SERVER_PATH: &str = "dist/language-server.js";
|
||||
async fn get_copilot_lsp(node_runtime: NodeRuntime) -> anyhow::Result<PathBuf> {
|
||||
const PACKAGE_NAME: &str = "@github/copilot-language-server";
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/@github/copilot-language-server/dist/language-server.js";
|
||||
|
||||
///Check for the latest copilot language server and download it if we haven't already
|
||||
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
let release =
|
||||
get_release_by_tag_name("zed-industries/copilot", "v0.7.0", http.clone()).await?;
|
||||
let latest_version = node_runtime
|
||||
.npm_package_latest_version(PACKAGE_NAME)
|
||||
.await?;
|
||||
let server_path = paths::copilot_dir().join(SERVER_PATH);
|
||||
|
||||
let version_dir = &paths::copilot_dir().join(format!("copilot-{}", release.tag_name));
|
||||
|
||||
fs::create_dir_all(version_dir).await?;
|
||||
let server_path = version_dir.join(SERVER_PATH);
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
// Copilot LSP looks for this dist dir specifically, so lets add it in.
|
||||
let dist_dir = version_dir.join("dist");
|
||||
fs::create_dir_all(dist_dir.as_path()).await?;
|
||||
|
||||
let url = &release
|
||||
.assets
|
||||
.first()
|
||||
.context("Github release for copilot contained no assets")?
|
||||
.browser_download_url;
|
||||
|
||||
let mut response = http
|
||||
.get(url, Default::default(), true)
|
||||
.await
|
||||
.context("error downloading copilot release")?;
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(dist_dir).await?;
|
||||
|
||||
remove_matching(paths::copilot_dir(), |entry| entry != version_dir).await;
|
||||
}
|
||||
|
||||
Ok(server_path)
|
||||
let should_install = node_runtime
|
||||
.should_install_npm_package(
|
||||
PACKAGE_NAME,
|
||||
&server_path,
|
||||
paths::copilot_dir(),
|
||||
&latest_version,
|
||||
)
|
||||
.await;
|
||||
if should_install {
|
||||
node_runtime
|
||||
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
|
||||
.await?;
|
||||
}
|
||||
|
||||
match fetch_latest(http).await {
|
||||
ok @ Result::Ok(..) => ok,
|
||||
e @ Err(..) => {
|
||||
e.log_err();
|
||||
// Fetch a cached binary, if it exists
|
||||
maybe!(async {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(paths::copilot_dir()).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir =
|
||||
last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(server_path)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
Ok(server_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -43,6 +43,11 @@ pub enum Model {
|
||||
Claude3_5Sonnet,
|
||||
#[serde(alias = "claude-3-7-sonnet", rename = "claude-3.7-sonnet")]
|
||||
Claude3_7Sonnet,
|
||||
#[serde(
|
||||
alias = "claude-3.7-sonnet-thought",
|
||||
rename = "claude-3.7-sonnet-thought"
|
||||
)]
|
||||
Claude3_7SonnetThinking,
|
||||
#[serde(alias = "gemini-2.0-flash", rename = "gemini-2.0-flash-001")]
|
||||
Gemini20Flash,
|
||||
}
|
||||
@@ -54,7 +59,8 @@ impl Model {
|
||||
| Self::Gpt4
|
||||
| Self::Gpt3_5Turbo
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet => true,
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking => true,
|
||||
Self::O3Mini | Self::O1 | Self::Gemini20Flash => false,
|
||||
}
|
||||
}
|
||||
@@ -68,6 +74,7 @@ impl Model {
|
||||
"o3-mini" => Ok(Self::O3Mini),
|
||||
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
|
||||
"claude-3-7-sonnet" => Ok(Self::Claude3_7Sonnet),
|
||||
"claude-3.7-sonnet-thought" => Ok(Self::Claude3_7SonnetThinking),
|
||||
"gemini-2.0-flash-001" => Ok(Self::Gemini20Flash),
|
||||
_ => Err(anyhow!("Invalid model id: {}", id)),
|
||||
}
|
||||
@@ -82,6 +89,7 @@ impl Model {
|
||||
Self::O1 => "o1",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
Self::Claude3_7Sonnet => "claude-3-7-sonnet",
|
||||
Self::Claude3_7SonnetThinking => "claude-3.7-sonnet-thought",
|
||||
Self::Gemini20Flash => "gemini-2.0-flash-001",
|
||||
}
|
||||
}
|
||||
@@ -95,6 +103,7 @@ impl Model {
|
||||
Self::O1 => "o1",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
|
||||
Self::Gemini20Flash => "Gemini 2.0 Flash",
|
||||
}
|
||||
}
|
||||
@@ -108,6 +117,7 @@ impl Model {
|
||||
Self::O1 => 20_000,
|
||||
Self::Claude3_5Sonnet => 200_000,
|
||||
Self::Claude3_7Sonnet => 90_000,
|
||||
Self::Claude3_7SonnetThinking => 90_000,
|
||||
Model::Gemini20Flash => 128_000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,15 +37,6 @@ impl DebugAdapter for LldbDebugAdapter {
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let lldb_dap_path = if let Some(user_installed_path) = user_installed_path {
|
||||
user_installed_path.to_string_lossy().into()
|
||||
} else if cfg!(target_os = "macos") {
|
||||
util::command::new_smol_command("xcrun")
|
||||
.args(&["-f", "lldb-dap"])
|
||||
.output()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
.map(|path| path.trim().to_string())
|
||||
.ok_or(anyhow!("Failed to find lldb-dap in user's path"))?
|
||||
} else {
|
||||
delegate
|
||||
.which(OsStr::new("lldb-dap"))
|
||||
|
||||
@@ -24,7 +24,7 @@ use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane, ClearBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto,
|
||||
pane, ClearAllBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto,
|
||||
StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, Workspace,
|
||||
};
|
||||
|
||||
@@ -174,7 +174,7 @@ impl DebugPanel {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let debug_panel = DebugPanel::new(workspace, window, cx);
|
||||
|
||||
workspace.register_action(|workspace, _: &ClearBreakpoints, _, cx| {
|
||||
workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
|
||||
workspace.project().read(cx).breakpoint_store().update(
|
||||
cx,
|
||||
|breakpoint_store, cx| {
|
||||
|
||||
@@ -19,7 +19,7 @@ use project::Project;
|
||||
use rpc::proto::{self, PeerId};
|
||||
use running::RunningState;
|
||||
use starting::{StartingEvent, StartingState};
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, Indicator};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{self, Item},
|
||||
@@ -214,23 +214,48 @@ impl Focusable for DebugSession {
|
||||
impl Item for DebugSession {
|
||||
type Event = DebugPanelItemEvent;
|
||||
fn tab_content(&self, _: item::TabContentParams, _: &Window, cx: &App) -> AnyElement {
|
||||
let (label, color) = match &self.mode {
|
||||
DebugSessionState::Inert(_) => ("New Session", Color::Default),
|
||||
DebugSessionState::Starting(_) => ("Starting", Color::Default),
|
||||
DebugSessionState::Failed(_) => ("Failed", Color::Error),
|
||||
DebugSessionState::Running(state) => (
|
||||
state
|
||||
.read_with(cx, |state, cx| state.thread_status(cx))
|
||||
.map(|status| status.label())
|
||||
.unwrap_or("Running"),
|
||||
Color::Default,
|
||||
let (icon, label, color) = match &self.mode {
|
||||
DebugSessionState::Inert(_) => (None, "New Session", Color::Default),
|
||||
DebugSessionState::Starting(_) => (None, "Starting", Color::Default),
|
||||
DebugSessionState::Failed(_) => (
|
||||
Some(Indicator::dot().color(Color::Error)),
|
||||
"Failed",
|
||||
Color::Error,
|
||||
),
|
||||
DebugSessionState::Running(state) => {
|
||||
if state.read(cx).session().read(cx).is_terminated() {
|
||||
(
|
||||
Some(Indicator::dot().color(Color::Error)),
|
||||
"Terminated",
|
||||
Color::Error,
|
||||
)
|
||||
} else {
|
||||
match state.read(cx).thread_status(cx).unwrap_or_default() {
|
||||
project::debugger::session::ThreadStatus::Stopped => (
|
||||
Some(Indicator::dot().color(Color::Conflict)),
|
||||
state
|
||||
.read_with(cx, |state, cx| state.thread_status(cx))
|
||||
.map(|status| status.label())
|
||||
.unwrap_or("Stopped"),
|
||||
Color::Conflict,
|
||||
),
|
||||
_ => (
|
||||
Some(Indicator::dot().color(Color::Success)),
|
||||
state
|
||||
.read_with(cx, |state, cx| state.thread_status(cx))
|
||||
.map(|status| status.label())
|
||||
.unwrap_or("Running"),
|
||||
Color::Success,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let is_starting = matches!(self.mode, DebugSessionState::Starting(_));
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_2()
|
||||
.children(is_starting.then(|| {
|
||||
Icon::new(IconName::ArrowCircle).with_animation(
|
||||
"starting-debug-session",
|
||||
@@ -238,6 +263,8 @@ impl Item for DebugSession {
|
||||
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
}))
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
.child(Label::new(label).color(color))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ use settings::Settings;
|
||||
use stack_frame_list::StackFrameList;
|
||||
use ui::{
|
||||
div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize,
|
||||
Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
ContextMenu, Disableable, Divider, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize,
|
||||
Indicator, InteractiveElement, IntoElement, Label, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Tooltip, Window,
|
||||
};
|
||||
use util::ResultExt;
|
||||
@@ -61,14 +61,12 @@ impl Render for RunningState {
|
||||
this.disabled(thread_status != ThreadStatus::Stopped, cx);
|
||||
});
|
||||
|
||||
let is_terminated = self.session.read(cx).is_terminated();
|
||||
let active_thread_item = &self.active_thread_item;
|
||||
|
||||
let has_no_threads = threads.is_empty();
|
||||
let capabilities = self.capabilities(cx);
|
||||
let state = cx.entity();
|
||||
h_flex()
|
||||
.when(is_terminated, |this| this.bg(gpui::red()))
|
||||
.key_context("DebugPanelItem")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
@@ -85,9 +83,10 @@ impl Render for RunningState {
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.map(|this| {
|
||||
if thread_status == ThreadStatus::Running {
|
||||
this.child(
|
||||
@@ -95,7 +94,7 @@ impl Render for RunningState {
|
||||
"debug-pause",
|
||||
IconName::DebugPause,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.pause_thread(cx);
|
||||
}))
|
||||
@@ -109,7 +108,7 @@ impl Render for RunningState {
|
||||
"debug-continue",
|
||||
IconName::DebugContinue,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.continue_thread(cx)
|
||||
}))
|
||||
@@ -120,61 +119,9 @@ impl Render for RunningState {
|
||||
)
|
||||
}
|
||||
})
|
||||
.when(
|
||||
capabilities.supports_step_back.unwrap_or(false),
|
||||
|this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"debug-step-back",
|
||||
IconName::DebugStepBack,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_back(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step back")(window, cx)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-over", IconName::DebugStepOver)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_over(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step over")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-in", IconName::DebugStepInto)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_in(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step in")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-out", IconName::DebugStepOut)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_out(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step out")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.restart_session(cx);
|
||||
}))
|
||||
@@ -189,7 +136,7 @@ impl Render for RunningState {
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-stop", IconName::DebugStop)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.stop_thread(cx);
|
||||
}))
|
||||
@@ -214,7 +161,7 @@ impl Render for RunningState {
|
||||
"debug-disconnect",
|
||||
IconName::DebugDisconnect,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.disconnect_client(cx);
|
||||
}))
|
||||
@@ -222,12 +169,62 @@ impl Render for RunningState {
|
||||
thread_status == ThreadStatus::Exited
|
||||
|| thread_status == ThreadStatus::Ended,
|
||||
)
|
||||
.tooltip(
|
||||
move |window, cx| {
|
||||
Tooltip::text("Disconnect")(window, cx)
|
||||
},
|
||||
),
|
||||
.tooltip(Tooltip::text("Disconnect")),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
.when(
|
||||
capabilities.supports_step_back.unwrap_or(false),
|
||||
|this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"debug-step-back",
|
||||
IconName::DebugStepBack,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_back(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step back")(window, cx)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-over", IconName::DebugStepOver)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_over(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step over")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-in", IconName::DebugStepInto)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_in(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step in")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-out", IconName::DebugStepOut)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_out(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step out")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new(
|
||||
"debug-ignore-breakpoints",
|
||||
@@ -237,7 +234,7 @@ impl Render for RunningState {
|
||||
IconName::DebugIgnoreBreakpoints
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.toggle_ignore_breakpoints(cx);
|
||||
}))
|
||||
@@ -252,33 +249,47 @@ impl Render for RunningState {
|
||||
),
|
||||
),
|
||||
)
|
||||
//.child(h_flex())
|
||||
.child(
|
||||
h_flex().p_1().mx_2().w_3_4().justify_end().child(
|
||||
DropdownMenu::new(
|
||||
("thread-list", self.session_id.0),
|
||||
selected_thread_name,
|
||||
ContextMenu::build(window, cx, move |mut this, _, _| {
|
||||
for (thread, _) in threads {
|
||||
let state = state.clone();
|
||||
let thread_id = thread.id;
|
||||
this =
|
||||
this.entry(thread.name, None, move |_, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.select_thread(
|
||||
ThreadId(thread_id),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.disabled(
|
||||
has_no_threads || thread_status != ThreadStatus::Stopped,
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.gap_2()
|
||||
.w_3_4()
|
||||
.justify_end()
|
||||
.child(Label::new("Thread:"))
|
||||
.child(
|
||||
DropdownMenu::new(
|
||||
("thread-list", self.session_id.0),
|
||||
selected_thread_name,
|
||||
ContextMenu::build(
|
||||
window,
|
||||
cx,
|
||||
move |mut this, _, _| {
|
||||
for (thread, _) in threads {
|
||||
let state = state.clone();
|
||||
let thread_id = thread.id;
|
||||
this = this.entry(
|
||||
thread.name,
|
||||
None,
|
||||
move |_, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.select_thread(
|
||||
ThreadId(thread_id),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
},
|
||||
),
|
||||
)
|
||||
.disabled(
|
||||
has_no_threads
|
||||
|| thread_status != ThreadStatus::Stopped,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::{
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use dap::OutputEvent;
|
||||
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
|
||||
use language::{Buffer, CodeLabel};
|
||||
@@ -246,6 +246,7 @@ struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
|
||||
impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
_excerpt_id: ExcerptId,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_trigger: editor::CompletionContext,
|
||||
@@ -367,6 +368,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
text: format!("{} {}", string_match.string.clone(), variable_value),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
@@ -408,6 +410,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
text: completion.label.clone(),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
|
||||
@@ -18,6 +18,7 @@ actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
|
||||
pub(crate) struct EntryState {
|
||||
depth: usize,
|
||||
is_expanded: bool,
|
||||
has_children: bool,
|
||||
parent_reference: VariableReference,
|
||||
}
|
||||
|
||||
@@ -246,6 +247,7 @@ impl VariableList {
|
||||
.entry(path.clone())
|
||||
.and_modify(|state| {
|
||||
state.parent_reference = container_reference;
|
||||
state.has_children = variables_reference != 0;
|
||||
})
|
||||
.or_insert(EntryState {
|
||||
depth: path.indices.len(),
|
||||
@@ -258,6 +260,7 @@ impl VariableList {
|
||||
.unwrap_or(scope.name.to_lowercase().starts_with("local"))
|
||||
}),
|
||||
parent_reference: container_reference,
|
||||
has_children: variables_reference != 0,
|
||||
});
|
||||
|
||||
entries.push(ListEntry {
|
||||
@@ -358,41 +361,45 @@ impl VariableList {
|
||||
fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(selection) = &self.selection {
|
||||
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
|
||||
if &var.path == selection {
|
||||
let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
|
||||
if &var.path == selection && ix > 0 {
|
||||
Some(ix.saturating_sub(1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(new_selection) =
|
||||
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
|
||||
{
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.select_last(&SelectLast, window, cx);
|
||||
}
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
self.select_last(&SelectLast, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(selection) = &self.selection {
|
||||
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
|
||||
let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
|
||||
if &var.path == selection {
|
||||
Some(ix.saturating_add(1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(new_selection) =
|
||||
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
|
||||
{
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
@@ -437,7 +444,7 @@ impl VariableList {
|
||||
fn collapse_selected_entry(
|
||||
&mut self,
|
||||
_: &CollapseSelectedEntry,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(ref selected_entry) = self.selection {
|
||||
@@ -446,25 +453,33 @@ impl VariableList {
|
||||
return;
|
||||
};
|
||||
|
||||
entry_state.is_expanded = false;
|
||||
cx.notify();
|
||||
if !entry_state.is_expanded || !entry_state.has_children {
|
||||
self.select_prev(&SelectPrevious, window, cx);
|
||||
} else {
|
||||
entry_state.is_expanded = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_selected_entry(
|
||||
&mut self,
|
||||
_: &ExpandSelectedEntry,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(ref selected_entry) = self.selection {
|
||||
if let Some(selected_entry) = &self.selection {
|
||||
let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
|
||||
debug_panic!("Trying to toggle variable in variable list that has an no state");
|
||||
return;
|
||||
};
|
||||
|
||||
entry_state.is_expanded = true;
|
||||
cx.notify();
|
||||
if entry_state.is_expanded || !entry_state.has_children {
|
||||
self.select_next(&SelectNext, window, cx);
|
||||
} else {
|
||||
entry_state.is_expanded = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,6 +664,7 @@ impl VariableList {
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
let path = entry.path.clone();
|
||||
|
||||
div()
|
||||
.id(var_ref as usize)
|
||||
@@ -661,7 +677,8 @@ impl VariableList {
|
||||
.h_full()
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.on_click(cx.listener({
|
||||
move |_this, _, _window, cx| {
|
||||
move |this, _, _window, cx| {
|
||||
this.selection = Some(path.clone());
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
@@ -832,6 +849,7 @@ impl VariableList {
|
||||
.single_line()
|
||||
.truncate()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.when_some(variable_color, |this, color| {
|
||||
this.color(Color::from(color))
|
||||
}),
|
||||
|
||||
@@ -1385,7 +1385,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(workspace::ClearBreakpoints);
|
||||
cx.dispatch_action(workspace::ClearAllBreakpoints);
|
||||
cx.run_until_parked();
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
|
||||
@@ -1103,6 +1103,219 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
});
|
||||
});
|
||||
|
||||
// select scope 2 backwards
|
||||
cx.dispatch_action(SelectPrevious);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec!["> Scope 1", "> Scope 2 <=== selected"]);
|
||||
});
|
||||
});
|
||||
|
||||
// select scope 1 backwards
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]);
|
||||
});
|
||||
});
|
||||
|
||||
// test stepping through nested with ExpandSelectedEntry/CollapseSelectedEntry actions
|
||||
|
||||
cx.dispatch_action(ExpandSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1 <=== selected",
|
||||
" > variable1",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(ExpandSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" > variable1 <=== selected",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(ExpandSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" v variable1 <=== selected",
|
||||
" > nested1",
|
||||
" > nested2",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(ExpandSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" v variable1",
|
||||
" > nested1 <=== selected",
|
||||
" > nested2",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(ExpandSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" v variable1",
|
||||
" > nested1",
|
||||
" > nested2 <=== selected",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(ExpandSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" v variable1",
|
||||
" > nested1",
|
||||
" > nested2",
|
||||
" > variable2 <=== selected",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(CollapseSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" v variable1",
|
||||
" > nested1",
|
||||
" > nested2 <=== selected",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(CollapseSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" v variable1",
|
||||
" > nested1 <=== selected",
|
||||
" > nested2",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(CollapseSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" v variable1 <=== selected",
|
||||
" > nested1",
|
||||
" > nested2",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(CollapseSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1",
|
||||
" > variable1 <=== selected",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(CollapseSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec![
|
||||
"v Scope 1 <=== selected",
|
||||
" > variable1",
|
||||
" > variable2",
|
||||
"> Scope 2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.dispatch_action(CollapseSelectedEntry);
|
||||
cx.run_until_parked();
|
||||
running_state.update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]);
|
||||
});
|
||||
});
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
||||
|
||||
@@ -31,6 +31,11 @@ impl Template for KeybindingTemplate {
|
||||
let action = args.get("action").map(String::as_str).unwrap_or("");
|
||||
let macos_binding = context.find_binding("macos", action).unwrap_or_default();
|
||||
let linux_binding = context.find_binding("linux", action).unwrap_or_default();
|
||||
|
||||
if macos_binding.is_empty() && linux_binding.is_empty() {
|
||||
return "<div>No default binding</div>".to_string();
|
||||
}
|
||||
|
||||
format!("<kbd class=\"keybinding\">{macos_binding}|{linux_binding}</kbd>")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +414,8 @@ actions!(
|
||||
Tab,
|
||||
Backtab,
|
||||
ToggleBreakpoint,
|
||||
DisableBreakpoint,
|
||||
EnableBreakpoint,
|
||||
EditLogBreakpoint,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlameInline,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, Entity, Focusable, FontWeight,
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight,
|
||||
ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
||||
UniformListScrollHandle,
|
||||
};
|
||||
@@ -236,6 +236,7 @@ impl CompletionsMenu {
|
||||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: CompletionSource::Custom,
|
||||
@@ -539,9 +540,25 @@ impl CompletionsMenu {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let color_swatch = completion
|
||||
|
||||
let start_slot = completion
|
||||
.color()
|
||||
.map(|color| div().size_4().bg(color).rounded_xs());
|
||||
.map(|color| {
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size_3p5()
|
||||
.rounded_xs()
|
||||
.bg(color)
|
||||
.into_any_element()
|
||||
})
|
||||
.or_else(|| {
|
||||
completion.icon_path.as_ref().map(|path| {
|
||||
Icon::from_path(path)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
})
|
||||
});
|
||||
|
||||
div().min_w(px(280.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
@@ -559,7 +576,7 @@ impl CompletionsMenu {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.start_slot::<Div>(color_swatch)
|
||||
.start_slot::<AnyElement>(start_slot)
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
|
||||
@@ -60,6 +60,7 @@ use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
use display_map::*;
|
||||
pub use display_map::{DisplayPoint, FoldPlaceholder};
|
||||
use editor_settings::GoToDefinitionFallback;
|
||||
pub use editor_settings::{
|
||||
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
|
||||
};
|
||||
@@ -115,7 +116,9 @@ use linked_editing_ranges::refresh_linked_ranges;
|
||||
use mouse_context_menu::MouseContextMenu;
|
||||
use persistence::DB;
|
||||
use project::{
|
||||
debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent},
|
||||
debugger::breakpoint_store::{
|
||||
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
|
||||
},
|
||||
ProjectPath,
|
||||
};
|
||||
|
||||
@@ -531,6 +534,18 @@ impl EditPredictionPreview {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextMenuOptions {
|
||||
pub min_entries_visible: usize,
|
||||
pub max_entries_visible: usize,
|
||||
pub placement: Option<ContextMenuPlacement>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ContextMenuPlacement {
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
|
||||
struct EditorActionId(usize);
|
||||
|
||||
@@ -677,6 +692,7 @@ pub struct Editor {
|
||||
active_indent_guides_state: ActiveIndentGuidesState,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
context_menu: RefCell<Option<CodeContextMenu>>,
|
||||
context_menu_options: Option<ContextMenuOptions>,
|
||||
mouse_context_menu: Option<MouseContextMenu>,
|
||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||
signature_help_state: SignatureHelpState,
|
||||
@@ -1441,6 +1457,7 @@ impl Editor {
|
||||
active_indent_guides_state: ActiveIndentGuidesState::default(),
|
||||
nav_history: None,
|
||||
context_menu: RefCell::new(None),
|
||||
context_menu_options: None,
|
||||
mouse_context_menu: None,
|
||||
completion_tasks: Default::default(),
|
||||
signature_help_state: SignatureHelpState::default(),
|
||||
@@ -1562,7 +1579,7 @@ impl Editor {
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
|
||||
this.end_selection(window, cx);
|
||||
this.scroll_manager.show_scrollbar(window, cx);
|
||||
this.scroll_manager.show_scrollbars(window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
|
||||
|
||||
if mode == EditorMode::Full {
|
||||
@@ -4251,8 +4268,14 @@ impl Editor {
|
||||
|
||||
let (mut words, provided_completions) = match provider {
|
||||
Some(provider) => {
|
||||
let completions =
|
||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
||||
let completions = provider.completions(
|
||||
position.excerpt_id,
|
||||
&buffer,
|
||||
buffer_position,
|
||||
completion_context,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
||||
@@ -4310,6 +4333,7 @@ impl Editor {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
@@ -4384,6 +4408,17 @@ impl Editor {
|
||||
self.completion_tasks.push((id, task));
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn current_completions(&self) -> Option<Vec<project::Completion>> {
|
||||
let menu = self.context_menu.borrow();
|
||||
if let CodeContextMenu::Completions(menu) = menu.as_ref()? {
|
||||
let completions = menu.completions.borrow();
|
||||
Some(completions.to_vec())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn confirm_completion(
|
||||
&mut self,
|
||||
action: &ConfirmCompletion,
|
||||
@@ -4409,6 +4444,8 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
let completions_menu =
|
||||
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? {
|
||||
menu
|
||||
@@ -4433,12 +4470,6 @@ impl Editor {
|
||||
.clone();
|
||||
cx.stop_propagation();
|
||||
|
||||
if self.selections.newest_anchor().start.buffer_id
|
||||
!= Some(buffer_handle.read(cx).remote_id())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let snippet;
|
||||
let new_text;
|
||||
if completion.is_snippet() {
|
||||
@@ -4448,15 +4479,24 @@ impl Editor {
|
||||
snippet = None;
|
||||
new_text = completion.new_text.clone();
|
||||
};
|
||||
|
||||
let newest_selection = self.selections.newest::<usize>(cx);
|
||||
let selections = self.selections.all::<usize>(cx);
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let old_range = completion.old_range.to_offset(buffer);
|
||||
let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
|
||||
|
||||
let start_distance = newest_selection.start.saturating_sub(old_range.start);
|
||||
let end_distance = old_range.end.saturating_sub(newest_selection.end);
|
||||
let newest_selection = self.selections.newest_anchor();
|
||||
if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let lookbehind = newest_selection
|
||||
.start
|
||||
.text_anchor
|
||||
.to_offset(buffer)
|
||||
.saturating_sub(old_range.start);
|
||||
let lookahead = old_range
|
||||
.end
|
||||
.saturating_sub(newest_selection.end.text_anchor.to_offset(buffer));
|
||||
let mut common_prefix_len = old_text
|
||||
.bytes()
|
||||
.zip(new_text.bytes())
|
||||
@@ -4468,9 +4508,9 @@ impl Editor {
|
||||
let mut ranges = Vec::new();
|
||||
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
for selection in &selections {
|
||||
if snapshot.contains_str_at(selection.start.saturating_sub(start_distance), &old_text) {
|
||||
let start = selection.start.saturating_sub(start_distance);
|
||||
let end = selection.end + end_distance;
|
||||
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
|
||||
let start = selection.start.saturating_sub(lookbehind);
|
||||
let end = selection.end + lookahead;
|
||||
if selection.id == newest_selection.id {
|
||||
range_to_replace = Some(
|
||||
((start + common_prefix_len) as isize - selection.start as isize)
|
||||
@@ -5981,13 +6021,7 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<IconButton> {
|
||||
let color = Color::Muted;
|
||||
|
||||
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
|
||||
let bp_kind = Arc::new(
|
||||
breakpoint
|
||||
.map(|(_, bp)| bp.kind.clone())
|
||||
.unwrap_or(BreakpointKind::Standard),
|
||||
);
|
||||
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
@@ -6024,7 +6058,6 @@ impl Editor {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
position,
|
||||
bp_kind.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
@@ -6077,7 +6110,7 @@ impl Editor {
|
||||
for breakpoint in
|
||||
breakpoint_store
|
||||
.read(cx)
|
||||
.breakpoints(&buffer, None, buffer_snapshot.clone(), cx)
|
||||
.breakpoints(&buffer, None, &buffer_snapshot, cx)
|
||||
{
|
||||
let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0);
|
||||
let mut anchor = multi_buffer_snapshot.anchor_before(point);
|
||||
@@ -6102,49 +6135,33 @@ impl Editor {
|
||||
|
||||
let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left)
|
||||
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
|
||||
for excerpt_boundary in multi_buffer_snapshot.excerpt_boundaries_in_range(range) {
|
||||
let info = excerpt_boundary.next;
|
||||
|
||||
let Some(excerpt_ranges) = multi_buffer_snapshot.range_for_excerpt(info.id) else {
|
||||
for (buffer_snapshot, range, excerpt_id) in
|
||||
multi_buffer_snapshot.range_to_buffer_ranges(range)
|
||||
{
|
||||
let Some(buffer) = project.read_with(cx, |this, cx| {
|
||||
this.buffer_for_id(buffer_snapshot.remote_id(), cx)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(buffer) =
|
||||
project.read_with(cx, |this, cx| this.buffer_for_id(info.buffer_id, cx))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if buffer.read(cx).file().is_none() {
|
||||
continue;
|
||||
}
|
||||
let breakpoints = breakpoint_store.read(cx).breakpoints(
|
||||
&buffer,
|
||||
Some(info.range.context.start..info.range.context.end),
|
||||
info.buffer.clone(),
|
||||
Some(
|
||||
buffer_snapshot.anchor_before(range.start)
|
||||
..buffer_snapshot.anchor_after(range.end),
|
||||
),
|
||||
buffer_snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
// To translate a breakpoint's position within a singular buffer to a multi buffer
|
||||
// position we need to know it's excerpt starting location, it's position within
|
||||
// the singular buffer, and if that position is within the excerpt's range.
|
||||
let excerpt_head = excerpt_ranges
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot);
|
||||
|
||||
let buffer_start = info
|
||||
.buffer
|
||||
.summary_for_anchor::<Point>(&info.range.context.start);
|
||||
|
||||
for (anchor, breakpoint) in breakpoints {
|
||||
let as_row = info.buffer.summary_for_anchor::<Point>(&anchor).row;
|
||||
let delta = as_row - buffer_start.row;
|
||||
let multi_buffer_anchor =
|
||||
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor);
|
||||
let position = multi_buffer_anchor
|
||||
.to_point(&multi_buffer_snapshot)
|
||||
.to_display_point(&snapshot);
|
||||
|
||||
let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0);
|
||||
|
||||
let anchor = snapshot.display_point_to_anchor(position, Bias::Left);
|
||||
|
||||
breakpoint_display_points.insert(position.row(), (anchor, breakpoint.clone()));
|
||||
breakpoint_display_points
|
||||
.insert(position.row(), (multi_buffer_anchor, breakpoint.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6154,30 +6171,80 @@ impl Editor {
|
||||
fn breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
kind: Arc<BreakpointKind>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
let weak_editor = cx.weak_entity();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let second_entry_msg = if kind.log_message().is_some() {
|
||||
let row = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.summary_for_anchor::<Point>(&anchor)
|
||||
.row;
|
||||
|
||||
let breakpoint = self
|
||||
.breakpoint_at_row(row, window, cx)
|
||||
.map(|(_, bp)| Arc::from(bp));
|
||||
|
||||
let log_breakpoint_msg = if breakpoint
|
||||
.as_ref()
|
||||
.is_some_and(|bp| bp.kind.log_message().is_some())
|
||||
{
|
||||
"Edit Log Breakpoint"
|
||||
} else {
|
||||
"Add Log Breakpoint"
|
||||
"Set Log Breakpoint"
|
||||
};
|
||||
|
||||
let set_breakpoint_msg = if breakpoint.as_ref().is_some() {
|
||||
"Unset Breakpoint"
|
||||
} else {
|
||||
"Set Breakpoint"
|
||||
};
|
||||
|
||||
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.state {
|
||||
BreakpointState::Enabled => Some("Disable"),
|
||||
BreakpointState::Disabled => Some("Enable"),
|
||||
});
|
||||
|
||||
let breakpoint = breakpoint.unwrap_or_else(|| {
|
||||
Arc::new(Breakpoint {
|
||||
state: BreakpointState::Enabled,
|
||||
kind: BreakpointKind::Standard,
|
||||
})
|
||||
});
|
||||
|
||||
ui::ContextMenu::build(window, cx, |menu, _, _cx| {
|
||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||
.context(focus_handle)
|
||||
.entry("Toggle Breakpoint", None, {
|
||||
.when_some(toggle_state_msg, |this, msg| {
|
||||
this.entry(msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
})
|
||||
.entry(set_breakpoint_msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
BreakpointKind::Standard,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
@@ -6185,10 +6252,10 @@ impl Editor {
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(second_entry_msg, None, move |window, cx| {
|
||||
.entry(log_breakpoint_msg, None, move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(anchor, kind.as_ref(), window, cx);
|
||||
this.add_edit_breakpoint_block(anchor, breakpoint.as_ref(), window, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -6199,44 +6266,51 @@ impl Editor {
|
||||
&self,
|
||||
position: Anchor,
|
||||
row: DisplayRow,
|
||||
kind: &BreakpointKind,
|
||||
breakpoint: &Breakpoint,
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let color = if self
|
||||
.gutter_breakpoint_indicator
|
||||
.is_some_and(|gutter_bp| gutter_bp.row() == row)
|
||||
{
|
||||
Color::Hint
|
||||
} else {
|
||||
Color::Debugger
|
||||
let (color, icon) = {
|
||||
let color = if self
|
||||
.gutter_breakpoint_indicator
|
||||
.is_some_and(|point| point.row() == row)
|
||||
{
|
||||
Color::Hint
|
||||
} else if breakpoint.is_disabled() {
|
||||
Color::Custom(Color::Debugger.color(cx).opacity(0.5))
|
||||
} else {
|
||||
Color::Debugger
|
||||
};
|
||||
let icon = match &breakpoint.kind {
|
||||
BreakpointKind::Standard => ui::IconName::DebugBreakpoint,
|
||||
BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint,
|
||||
};
|
||||
(color, icon)
|
||||
};
|
||||
|
||||
let icon = match &kind {
|
||||
BreakpointKind::Standard => ui::IconName::DebugBreakpoint,
|
||||
BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint,
|
||||
};
|
||||
let arc_kind = Arc::new(kind.clone());
|
||||
let arc_kind2 = arc_kind.clone();
|
||||
let breakpoint = Arc::from(breakpoint.clone());
|
||||
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(color)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener(move |editor, _e, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
arc_kind.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
.on_click(cx.listener({
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
move |editor, _e, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(position),
|
||||
arc_kind2.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
@@ -6384,13 +6458,7 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let color = Color::Muted;
|
||||
|
||||
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
|
||||
let bp_kind = Arc::new(
|
||||
breakpoint
|
||||
.map(|(_, bp)| bp.kind)
|
||||
.unwrap_or(BreakpointKind::Standard),
|
||||
);
|
||||
|
||||
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
@@ -6408,14 +6476,7 @@ impl Editor {
|
||||
);
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
position,
|
||||
bp_kind.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -6435,6 +6496,10 @@ impl Editor {
|
||||
.map(|menu| menu.origin())
|
||||
}
|
||||
|
||||
pub fn set_context_menu_options(&mut self, options: ContextMenuOptions) {
|
||||
self.context_menu_options = Some(options);
|
||||
}
|
||||
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
@@ -6864,7 +6929,7 @@ impl Editor {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(line_height + BORDER_WIDTH * px(2.))
|
||||
.h(line_height + BORDER_WIDTH * 2.)
|
||||
.px_1p5()
|
||||
.gap_1()
|
||||
// Workaround: For some reason, there's a gap if we don't do this
|
||||
@@ -8388,9 +8453,8 @@ impl Editor {
|
||||
|
||||
fn set_breakpoint_context_menu(
|
||||
&mut self,
|
||||
row: DisplayRow,
|
||||
display_row: DisplayRow,
|
||||
position: Option<Anchor>,
|
||||
kind: Arc<BreakpointKind>,
|
||||
clicked_point: gpui::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -8402,10 +8466,9 @@ impl Editor {
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.anchor_before(Point::new(row.0, 0u32));
|
||||
.anchor_before(Point::new(display_row.0, 0u32));
|
||||
|
||||
let context_menu =
|
||||
self.breakpoint_context_menu(position.unwrap_or(source), kind, window, cx);
|
||||
let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx);
|
||||
|
||||
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
self,
|
||||
@@ -8420,13 +8483,14 @@ impl Editor {
|
||||
fn add_edit_breakpoint_block(
|
||||
&mut self,
|
||||
anchor: Anchor,
|
||||
kind: &BreakpointKind,
|
||||
breakpoint: &Breakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let weak_editor = cx.weak_entity();
|
||||
let bp_prompt =
|
||||
cx.new(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), window, cx));
|
||||
let bp_prompt = cx.new(|cx| {
|
||||
BreakpointPromptEditor::new(weak_editor, anchor, breakpoint.clone(), window, cx)
|
||||
});
|
||||
|
||||
let height = bp_prompt.update(cx, |this, cx| {
|
||||
this.prompt
|
||||
@@ -8453,36 +8517,45 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn breakpoint_at_cursor_head(
|
||||
fn breakpoint_at_cursor_head(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<(Anchor, Breakpoint)> {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
self.breakpoint_at_row(cursor_position.row, window, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn breakpoint_at_row(
|
||||
&self,
|
||||
row: u32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<(Anchor, Breakpoint)> {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
// We Set the column position to zero so this function interacts correctly
|
||||
// between calls by clicking on the gutter & using an action to toggle a
|
||||
// breakpoint. Otherwise, toggling a breakpoint through an action wouldn't
|
||||
// untoggle a breakpoint that was added through clicking on the gutter
|
||||
let cursor_position = snapshot
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(cursor_position.row, 0));
|
||||
let breakpoint_position = snapshot.buffer_snapshot.anchor_before(Point::new(row, 0));
|
||||
|
||||
let project = self.project.clone();
|
||||
let project = self.project.clone()?;
|
||||
|
||||
let buffer_id = cursor_position.text_anchor.buffer_id?;
|
||||
let enclosing_excerpt = snapshot
|
||||
.buffer_snapshot
|
||||
.excerpt_ids_for_range(cursor_position..cursor_position)
|
||||
.next()?;
|
||||
let buffer = project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?;
|
||||
let buffer_id = breakpoint_position.buffer_id.or_else(|| {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_id_for_excerpt(breakpoint_position.excerpt_id)
|
||||
})?;
|
||||
|
||||
let enclosing_excerpt = breakpoint_position.excerpt_id;
|
||||
let buffer = project.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?;
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let row = buffer_snapshot
|
||||
.summary_for_anchor::<text::PointUtf16>(&cursor_position.text_anchor)
|
||||
.summary_for_anchor::<text::PointUtf16>(&breakpoint_position.text_anchor)
|
||||
.row;
|
||||
|
||||
let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row));
|
||||
let anchor_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(row, line_len));
|
||||
|
||||
let bp = self
|
||||
.breakpoint_store
|
||||
.as_ref()?
|
||||
@@ -8490,12 +8563,12 @@ impl Editor {
|
||||
breakpoint_store
|
||||
.breakpoints(
|
||||
&buffer,
|
||||
Some(cursor_position.text_anchor..(text::Anchor::MAX)),
|
||||
buffer_snapshot.clone(),
|
||||
Some(breakpoint_position.text_anchor..anchor_end.text_anchor),
|
||||
&buffer_snapshot,
|
||||
cx,
|
||||
)
|
||||
.next()
|
||||
.and_then(move |(anchor, bp)| {
|
||||
.and_then(|(anchor, bp)| {
|
||||
let breakpoint_row = buffer_snapshot
|
||||
.summary_for_anchor::<text::PointUtf16>(anchor)
|
||||
.row;
|
||||
@@ -8534,11 +8607,48 @@ impl Editor {
|
||||
breakpoint_position,
|
||||
Breakpoint {
|
||||
kind: BreakpointKind::Standard,
|
||||
state: BreakpointState::Enabled,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
self.add_edit_breakpoint_block(anchor, &bp.kind, window, cx);
|
||||
self.add_edit_breakpoint_block(anchor, &bp, window, cx);
|
||||
}
|
||||
|
||||
pub fn enable_breakpoint(
|
||||
&mut self,
|
||||
_: &crate::actions::EnableBreakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
|
||||
if breakpoint.is_disabled() {
|
||||
self.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint,
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_breakpoint(
|
||||
&mut self,
|
||||
_: &crate::actions::DisableBreakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
|
||||
if breakpoint.is_enabled() {
|
||||
self.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint,
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint(
|
||||
@@ -8550,7 +8660,7 @@ impl Editor {
|
||||
let edit_action = BreakpointEditAction::Toggle;
|
||||
|
||||
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
|
||||
self.edit_breakpoint_at_anchor(anchor, breakpoint.kind, edit_action, cx);
|
||||
self.edit_breakpoint_at_anchor(anchor, breakpoint, edit_action, cx);
|
||||
} else {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
|
||||
@@ -8562,7 +8672,7 @@ impl Editor {
|
||||
|
||||
self.edit_breakpoint_at_anchor(
|
||||
breakpoint_position,
|
||||
BreakpointKind::Standard,
|
||||
Breakpoint::new_standard(),
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
@@ -8572,7 +8682,7 @@ impl Editor {
|
||||
pub fn edit_breakpoint_at_anchor(
|
||||
&mut self,
|
||||
breakpoint_position: Anchor,
|
||||
kind: BreakpointKind,
|
||||
breakpoint: Breakpoint,
|
||||
edit_action: BreakpointEditAction,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -8601,7 +8711,7 @@ impl Editor {
|
||||
breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
breakpoint_store.toggle_breakpoint(
|
||||
buffer,
|
||||
(breakpoint_position.text_anchor, Breakpoint { kind }),
|
||||
(breakpoint_position.text_anchor, breakpoint),
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
@@ -12603,15 +12713,14 @@ impl Editor {
|
||||
let start = snapshot.buffer_snapshot.anchor_before(start);
|
||||
let end = snapshot.buffer_snapshot.anchor_before(end);
|
||||
|
||||
self.clear_row_highlights::<T>();
|
||||
self.highlight_rows::<T>(
|
||||
start..end,
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
true,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center(), cx);
|
||||
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
|
||||
}
|
||||
|
||||
pub fn go_to_definition(
|
||||
@@ -12622,15 +12731,21 @@ impl Editor {
|
||||
) -> Task<Result<Navigated>> {
|
||||
let definition =
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx);
|
||||
let fallback_strategy = EditorSettings::get_global(cx).go_to_definition_fallback;
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
if definition.await? == Navigated::Yes {
|
||||
return Ok(Navigated::Yes);
|
||||
}
|
||||
match editor.update_in(cx, |editor, window, cx| {
|
||||
editor.find_all_references(&FindAllReferences, window, cx)
|
||||
})? {
|
||||
Some(references) => references.await,
|
||||
None => Ok(Navigated::No),
|
||||
match fallback_strategy {
|
||||
GoToDefinitionFallback::None => Ok(Navigated::No),
|
||||
GoToDefinitionFallback::FindAllReferences => {
|
||||
match editor.update_in(cx, |editor, window, cx| {
|
||||
editor.find_all_references(&FindAllReferences, window, cx)
|
||||
})? {
|
||||
Some(references) => references.await,
|
||||
None => Ok(Navigated::No),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -17857,6 +17972,7 @@ pub trait SemanticsProvider {
|
||||
pub trait CompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
excerpt_id: ExcerptId,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: text::Anchor,
|
||||
trigger: CompletionContext,
|
||||
@@ -18090,6 +18206,7 @@ fn snippet_completions(
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: snippet
|
||||
.description
|
||||
.clone()
|
||||
@@ -18106,6 +18223,7 @@ fn snippet_completions(
|
||||
impl CompletionProvider for Entity<Project> {
|
||||
fn completions(
|
||||
&self,
|
||||
_excerpt_id: ExcerptId,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: text::Anchor,
|
||||
options: CompletionContext,
|
||||
@@ -19555,7 +19673,7 @@ struct BreakpointPromptEditor {
|
||||
pub(crate) prompt: Entity<Editor>,
|
||||
editor: WeakEntity<Editor>,
|
||||
breakpoint_anchor: Anchor,
|
||||
kind: BreakpointKind,
|
||||
breakpoint: Breakpoint,
|
||||
block_ids: HashSet<CustomBlockId>,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -19567,13 +19685,15 @@ impl BreakpointPromptEditor {
|
||||
fn new(
|
||||
editor: WeakEntity<Editor>,
|
||||
breakpoint_anchor: Anchor,
|
||||
kind: BreakpointKind,
|
||||
breakpoint: Breakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::local(
|
||||
kind.log_message()
|
||||
breakpoint
|
||||
.kind
|
||||
.log_message()
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or_default(),
|
||||
cx,
|
||||
@@ -19605,7 +19725,7 @@ impl BreakpointPromptEditor {
|
||||
prompt,
|
||||
editor,
|
||||
breakpoint_anchor,
|
||||
kind,
|
||||
breakpoint,
|
||||
gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())),
|
||||
block_ids: Default::default(),
|
||||
_subscriptions: vec![],
|
||||
@@ -19632,7 +19752,7 @@ impl BreakpointPromptEditor {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
self.breakpoint_anchor,
|
||||
self.kind.clone(),
|
||||
self.breakpoint.clone(),
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -36,6 +36,8 @@ pub struct EditorSettings {
|
||||
pub search: SearchSettings,
|
||||
pub auto_signature_help: bool,
|
||||
pub show_signature_help_after_edits: bool,
|
||||
#[serde(default)]
|
||||
pub go_to_definition_fallback: GoToDefinitionFallback,
|
||||
pub jupyter: Jupyter,
|
||||
}
|
||||
|
||||
@@ -156,7 +158,7 @@ pub struct ScrollbarAxes {
|
||||
/// Which diagnostic indicators to show in the scrollbar.
|
||||
///
|
||||
/// Default: all
|
||||
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ScrollbarDiagnostics {
|
||||
/// Show all diagnostic levels: hint, information, warnings, error.
|
||||
@@ -171,55 +173,6 @@ pub enum ScrollbarDiagnostics {
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ScrollbarDiagnostics {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for Visitor {
|
||||
type Value = ScrollbarDiagnostics;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"a boolean or one of "all", "information", "warning", "error", "none""#
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match b {
|
||||
false => Ok(ScrollbarDiagnostics::None),
|
||||
true => Ok(ScrollbarDiagnostics::All),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match s {
|
||||
"all" => Ok(ScrollbarDiagnostics::All),
|
||||
"information" => Ok(ScrollbarDiagnostics::Information),
|
||||
"warning" => Ok(ScrollbarDiagnostics::Warning),
|
||||
"error" => Ok(ScrollbarDiagnostics::Error),
|
||||
"none" => Ok(ScrollbarDiagnostics::None),
|
||||
_ => Err(E::unknown_variant(
|
||||
s,
|
||||
&["all", "information", "warning", "error", "none"],
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// The key to use for adding multiple cursors
|
||||
///
|
||||
/// Default: alt
|
||||
@@ -260,6 +213,17 @@ pub struct SearchSettings {
|
||||
pub regex: bool,
|
||||
}
|
||||
|
||||
/// What to do when go to definition yields no results.
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GoToDefinitionFallback {
|
||||
/// Disables the fallback.
|
||||
None,
|
||||
/// Looks up references of the same symbol instead.
|
||||
#[default]
|
||||
FindAllReferences,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditorSettingsContent {
|
||||
/// Whether the cursor blinks in the editor.
|
||||
@@ -379,6 +343,13 @@ pub struct EditorSettingsContent {
|
||||
/// Default: false
|
||||
pub show_signature_help_after_edits: Option<bool>,
|
||||
|
||||
/// Whether to follow-up empty go to definition responses from the language server or not.
|
||||
/// `FindAllReferences` allows to look up references of the same symbol instead.
|
||||
/// `None` disables the fallback.
|
||||
///
|
||||
/// Default: FindAllReferences
|
||||
pub go_to_definition_fallback: Option<GoToDefinitionFallback>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<JupyterContent>,
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ use multi_buffer::{IndentGuide, PathKey};
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{
|
||||
debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint},
|
||||
debugger::breakpoint_store::{BreakpointKind, BreakpointState, SerializedBreakpoint},
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
FakeFs,
|
||||
};
|
||||
@@ -16434,6 +16434,69 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestApp
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
cx.update(|cx| {
|
||||
let mut editor_settings = EditorSettings::get_global(cx).clone();
|
||||
editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
|
||||
EditorSettings::override_global(editor_settings, cx);
|
||||
});
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
references_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let original_state = r#"fn one() {
|
||||
let mut a = ˇtwo();
|
||||
}
|
||||
|
||||
fn two() {}"#
|
||||
.unindent();
|
||||
cx.set_state(&original_state);
|
||||
|
||||
let mut go_to_definition = cx
|
||||
.lsp
|
||||
.set_request_handler::<lsp::request::GotoDefinition, _, _>(
|
||||
move |_, _| async move { Ok(None) },
|
||||
);
|
||||
let _references = cx
|
||||
.lsp
|
||||
.set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
|
||||
panic!("Should not call for references with no go to definition fallback")
|
||||
});
|
||||
|
||||
let navigated = cx
|
||||
.update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
|
||||
.await
|
||||
.expect("Failed to navigate to lookup references");
|
||||
go_to_definition
|
||||
.next()
|
||||
.await
|
||||
.expect("Should have called the go_to_definition handler");
|
||||
|
||||
assert_eq!(
|
||||
navigated,
|
||||
Navigated::No,
|
||||
"Should have navigated to references as a fallback after empty GoToDefinition response"
|
||||
);
|
||||
cx.assert_editor_state(&original_state);
|
||||
let editors = cx.update_workspace(|workspace, _, cx| {
|
||||
workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
|
||||
});
|
||||
cx.update_editor(|_, _, _| {
|
||||
assert_eq!(
|
||||
editors.len(),
|
||||
1,
|
||||
"After unsuccessful fallback, no other editor should have been opened"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -17329,7 +17392,7 @@ async fn assert_highlighted_edits(
|
||||
fn assert_breakpoint(
|
||||
breakpoints: &BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
|
||||
path: &Arc<Path>,
|
||||
expected: Vec<(u32, BreakpointKind)>,
|
||||
expected: Vec<(u32, Breakpoint)>,
|
||||
) {
|
||||
if expected.len() == 0usize {
|
||||
assert!(!breakpoints.contains_key(path));
|
||||
@@ -17338,7 +17401,15 @@ fn assert_breakpoint(
|
||||
.get(path)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|breakpoint| (breakpoint.position, breakpoint.kind.clone()))
|
||||
.map(|breakpoint| {
|
||||
(
|
||||
breakpoint.position,
|
||||
Breakpoint {
|
||||
kind: breakpoint.kind.clone(),
|
||||
state: breakpoint.state,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
|
||||
@@ -17366,12 +17437,18 @@ fn add_log_breakpoint_at_cursor(
|
||||
|
||||
let kind = BreakpointKind::Log(Arc::from(log_message));
|
||||
|
||||
(breakpoint_position, Breakpoint { kind })
|
||||
(
|
||||
breakpoint_position,
|
||||
Breakpoint {
|
||||
kind,
|
||||
state: BreakpointState::Enabled,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
bp.kind,
|
||||
bp,
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
@@ -17459,7 +17536,10 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
|
||||
vec![
|
||||
(0, Breakpoint::new_standard()),
|
||||
(3, Breakpoint::new_standard()),
|
||||
],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
@@ -17478,7 +17558,11 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints.len());
|
||||
assert_breakpoint(&breakpoints, &abs_path, vec![(3, BreakpointKind::Standard)]);
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![(3, Breakpoint::new_standard())],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
@@ -17565,7 +17649,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![(0, BreakpointKind::Log("hello world".into()))],
|
||||
vec![(0, Breakpoint::new_log("hello world"))],
|
||||
);
|
||||
|
||||
// Removing a log message from a log breakpoint should remove it
|
||||
@@ -17606,7 +17690,10 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
|
||||
vec![
|
||||
(0, Breakpoint::new_standard()),
|
||||
(3, Breakpoint::new_standard()),
|
||||
],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
@@ -17627,8 +17714,8 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![
|
||||
(0, BreakpointKind::Standard),
|
||||
(3, BreakpointKind::Log("hello world".into())),
|
||||
(0, Breakpoint::new_standard()),
|
||||
(3, Breakpoint::new_log("hello world")),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -17650,8 +17737,167 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![
|
||||
(0, BreakpointKind::Standard),
|
||||
(3, BreakpointKind::Log("hello Earth !!".into())),
|
||||
(0, Breakpoint::new_standard()),
|
||||
(3, Breakpoint::new_log("hello Earth !!")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// This also tests that Editor::breakpoint_at_cursor_head is working properly
|
||||
/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
|
||||
/// or when breakpoints were placed out of order. This tests for a regression too
|
||||
#[gpui::test]
|
||||
async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/a"),
|
||||
json!({
|
||||
"main.rs": sample_text,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/a"),
|
||||
json!({
|
||||
"main.rs": sample_text,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let worktree_id = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::Full,
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
|
||||
let abs_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.absolute_path(&project_path, cx)
|
||||
.map(|path_buf| Arc::from(path_buf.to_owned()))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
// assert we can add breakpoint on the first line
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints.len());
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![
|
||||
(0, Breakpoint::new_standard()),
|
||||
(2, Breakpoint::new_standard()),
|
||||
(3, Breakpoint::new_standard()),
|
||||
],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.move_to_beginning(&MoveToBeginning, window, cx);
|
||||
editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
let disable_breakpoint = {
|
||||
let mut bp = Breakpoint::new_standard();
|
||||
bp.state = BreakpointState::Disabled;
|
||||
bp
|
||||
};
|
||||
|
||||
assert_eq!(1, breakpoints.len());
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![
|
||||
(0, disable_breakpoint.clone()),
|
||||
(2, Breakpoint::new_standard()),
|
||||
(3, disable_breakpoint.clone()),
|
||||
],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.move_to_beginning(&MoveToBeginning, window, cx);
|
||||
editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints.len());
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&abs_path,
|
||||
vec![
|
||||
(0, Breakpoint::new_standard()),
|
||||
(2, disable_breakpoint),
|
||||
(3, Breakpoint::new_standard()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
editor_settings::MultiCursorModifier,
|
||||
editor_settings::{GoToDefinitionFallback, MultiCursorModifier},
|
||||
hover_popover::{self, InlayHover},
|
||||
scroll::ScrollAmount,
|
||||
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
|
||||
@@ -174,7 +174,12 @@ impl Editor {
|
||||
if definition_revealed == Navigated::Yes {
|
||||
return None;
|
||||
}
|
||||
editor.find_all_references(&FindAllReferences, window, cx)
|
||||
match EditorSettings::get_global(cx).go_to_definition_fallback {
|
||||
GoToDefinitionFallback::None => None,
|
||||
GoToDefinitionFallback::FindAllReferences => {
|
||||
editor.find_all_references(&FindAllReferences, window, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
@@ -29,7 +29,7 @@ use workspace::{OpenOptions, OpenVisible, Workspace};
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
|
||||
pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
|
||||
pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
|
||||
pub const HOVER_POPOVER_GAP: Pixels = px(10.);
|
||||
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
|
||||
@@ -2,7 +2,7 @@ mod actions;
|
||||
pub(crate) mod autoscroll;
|
||||
pub(crate) mod scroll_amount;
|
||||
|
||||
use crate::editor_settings::{ScrollBeyondLastLine, ScrollbarAxes};
|
||||
use crate::editor_settings::ScrollBeyondLastLine;
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
|
||||
use core::fmt::Debug;
|
||||
use gpui::{point, px, Along, App, Axis, Context, Global, Pixels, Task, Window};
|
||||
use gpui::{point, px, App, Axis, Context, Global, Pixels, Task, Window};
|
||||
use language::{Bias, Point};
|
||||
pub use scroll_amount::ScrollAmount;
|
||||
use settings::Settings;
|
||||
@@ -61,55 +61,6 @@ impl ScrollAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AxisPair<T: Clone> {
|
||||
pub vertical: T,
|
||||
pub horizontal: T,
|
||||
}
|
||||
|
||||
pub fn axis_pair<T: Clone>(horizontal: T, vertical: T) -> AxisPair<T> {
|
||||
AxisPair {
|
||||
vertical,
|
||||
horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> AxisPair<T> {
|
||||
pub fn as_xy(&self) -> (&T, &T) {
|
||||
(&self.horizontal, &self.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Along for AxisPair<T> {
|
||||
type Unit = T;
|
||||
|
||||
fn along(&self, axis: gpui::Axis) -> Self::Unit {
|
||||
match axis {
|
||||
gpui::Axis::Horizontal => self.horizontal.clone(),
|
||||
gpui::Axis::Vertical => self.vertical.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_along(&self, axis: gpui::Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self {
|
||||
match axis {
|
||||
gpui::Axis::Horizontal => Self {
|
||||
horizontal: f(self.horizontal.clone()),
|
||||
vertical: self.vertical.clone(),
|
||||
},
|
||||
gpui::Axis::Vertical => Self {
|
||||
horizontal: self.horizontal.clone(),
|
||||
vertical: f(self.vertical.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScrollbarAxes> for AxisPair<bool> {
|
||||
fn from(value: ScrollbarAxes) -> Self {
|
||||
axis_pair(value.horizontal, value.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct OngoingScroll {
|
||||
last_event: Instant,
|
||||
@@ -180,7 +131,7 @@ pub struct ScrollManager {
|
||||
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
dragging_scrollbar: AxisPair<bool>,
|
||||
dragging_scrollbar: Option<Axis>,
|
||||
visible_line_count: Option<f32>,
|
||||
forbid_vertical_scroll: bool,
|
||||
}
|
||||
@@ -194,7 +145,7 @@ impl ScrollManager {
|
||||
autoscroll_request: None,
|
||||
show_scrollbars: true,
|
||||
hide_scrollbar_task: None,
|
||||
dragging_scrollbar: axis_pair(false, false),
|
||||
dragging_scrollbar: None,
|
||||
last_autoscroll: None,
|
||||
visible_line_count: None,
|
||||
forbid_vertical_scroll: false,
|
||||
@@ -312,7 +263,7 @@ impl ScrollManager {
|
||||
}
|
||||
self.anchor = anchor;
|
||||
cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
|
||||
self.show_scrollbar(window, cx);
|
||||
self.show_scrollbars(window, cx);
|
||||
self.autoscroll_request.take();
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
let item_id = cx.entity().entity_id().as_u64() as ItemId;
|
||||
@@ -334,7 +285,7 @@ impl ScrollManager {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn show_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
if !self.show_scrollbars {
|
||||
self.show_scrollbars = true;
|
||||
cx.notify();
|
||||
@@ -365,18 +316,26 @@ impl ScrollManager {
|
||||
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
|
||||
}
|
||||
|
||||
pub fn is_dragging_scrollbar(&self, axis: Axis) -> bool {
|
||||
self.dragging_scrollbar.along(axis)
|
||||
pub fn dragging_scrollbar_axis(&self) -> Option<Axis> {
|
||||
self.dragging_scrollbar
|
||||
}
|
||||
|
||||
pub fn set_is_dragging_scrollbar(
|
||||
&mut self,
|
||||
axis: Axis,
|
||||
dragging: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.dragging_scrollbar = self.dragging_scrollbar.apply_along(axis, |_| dragging);
|
||||
cx.notify();
|
||||
pub fn any_scrollbar_dragged(&self) -> bool {
|
||||
self.dragging_scrollbar.is_some()
|
||||
}
|
||||
|
||||
pub fn set_dragged_scrollbar_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
|
||||
if self.dragging_scrollbar != Some(axis) {
|
||||
self.dragging_scrollbar = Some(axis);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_scrollbar_dragging_state(&mut self, cx: &mut Context<Editor>) {
|
||||
if self.dragging_scrollbar.is_some() {
|
||||
self.dragging_scrollbar = None;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
|
||||
|
||||
@@ -3,54 +3,66 @@ use crate::{
|
||||
};
|
||||
use gpui::{px, Bounds, Context, Pixels, Window};
|
||||
use language::Point;
|
||||
use multi_buffer::Anchor;
|
||||
use std::{cmp, f32};
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Autoscroll {
|
||||
Next,
|
||||
Strategy(AutoscrollStrategy),
|
||||
Strategy(AutoscrollStrategy, Option<Anchor>),
|
||||
}
|
||||
|
||||
impl Autoscroll {
|
||||
/// scrolls the minimal amount to (try) and fit all cursors onscreen
|
||||
pub fn fit() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Fit)
|
||||
Self::Strategy(AutoscrollStrategy::Fit, None)
|
||||
}
|
||||
|
||||
/// scrolls the minimal amount to fit the newest cursor
|
||||
pub fn newest() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Newest)
|
||||
Self::Strategy(AutoscrollStrategy::Newest, None)
|
||||
}
|
||||
|
||||
/// scrolls so the newest cursor is vertically centered
|
||||
pub fn center() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Center)
|
||||
Self::Strategy(AutoscrollStrategy::Center, None)
|
||||
}
|
||||
|
||||
/// scrolls so the newest cursor is near the top
|
||||
/// (offset by vertical_scroll_margin)
|
||||
pub fn focused() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Focused)
|
||||
Self::Strategy(AutoscrollStrategy::Focused, None)
|
||||
}
|
||||
|
||||
/// Scrolls so that the newest cursor is roughly an n-th line from the top.
|
||||
pub fn top_relative(n: usize) -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::TopRelative(n))
|
||||
Self::Strategy(AutoscrollStrategy::TopRelative(n), None)
|
||||
}
|
||||
|
||||
/// Scrolls so that the newest cursor is at the top.
|
||||
pub fn top() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Top)
|
||||
Self::Strategy(AutoscrollStrategy::Top, None)
|
||||
}
|
||||
|
||||
/// Scrolls so that the newest cursor is roughly an n-th line from the bottom.
|
||||
pub fn bottom_relative(n: usize) -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::BottomRelative(n))
|
||||
Self::Strategy(AutoscrollStrategy::BottomRelative(n), None)
|
||||
}
|
||||
|
||||
/// Scrolls so that the newest cursor is at the bottom.
|
||||
pub fn bottom() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Bottom)
|
||||
Self::Strategy(AutoscrollStrategy::Bottom, None)
|
||||
}
|
||||
|
||||
/// Applies a given auto-scroll strategy to a given anchor instead of a cursor.
|
||||
/// E.G: Autoscroll::center().for_anchor(...) results in the anchor being at the center of the screen.
|
||||
pub fn for_anchor(self, anchor: Anchor) -> Self {
|
||||
match self {
|
||||
Autoscroll::Next => self,
|
||||
Autoscroll::Strategy(autoscroll_strategy, _) => {
|
||||
Self::Strategy(autoscroll_strategy, Some(anchor))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,8 +154,11 @@ impl Editor {
|
||||
.as_f32();
|
||||
|
||||
let selections_fit = target_bottom - target_top <= visible_lines;
|
||||
if autoscroll == Autoscroll::newest()
|
||||
|| (autoscroll == Autoscroll::fit() && !selections_fit)
|
||||
if matches!(
|
||||
autoscroll,
|
||||
Autoscroll::Strategy(AutoscrollStrategy::Newest, _)
|
||||
) || (matches!(autoscroll, Autoscroll::Strategy(AutoscrollStrategy::Fit, _))
|
||||
&& !selections_fit)
|
||||
{
|
||||
let newest_selection_top = selections
|
||||
.iter()
|
||||
@@ -165,7 +180,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
let strategy = match autoscroll {
|
||||
Autoscroll::Strategy(strategy) => strategy,
|
||||
Autoscroll::Strategy(strategy, _) => strategy,
|
||||
Autoscroll::Next => {
|
||||
let last_autoscroll = &self.scroll_manager.last_autoscroll;
|
||||
if let Some(last_autoscroll) = last_autoscroll {
|
||||
@@ -182,6 +197,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Autoscroll::Strategy(_, Some(anchor)) = autoscroll {
|
||||
target_top = anchor.to_display_point(&display_map).row().as_f32();
|
||||
target_bottom = target_top + 1.;
|
||||
}
|
||||
|
||||
match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
|
||||
@@ -76,6 +76,20 @@ pub trait Extension: Send + Sync + 'static {
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn language_server_additional_initialization_options(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
target_language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn language_server_additional_workspace_configuration(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
target_language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn labels_for_completions(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "zed_extension_api"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
description = "APIs for creating Zed extensions in Rust"
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
documentation = "https://docs.rs/zed_extension_api"
|
||||
keywords = ["zed", "extension"]
|
||||
edition.workspace = true
|
||||
publish = true
|
||||
# Change back to `true` when we're ready to publish v0.4.0.
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -93,6 +93,26 @@ pub trait Extension: Send + Sync {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the initialization options to pass to the other language server.
|
||||
fn language_server_additional_initialization_options(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_target_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the workspace configuration options to pass to the other language server.
|
||||
fn language_server_additional_workspace_configuration(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_target_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the label for the given completion.
|
||||
fn label_for_completion(
|
||||
&self,
|
||||
@@ -198,7 +218,7 @@ mod wit {
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.3.0",
|
||||
path: "./wit/since_v0.4.0",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,6 +255,38 @@ impl wit::Guest for Component {
|
||||
.and_then(|value| serde_json::to_string(&value).ok()))
|
||||
}
|
||||
|
||||
fn language_server_additional_initialization_options(
|
||||
language_server_id: String,
|
||||
target_language_server_id: String,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Option<String>, String> {
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
let target_language_server_id = LanguageServerId(target_language_server_id);
|
||||
Ok(extension()
|
||||
.language_server_additional_initialization_options(
|
||||
&language_server_id,
|
||||
&target_language_server_id,
|
||||
worktree,
|
||||
)?
|
||||
.and_then(|value| serde_json::to_string(&value).ok()))
|
||||
}
|
||||
|
||||
fn language_server_additional_workspace_configuration(
|
||||
language_server_id: String,
|
||||
target_language_server_id: String,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Option<String>, String> {
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
let target_language_server_id = LanguageServerId(target_language_server_id);
|
||||
Ok(extension()
|
||||
.language_server_additional_workspace_configuration(
|
||||
&language_server_id,
|
||||
&target_language_server_id,
|
||||
worktree,
|
||||
)?
|
||||
.and_then(|value| serde_json::to_string(&value).ok()))
|
||||
}
|
||||
|
||||
fn labels_for_completions(
|
||||
language_server_id: String,
|
||||
completions: Vec<Completion>,
|
||||
|
||||
12
crates/extension_api/wit/since_v0.4.0/common.wit
Normal file
12
crates/extension_api/wit/since_v0.4.0/common.wit
Normal file
@@ -0,0 +1,12 @@
|
||||
interface common {
|
||||
/// A (half-open) range (`[start, end)`).
|
||||
record range {
|
||||
/// The start of the range (inclusive).
|
||||
start: u32,
|
||||
/// The end of the range (exclusive).
|
||||
end: u32,
|
||||
}
|
||||
|
||||
/// A list of environment variables.
|
||||
type env-vars = list<tuple<string, string>>;
|
||||
}
|
||||
151
crates/extension_api/wit/since_v0.4.0/extension.wit
Normal file
151
crates/extension_api/wit/since_v0.4.0/extension.wit
Normal file
@@ -0,0 +1,151 @@
|
||||
package zed:extension;
|
||||
|
||||
world extension {
|
||||
import github;
|
||||
import http-client;
|
||||
import platform;
|
||||
import process;
|
||||
import nodejs;
|
||||
|
||||
use common.{env-vars, range};
|
||||
use lsp.{completion, symbol};
|
||||
use process.{command};
|
||||
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
|
||||
|
||||
/// Initializes the extension.
|
||||
export init-extension: func();
|
||||
|
||||
/// The type of a downloaded file.
|
||||
enum downloaded-file-type {
|
||||
/// A gzipped file (`.gz`).
|
||||
gzip,
|
||||
/// A gzipped tar archive (`.tar.gz`).
|
||||
gzip-tar,
|
||||
/// A ZIP file (`.zip`).
|
||||
zip,
|
||||
/// An uncompressed file.
|
||||
uncompressed,
|
||||
}
|
||||
|
||||
/// The installation status for a language server.
|
||||
variant language-server-installation-status {
|
||||
/// The language server has no installation status.
|
||||
none,
|
||||
/// The language server is being downloaded.
|
||||
downloading,
|
||||
/// The language server is checking for updates.
|
||||
checking-for-update,
|
||||
/// The language server installation failed for specified reason.
|
||||
failed(string),
|
||||
}
|
||||
|
||||
record settings-location {
|
||||
worktree-id: u64,
|
||||
path: string,
|
||||
}
|
||||
|
||||
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
|
||||
|
||||
/// Downloads a file from the given URL and saves it to the given path within the extension's
|
||||
/// working directory.
|
||||
///
|
||||
/// The file will be extracted according to the given file type.
|
||||
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
|
||||
|
||||
/// Makes the file at the given path executable.
|
||||
import make-file-executable: func(filepath: string) -> result<_, string>;
|
||||
|
||||
/// Updates the installation status for the given language server.
|
||||
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
|
||||
|
||||
/// A Zed worktree.
|
||||
resource worktree {
|
||||
/// Returns the ID of the worktree.
|
||||
id: func() -> u64;
|
||||
/// Returns the root path of the worktree.
|
||||
root-path: func() -> string;
|
||||
/// Returns the textual contents of the specified file in the worktree.
|
||||
read-text-file: func(path: string) -> result<string, string>;
|
||||
/// Returns the path to the given binary name, if one is present on the `$PATH`.
|
||||
which: func(binary-name: string) -> option<string>;
|
||||
/// Returns the current shell environment.
|
||||
shell-env: func() -> env-vars;
|
||||
}
|
||||
|
||||
/// A Zed project.
|
||||
resource project {
|
||||
/// Returns the IDs of all of the worktrees in this project.
|
||||
worktree-ids: func() -> list<u64>;
|
||||
}
|
||||
|
||||
/// A key-value store.
|
||||
resource key-value-store {
|
||||
/// Inserts an entry under the specified key.
|
||||
insert: func(key: string, value: string) -> result<_, string>;
|
||||
}
|
||||
|
||||
/// Returns the command used to start up the language server.
|
||||
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
|
||||
|
||||
/// Returns the initialization options to pass to the language server on startup.
|
||||
///
|
||||
/// The initialization options are represented as a JSON string.
|
||||
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// Returns the workspace configuration options to pass to the language server.
|
||||
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// Returns the initialization options to pass to the other language server.
|
||||
export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// Returns the workspace configuration options to pass to the other language server.
|
||||
export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// A label containing some code.
|
||||
record code-label {
|
||||
/// The source code to parse with Tree-sitter.
|
||||
code: string,
|
||||
/// The spans to display in the label.
|
||||
spans: list<code-label-span>,
|
||||
/// The range of the displayed label to include when filtering.
|
||||
filter-range: range,
|
||||
}
|
||||
|
||||
/// A span within a code label.
|
||||
variant code-label-span {
|
||||
/// A range into the parsed code.
|
||||
code-range(range),
|
||||
/// A span containing a code literal.
|
||||
literal(code-label-span-literal),
|
||||
}
|
||||
|
||||
/// A span containing a code literal.
|
||||
record code-label-span-literal {
|
||||
/// The literal text.
|
||||
text: string,
|
||||
/// The name of the highlight to use for this literal.
|
||||
highlight-name: option<string>,
|
||||
}
|
||||
|
||||
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
|
||||
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
|
||||
|
||||
/// Returns the completions that should be shown when completing the provided slash command with the given query.
|
||||
export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>;
|
||||
|
||||
/// Returns the output from running the provided slash command.
|
||||
export run-slash-command: func(command: slash-command, args: list<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>;
|
||||
|
||||
/// Returns the command used to start up a context server.
|
||||
export context-server-command: func(context-server-id: string, project: borrow<project>) -> result<command, string>;
|
||||
|
||||
/// Returns a list of packages as suggestions to be included in the `/docs`
|
||||
/// search results.
|
||||
///
|
||||
/// This can be used to provide completions for known packages (e.g., from the
|
||||
/// local project or a registry) before a package has been indexed.
|
||||
export suggest-docs-packages: func(provider-name: string) -> result<list<string>, string>;
|
||||
|
||||
/// Indexes the docs for the specified package.
|
||||
export index-docs: func(provider-name: string, package-name: string, database: borrow<key-value-store>) -> result<_, string>;
|
||||
}
|
||||
35
crates/extension_api/wit/since_v0.4.0/github.wit
Normal file
35
crates/extension_api/wit/since_v0.4.0/github.wit
Normal file
@@ -0,0 +1,35 @@
|
||||
interface github {
|
||||
/// A GitHub release.
|
||||
record github-release {
|
||||
/// The version of the release.
|
||||
version: string,
|
||||
/// The list of assets attached to the release.
|
||||
assets: list<github-release-asset>,
|
||||
}
|
||||
|
||||
/// An asset from a GitHub release.
|
||||
record github-release-asset {
|
||||
/// The name of the asset.
|
||||
name: string,
|
||||
/// The download URL for the asset.
|
||||
download-url: string,
|
||||
}
|
||||
|
||||
/// The options used to filter down GitHub releases.
|
||||
record github-release-options {
|
||||
/// Whether releases without assets should be included.
|
||||
require-assets: bool,
|
||||
/// Whether pre-releases should be included.
|
||||
pre-release: bool,
|
||||
}
|
||||
|
||||
/// Returns the latest release for the given GitHub repository.
|
||||
///
|
||||
/// Takes repo as a string in the form "<owner-name>/<repo-name>", for example: "zed-industries/zed".
|
||||
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
|
||||
|
||||
/// Returns the GitHub release with the specified tag name for the given GitHub repository.
|
||||
///
|
||||
/// Returns an error if a release with the given tag name does not exist.
|
||||
github-release-by-tag-name: func(repo: string, tag: string) -> result<github-release, string>;
|
||||
}
|
||||
67
crates/extension_api/wit/since_v0.4.0/http-client.wit
Normal file
67
crates/extension_api/wit/since_v0.4.0/http-client.wit
Normal file
@@ -0,0 +1,67 @@
|
||||
interface http-client {
|
||||
/// An HTTP request.
|
||||
record http-request {
|
||||
/// The HTTP method for the request.
|
||||
method: http-method,
|
||||
/// The URL to which the request should be made.
|
||||
url: string,
|
||||
/// The headers for the request.
|
||||
headers: list<tuple<string, string>>,
|
||||
/// The request body.
|
||||
body: option<list<u8>>,
|
||||
/// The policy to use for redirects.
|
||||
redirect-policy: redirect-policy,
|
||||
}
|
||||
|
||||
/// HTTP methods.
|
||||
enum http-method {
|
||||
/// `GET`
|
||||
get,
|
||||
/// `HEAD`
|
||||
head,
|
||||
/// `POST`
|
||||
post,
|
||||
/// `PUT`
|
||||
put,
|
||||
/// `DELETE`
|
||||
delete,
|
||||
/// `OPTIONS`
|
||||
options,
|
||||
/// `PATCH`
|
||||
patch,
|
||||
}
|
||||
|
||||
/// The policy for dealing with redirects received from the server.
|
||||
variant redirect-policy {
|
||||
/// Redirects from the server will not be followed.
|
||||
///
|
||||
/// This is the default behavior.
|
||||
no-follow,
|
||||
/// Redirects from the server will be followed up to the specified limit.
|
||||
follow-limit(u32),
|
||||
/// All redirects from the server will be followed.
|
||||
follow-all,
|
||||
}
|
||||
|
||||
/// An HTTP response.
|
||||
record http-response {
|
||||
/// The response headers.
|
||||
headers: list<tuple<string, string>>,
|
||||
/// The response body.
|
||||
body: list<u8>,
|
||||
}
|
||||
|
||||
/// Performs an HTTP request and returns the response.
|
||||
fetch: func(req: http-request) -> result<http-response, string>;
|
||||
|
||||
/// An HTTP response stream.
|
||||
resource http-response-stream {
|
||||
/// Retrieves the next chunk of data from the response stream.
|
||||
///
|
||||
/// Returns `Ok(None)` if the stream has ended.
|
||||
next-chunk: func() -> result<option<list<u8>>, string>;
|
||||
}
|
||||
|
||||
/// Performs an HTTP request and returns a response stream.
|
||||
fetch-stream: func(req: http-request) -> result<http-response-stream, string>;
|
||||
}
|
||||
90
crates/extension_api/wit/since_v0.4.0/lsp.wit
Normal file
90
crates/extension_api/wit/since_v0.4.0/lsp.wit
Normal file
@@ -0,0 +1,90 @@
|
||||
interface lsp {
|
||||
/// An LSP completion.
|
||||
record completion {
|
||||
label: string,
|
||||
label-details: option<completion-label-details>,
|
||||
detail: option<string>,
|
||||
kind: option<completion-kind>,
|
||||
insert-text-format: option<insert-text-format>,
|
||||
}
|
||||
|
||||
/// The kind of an LSP completion.
|
||||
variant completion-kind {
|
||||
text,
|
||||
method,
|
||||
function,
|
||||
%constructor,
|
||||
field,
|
||||
variable,
|
||||
class,
|
||||
%interface,
|
||||
module,
|
||||
property,
|
||||
unit,
|
||||
value,
|
||||
%enum,
|
||||
keyword,
|
||||
snippet,
|
||||
color,
|
||||
file,
|
||||
reference,
|
||||
folder,
|
||||
enum-member,
|
||||
constant,
|
||||
struct,
|
||||
event,
|
||||
operator,
|
||||
type-parameter,
|
||||
other(s32),
|
||||
}
|
||||
|
||||
/// Label details for an LSP completion.
|
||||
record completion-label-details {
|
||||
detail: option<string>,
|
||||
description: option<string>,
|
||||
}
|
||||
|
||||
/// Defines how to interpret the insert text in a completion item.
|
||||
variant insert-text-format {
|
||||
plain-text,
|
||||
snippet,
|
||||
other(s32),
|
||||
}
|
||||
|
||||
/// An LSP symbol.
|
||||
record symbol {
|
||||
kind: symbol-kind,
|
||||
name: string,
|
||||
}
|
||||
|
||||
/// The kind of an LSP symbol.
|
||||
variant symbol-kind {
|
||||
file,
|
||||
module,
|
||||
namespace,
|
||||
%package,
|
||||
class,
|
||||
method,
|
||||
property,
|
||||
field,
|
||||
%constructor,
|
||||
%enum,
|
||||
%interface,
|
||||
function,
|
||||
variable,
|
||||
constant,
|
||||
%string,
|
||||
number,
|
||||
boolean,
|
||||
array,
|
||||
object,
|
||||
key,
|
||||
null,
|
||||
enum-member,
|
||||
struct,
|
||||
event,
|
||||
operator,
|
||||
type-parameter,
|
||||
other(s32),
|
||||
}
|
||||
}
|
||||
13
crates/extension_api/wit/since_v0.4.0/nodejs.wit
Normal file
13
crates/extension_api/wit/since_v0.4.0/nodejs.wit
Normal file
@@ -0,0 +1,13 @@
|
||||
interface nodejs {
|
||||
/// Returns the path to the Node binary used by Zed.
|
||||
node-binary-path: func() -> result<string, string>;
|
||||
|
||||
/// Returns the latest version of the given NPM package.
|
||||
npm-package-latest-version: func(package-name: string) -> result<string, string>;
|
||||
|
||||
/// Returns the installed version of the given NPM package, if it exists.
|
||||
npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
|
||||
|
||||
/// Installs the specified NPM package.
|
||||
npm-install-package: func(package-name: string, version: string) -> result<_, string>;
|
||||
}
|
||||
24
crates/extension_api/wit/since_v0.4.0/platform.wit
Normal file
24
crates/extension_api/wit/since_v0.4.0/platform.wit
Normal file
@@ -0,0 +1,24 @@
|
||||
interface platform {
|
||||
/// An operating system.
|
||||
enum os {
|
||||
/// macOS.
|
||||
mac,
|
||||
/// Linux.
|
||||
linux,
|
||||
/// Windows.
|
||||
windows,
|
||||
}
|
||||
|
||||
/// A platform architecture.
|
||||
enum architecture {
|
||||
/// AArch64 (e.g., Apple Silicon).
|
||||
aarch64,
|
||||
/// x86.
|
||||
x86,
|
||||
/// x86-64.
|
||||
x8664,
|
||||
}
|
||||
|
||||
/// Gets the current operating system and architecture.
|
||||
current-platform: func() -> tuple<os, architecture>;
|
||||
}
|
||||
29
crates/extension_api/wit/since_v0.4.0/process.wit
Normal file
29
crates/extension_api/wit/since_v0.4.0/process.wit
Normal file
@@ -0,0 +1,29 @@
|
||||
interface process {
|
||||
use common.{env-vars};
|
||||
|
||||
/// A command.
|
||||
record command {
|
||||
/// The command to execute.
|
||||
command: string,
|
||||
/// The arguments to pass to the command.
|
||||
args: list<string>,
|
||||
/// The environment variables to set for the command.
|
||||
env: env-vars,
|
||||
}
|
||||
|
||||
/// The output of a finished process.
|
||||
record output {
|
||||
/// The status (exit code) of the process.
|
||||
///
|
||||
/// On Unix, this will be `None` if the process was terminated by a signal.
|
||||
status: option<s32>,
|
||||
/// The data that the process wrote to stdout.
|
||||
stdout: list<u8>,
|
||||
/// The data that the process wrote to stderr.
|
||||
stderr: list<u8>,
|
||||
}
|
||||
|
||||
/// Executes the given command as a child process, waiting for it to finish
|
||||
/// and collecting all of its output.
|
||||
run-command: func(command: command) -> result<output, string>;
|
||||
}
|
||||
40
crates/extension_api/wit/since_v0.4.0/settings.rs
Normal file
40
crates/extension_api/wit/since_v0.4.0/settings.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, num::NonZeroU32};
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LanguageSettings {
|
||||
/// How many columns a tab should occupy.
|
||||
pub tab_size: NonZeroU32,
|
||||
}
|
||||
|
||||
/// The settings for a particular language server.
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct LspSettings {
|
||||
/// The settings for the language server binary.
|
||||
pub binary: Option<CommandSettings>,
|
||||
/// The initialization options to pass to the language server.
|
||||
pub initialization_options: Option<serde_json::Value>,
|
||||
/// The settings to pass to language server.
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// The settings for a particular context server.
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ContextServerSettings {
|
||||
/// The settings for the context server binary.
|
||||
pub command: Option<CommandSettings>,
|
||||
/// The settings to pass to the context server.
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// The settings for a command.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CommandSettings {
|
||||
/// The path to the command.
|
||||
pub path: Option<String>,
|
||||
/// The arguments to pass to the command.
|
||||
pub arguments: Option<Vec<String>>,
|
||||
/// The environment variables.
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user