Remove text thread and slash command crates (#52757)
🫡 Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Removed legacy Text Threads feature to help streamline the new agentic workflows in Zed. Thanks to all of you who were enthusiastic Text Thread users over the years ❤️! --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
1 parent
9503795c6c
commit
a0ef595454
113 files changed
+839
-17687
No files matched your search
@@ -32,9 +32,6 @@
|
||||
/crates/agent_ui/ @zed-industries/ai-team
|
||||
/crates/ai_onboarding/ @zed-industries/ai-team
|
||||
/crates/anthropic/ @zed-industries/ai-team
|
||||
/crates/assistant_slash_command/ @zed-industries/ai-team
|
||||
/crates/assistant_slash_commands/ @zed-industries/ai-team
|
||||
/crates/assistant_text_thread/ @zed-industries/ai-team
|
||||
/crates/bedrock/ @zed-industries/ai-team
|
||||
/crates/cloud_llm_client/ @zed-industries/ai-team
|
||||
/crates/codestral/ @zed-industries/ai-team
|
||||
|
||||
Generated
-111
@@ -335,9 +335,6 @@ dependencies = [
|
||||
"agent_settings",
|
||||
"ai_onboarding",
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"assistant_text_thread",
|
||||
"audio",
|
||||
"base64 0.22.1",
|
||||
"buffer_diff",
|
||||
@@ -393,7 +390,6 @@ dependencies = [
|
||||
"rope",
|
||||
"rules_library",
|
||||
"schemars",
|
||||
"search",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -783,108 +779,6 @@ dependencies = [
|
||||
"rust-embed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant_slash_command"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"extension",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"language",
|
||||
"language_model",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant_slash_commands"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"chrono",
|
||||
"collections",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"language",
|
||||
"multi_buffer",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"text",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"worktree",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant_text_thread"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent_settings",
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_server",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
"open_ai",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"text",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-attributes"
|
||||
version = "1.1.2"
|
||||
@@ -3183,8 +3077,6 @@ version = "0.44.0"
|
||||
dependencies = [
|
||||
"agent",
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"assistant_text_thread",
|
||||
"async-trait",
|
||||
"async-tungstenite",
|
||||
"aws-config",
|
||||
@@ -9446,7 +9338,6 @@ dependencies = [
|
||||
"open_ai",
|
||||
"open_router",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -15965,7 +15856,6 @@ dependencies = [
|
||||
"agent_settings",
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
"assistant_text_thread",
|
||||
"chrono",
|
||||
"collections",
|
||||
"editor",
|
||||
@@ -17510,7 +17400,6 @@ name = "terminal_view"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"async-recursion",
|
||||
"breadcrumbs",
|
||||
"collections",
|
||||
|
||||
@@ -13,9 +13,6 @@ members = [
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_slash_commands",
|
||||
"crates/assistant_text_thread",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_helper",
|
||||
@@ -271,9 +268,6 @@ ai_onboarding = { path = "crates/ai_onboarding" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant_text_thread = { path = "crates/assistant_text_thread" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.33333 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.6667 5H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 11H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 7V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 9H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 620 B |
@@ -148,7 +148,6 @@
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
"ctrl->": "agent::AddSelectionToThread",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange",
|
||||
@@ -185,7 +184,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"context": "Editor && jupyter",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace",
|
||||
@@ -221,29 +220,10 @@
|
||||
"shift-alt-z": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline",
|
||||
"ctrl-k c": "assistant::CopyCode",
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenSettings",
|
||||
"ctrl-alt-p": "agent::ManageProfiles",
|
||||
@@ -278,13 +258,6 @@
|
||||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && text_thread",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewTextThread",
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
@@ -719,8 +692,8 @@
|
||||
"context": "ThreadSwitcher",
|
||||
"bindings": {
|
||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
|
||||
}
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace && debugger_running",
|
||||
@@ -858,7 +831,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "!ContextEditor && !AcpThread > Editor && mode == full",
|
||||
"context": "!AcpThread > Editor && mode == full",
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
|
||||
@@ -173,7 +173,6 @@
|
||||
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
|
||||
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"cmd->": "agent::AddSelectionToThread",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer",
|
||||
},
|
||||
@@ -220,7 +219,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"context": "Editor && jupyter",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
@@ -260,31 +259,11 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline",
|
||||
"cmd-k c": "assistant::CopyCode",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewThread",
|
||||
"cmd-alt-n": "agent::NewTextThread",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenSettings",
|
||||
"cmd-alt-l": "agent::OpenRulesLibrary",
|
||||
@@ -315,14 +294,6 @@
|
||||
"cmd-c": "markdown::CopyAsMarkdown",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && text_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-n": "agent::NewExternalAgentThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
@@ -784,8 +755,8 @@
|
||||
"context": "ThreadSwitcher",
|
||||
"bindings": {
|
||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
|
||||
}
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace && debugger_running",
|
||||
@@ -918,7 +889,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "!ContextEditor && !AcpThread > Editor && mode == full",
|
||||
"context": "!AcpThread > Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
"ctrl-shift-.": "agent::AddSelectionToThread",
|
||||
"ctrl-shift-,": "assistant::InsertIntoEditor",
|
||||
"shift-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange",
|
||||
@@ -182,7 +181,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"context": "Editor && jupyter",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
@@ -221,30 +220,11 @@
|
||||
"shift-alt-z": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-i": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-shift-,": "assistant::InsertIntoEditor",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline",
|
||||
"ctrl-k c": "assistant::CopyCode",
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"shift-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"shift-alt-c": "agent::OpenSettings",
|
||||
"shift-alt-l": "agent::OpenRulesLibrary",
|
||||
@@ -278,14 +258,6 @@
|
||||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && text_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewTextThread",
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
@@ -721,8 +693,8 @@
|
||||
"context": "ThreadSwitcher",
|
||||
"bindings": {
|
||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
|
||||
}
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ApplicationMenu",
|
||||
@@ -858,7 +830,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "!ContextEditor && !AcpThread > Editor && mode == full",
|
||||
"context": "!AcpThread > Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
||||
"ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
||||
"ctrl-k": "assistant::InlineAssist",
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -34,7 +33,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||
"context": "AgentPanel || (MessageEditor > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-i": "workspace::ToggleRightDock",
|
||||
@@ -47,7 +46,6 @@
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
"ctrl-r": "agent::NewThread",
|
||||
"ctrl-shift-v": "editor::Paste",
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor",
|
||||
// "escape": "agent::ToggleFocus"
|
||||
///// Enable when Zed supports multiple thread tabs
|
||||
// "ctrl-t": // new thread tab
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
||||
"cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
||||
"cmd-k": "assistant::InlineAssist",
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -35,7 +34,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||
"context": "AgentPanel || (MessageEditor > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-i": "workspace::ToggleRightDock",
|
||||
@@ -48,7 +47,6 @@
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
"cmd-r": "agent::NewThread",
|
||||
"cmd-shift-v": "editor::Paste",
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor",
|
||||
// "escape": "agent::ToggleFocus"
|
||||
///// Enable when Zed supports multiple thread tabs
|
||||
// "cmd-t": // new thread tab
|
||||
|
||||
@@ -960,8 +960,6 @@
|
||||
"default_width": 640,
|
||||
// Default height when the agent panel is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// The view to use by default (thread, or text_thread)
|
||||
"default_view": "thread",
|
||||
// The default model to use when creating new threads.
|
||||
"default_model": {
|
||||
// The provider to use.
|
||||
@@ -1614,9 +1612,6 @@
|
||||
"prompt_format": "infer",
|
||||
"max_output_tokens": 64,
|
||||
},
|
||||
// Whether edit predictions are enabled when editing text threads in the agent panel.
|
||||
// This setting has no effect if globally disabled.
|
||||
"enabled_in_text_threads": true,
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
|
||||
@@ -32,10 +32,6 @@ pub enum MentionUri {
|
||||
id: acp::SessionId,
|
||||
name: String,
|
||||
},
|
||||
TextThread {
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
},
|
||||
Rule {
|
||||
id: PromptId,
|
||||
name: String,
|
||||
@@ -137,12 +133,6 @@ impl MentionUri {
|
||||
id: acp::SessionId::new(thread_id),
|
||||
name,
|
||||
})
|
||||
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
|
||||
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
|
||||
Ok(Self::TextThread {
|
||||
path: path.into(),
|
||||
name,
|
||||
})
|
||||
} else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
|
||||
let name = single_query_param(&url, "name")?.context("Missing rule name")?;
|
||||
let rule_id = UserPromptId(rule_id.parse()?);
|
||||
@@ -240,7 +230,6 @@ impl MentionUri {
|
||||
MentionUri::PastedImage => "Image".to_string(),
|
||||
MentionUri::Symbol { name, .. } => name.clone(),
|
||||
MentionUri::Thread { name, .. } => name.clone(),
|
||||
MentionUri::TextThread { name, .. } => name.clone(),
|
||||
MentionUri::Rule { name, .. } => name.clone(),
|
||||
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
|
||||
MentionUri::TerminalSelection { line_count } => {
|
||||
@@ -312,7 +301,6 @@ impl MentionUri {
|
||||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||
MentionUri::TextThread { .. } => IconName::Thread.path().into(),
|
||||
MentionUri::Rule { .. } => IconName::Reader.path().into(),
|
||||
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
|
||||
MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
|
||||
@@ -381,15 +369,6 @@ impl MentionUri {
|
||||
url.query_pairs_mut().append_pair("name", name);
|
||||
url
|
||||
}
|
||||
MentionUri::TextThread { path, name } => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path(&format!(
|
||||
"/agent/text-thread/{}",
|
||||
path.to_string_lossy().trim_start_matches('/')
|
||||
));
|
||||
url.query_pairs_mut().append_pair("name", name);
|
||||
url
|
||||
}
|
||||
MentionUri::Rule { name, id } => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path(&format!("/agent/rule/{id}"));
|
||||
|
||||
@@ -295,9 +295,6 @@ impl UserMessage {
|
||||
MentionUri::Thread { .. } => {
|
||||
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||
}
|
||||
MentionUri::TextThread { .. } => {
|
||||
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||
}
|
||||
MentionUri::Rule { .. } => {
|
||||
write!(
|
||||
&mut rules_context,
|
||||
|
||||
@@ -563,7 +563,7 @@ mod tests {
|
||||
use crate::tools::{DeletePathTool, EditFileTool, FetchTool, TerminalTool};
|
||||
use agent_settings::{AgentProfileId, CompiledRegex, InvalidRegexPattern, ToolRules};
|
||||
use gpui::px;
|
||||
use settings::{DefaultAgentView, DockPosition, NotifyWhenAgentWaiting};
|
||||
use settings::{DockPosition, NotifyWhenAgentWaiting};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn test_agent_settings(tool_permissions: ToolPermissions) -> AgentSettings {
|
||||
@@ -582,7 +582,6 @@ mod tests {
|
||||
inline_alternatives: vec![],
|
||||
favorite_models: vec![],
|
||||
default_profile: AgentProfileId::default(),
|
||||
default_view: DefaultAgentView::Thread,
|
||||
profiles: Default::default(),
|
||||
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
|
||||
play_sound_when_agent_done: false,
|
||||
|
||||
@@ -12,9 +12,9 @@ use project::DisableAiSettings;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,
|
||||
NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SettingsContent,
|
||||
SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
|
||||
DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NewThreadLocation,
|
||||
NotifyWhenAgentWaiting, RegisterSetting, Settings, SettingsContent, SettingsStore,
|
||||
SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
|
||||
update_settings_file,
|
||||
};
|
||||
|
||||
@@ -162,7 +162,6 @@ pub struct AgentSettings {
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub favorite_models: Vec<LanguageModelSelection>,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub default_view: DefaultAgentView,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
|
||||
|
||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||
@@ -611,7 +610,6 @@ impl Settings for AgentSettings {
|
||||
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
||||
favorite_models: agent.favorite_models,
|
||||
default_profile: AgentProfileId(agent.default_profile.unwrap()),
|
||||
default_view: agent.default_view.unwrap(),
|
||||
profiles: agent
|
||||
.profiles
|
||||
.unwrap()
|
||||
|
||||
@@ -14,7 +14,6 @@ doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"assistant_text_thread/test-support",
|
||||
"acp_thread/test-support",
|
||||
"eval_utils",
|
||||
"gpui/test-support",
|
||||
@@ -36,9 +35,6 @@ agent_settings.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
heapless.workspace = true
|
||||
assistant_text_thread.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
audio = { workspace = true, optional = true }
|
||||
base64.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
@@ -89,7 +85,6 @@ release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
rules_library.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
@@ -119,7 +114,6 @@ reqwest_client = { workspace = true, optional = true }
|
||||
[dev-dependencies]
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
assistant_text_thread = { workspace = true, features = ["test-support"] }
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
|
||||
db = { workspace = true, features = ["test-support"] }
|
||||
|
||||
+233
-1145
File diff suppressed because it is too large.
Load diff
@@ -11,6 +11,7 @@ mod config_options;
|
||||
mod context;
|
||||
mod context_server_configuration;
|
||||
pub(crate) mod conversation_view;
|
||||
mod diagnostics;
|
||||
mod entry_view_state;
|
||||
mod external_source_prompt;
|
||||
mod favorite_models;
|
||||
@@ -23,14 +24,10 @@ mod mode_selector;
|
||||
mod model_selector;
|
||||
mod model_selector_popover;
|
||||
mod profile_selector;
|
||||
mod slash_command;
|
||||
mod slash_command_picker;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test_support;
|
||||
mod text_thread_editor;
|
||||
mod text_thread_history;
|
||||
mod thread_history;
|
||||
mod thread_history_view;
|
||||
mod thread_import;
|
||||
@@ -41,10 +38,9 @@ mod ui;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::ui::IconName;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
@@ -65,9 +61,7 @@ use std::any::TypeId;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||
pub use crate::agent_panel::{
|
||||
AgentPanel, AgentPanelEvent, ConcreteAssistantPanelDelegate, WorktreeCreationStatus,
|
||||
};
|
||||
pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, WorktreeCreationStatus};
|
||||
use crate::agent_registry_ui::AgentRegistryPage;
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
@@ -76,7 +70,6 @@ pub use external_source_prompt::ExternalSourcePrompt;
|
||||
pub(crate) use mode_selector::ModeSelector;
|
||||
pub(crate) use model_selector::ModelSelector;
|
||||
pub(crate) use model_selector_popover::ModelSelectorPopover;
|
||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||
pub(crate) use thread_history::ThreadHistory;
|
||||
pub(crate) use thread_history_view::*;
|
||||
pub use thread_import::{AcpThreadImportOnboarding, ThreadImportModal};
|
||||
@@ -88,8 +81,6 @@ const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfill
|
||||
actions!(
|
||||
agent,
|
||||
[
|
||||
/// Creates a new text-based conversation thread.
|
||||
NewTextThread,
|
||||
/// Toggles the menu to create new agent threads.
|
||||
ToggleNewThreadMenu,
|
||||
/// Cycles through the options for where new threads start (current project or new worktree).
|
||||
@@ -244,11 +235,13 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||
from_session_id: agent_client_protocol::SessionId,
|
||||
}
|
||||
|
||||
// TODO unify this with AgentType
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Agent {
|
||||
#[default]
|
||||
#[serde(alias = "NativeAgent", alias = "TextThread")]
|
||||
NativeAgent,
|
||||
#[serde(alias = "Custom")]
|
||||
Custom {
|
||||
#[serde(rename = "name")]
|
||||
id: AgentId,
|
||||
@@ -273,6 +266,24 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_native(&self) -> bool {
|
||||
matches!(self, Self::NativeAgent)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> SharedString {
|
||||
match self {
|
||||
Self::NativeAgent => "Zed Agent".into(),
|
||||
Self::Custom { id, .. } => id.0.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> Option<IconName> {
|
||||
match self {
|
||||
Self::NativeAgent => None,
|
||||
Self::Custom { .. } => Some(IconName::Sparkle),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server(
|
||||
&self,
|
||||
fs: Arc<dyn fs::Fs>,
|
||||
@@ -350,10 +361,39 @@ impl ModelUsageContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn humanize_token_count(count: u64) -> String {
|
||||
match count {
|
||||
0..=999 => count.to_string(),
|
||||
1000..=9999 => {
|
||||
let thousands = count / 1000;
|
||||
let hundreds = (count % 1000 + 50) / 100;
|
||||
if hundreds == 0 {
|
||||
format!("{}k", thousands)
|
||||
} else if hundreds == 10 {
|
||||
format!("{}k", thousands + 1)
|
||||
} else {
|
||||
format!("{}.{}k", thousands, hundreds)
|
||||
}
|
||||
}
|
||||
10_000..=999_999 => format!("{}k", (count + 500) / 1000),
|
||||
1_000_000..=9_999_999 => {
|
||||
let millions = count / 1_000_000;
|
||||
let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
|
||||
if hundred_thousands == 0 {
|
||||
format!("{}M", millions)
|
||||
} else if hundred_thousands == 10 {
|
||||
format!("{}M", millions + 1)
|
||||
} else {
|
||||
format!("{}.{}M", millions, hundred_thousands)
|
||||
}
|
||||
}
|
||||
10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the `agent` crate.
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
is_new_install: bool,
|
||||
@@ -361,20 +401,16 @@ pub fn init(
|
||||
cx: &mut App,
|
||||
) {
|
||||
agent::ThreadStore::init_global(cx);
|
||||
assistant_text_thread::init(client, cx);
|
||||
rules_library::init(cx);
|
||||
if !is_eval {
|
||||
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
||||
// we're not running inside of the eval.
|
||||
init_language_model_settings(cx);
|
||||
}
|
||||
assistant_slash_command::init(cx);
|
||||
agent_panel::init(cx);
|
||||
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
|
||||
TextThreadEditor::init(cx);
|
||||
thread_metadata_store::init(cx);
|
||||
|
||||
register_slash_commands(cx);
|
||||
inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
|
||||
terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
|
||||
cx.observe_new(move |workspace, window, cx| {
|
||||
@@ -628,34 +664,6 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
});
|
||||
}
|
||||
|
||||
fn register_slash_commands(cx: &mut App) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
|
||||
slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
|
||||
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
|
||||
slash_command_registry
|
||||
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
|
||||
|
||||
cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
|
||||
move |is_enabled, _cx| {
|
||||
if is_enabled {
|
||||
slash_command_registry.register_command(
|
||||
assistant_slash_commands::StreamingExampleSlashCommand,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -666,9 +674,7 @@ mod tests {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{BorrowAppContext, TestAppContext, px};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||
};
|
||||
use settings::{DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
|
||||
@@ -697,7 +703,6 @@ mod tests {
|
||||
inline_alternatives: vec![],
|
||||
favorite_models: vec![],
|
||||
default_profile: AgentProfileId::default(),
|
||||
default_view: DefaultAgentView::Thread,
|
||||
profiles: Default::default(),
|
||||
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
|
||||
play_sound_when_agent_done: false,
|
||||
@@ -731,10 +736,6 @@ mod tests {
|
||||
!filter.is_hidden(&NewThread),
|
||||
"NewThread should be visible by default"
|
||||
);
|
||||
assert!(
|
||||
!filter.is_hidden(&text_thread_editor::CopyCode),
|
||||
"CopyCode should be visible when agent is enabled"
|
||||
);
|
||||
});
|
||||
|
||||
// Disable agent
|
||||
@@ -754,10 +755,6 @@ mod tests {
|
||||
filter.is_hidden(&NewThread),
|
||||
"NewThread should be hidden when agent is disabled"
|
||||
);
|
||||
assert!(
|
||||
filter.is_hidden(&text_thread_editor::CopyCode),
|
||||
"CopyCode should be hidden when agent is disabled"
|
||||
);
|
||||
});
|
||||
|
||||
// Test EditPredictionProvider
|
||||
@@ -903,11 +900,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_deserialize_external_agent_variants() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Agent>(r#""native_agent""#).unwrap(),
|
||||
serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
|
||||
Agent::NativeAgent,
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Agent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
|
||||
serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
|
||||
Agent::Custom {
|
||||
id: "my-agent".into(),
|
||||
},
|
||||
|
||||
@@ -2524,22 +2524,6 @@ impl ConversationView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts terminal text as a crease into the message editor.
|
||||
pub(crate) fn insert_terminal_text(
|
||||
&self,
|
||||
text: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(active_thread) = self.active_thread() {
|
||||
active_thread.update(cx, |thread, cx| {
|
||||
thread.message_editor.update(cx, |editor, cx| {
|
||||
editor.insert_terminal_crease(text, window, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn current_model_name(&self, cx: &App) -> SharedString {
|
||||
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
|
||||
// For ACP agents, use the agent name (e.g., "Claude Agent", "Gemini CLI")
|
||||
@@ -2751,7 +2735,6 @@ pub(crate) mod tests {
|
||||
use action_log::ActionLog;
|
||||
use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
|
||||
use agent_client_protocol::SessionId;
|
||||
use assistant_text_thread::TextThreadStore;
|
||||
use editor::MultiBufferOffset;
|
||||
use fs::FakeFs;
|
||||
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
|
||||
@@ -3309,10 +3292,7 @@ pub(crate) mod tests {
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
||||
|
||||
workspace1.update_in(cx, |workspace, window, cx| {
|
||||
let text_thread_store =
|
||||
cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx));
|
||||
let panel =
|
||||
cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx));
|
||||
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx));
|
||||
workspace.add_panel(panel, window, cx);
|
||||
|
||||
// Open the dock and activate the agent panel so it's visible
|
||||
|
||||
@@ -3419,12 +3419,10 @@ impl ThreadView {
|
||||
}
|
||||
};
|
||||
|
||||
let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
|
||||
let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
|
||||
let input_tokens_label =
|
||||
crate::text_thread_editor::humanize_token_count(usage.input_tokens);
|
||||
let output_tokens_label =
|
||||
crate::text_thread_editor::humanize_token_count(usage.output_tokens);
|
||||
let used = crate::humanize_token_count(usage.used_tokens);
|
||||
let max = crate::humanize_token_count(usage.max_tokens);
|
||||
let input_tokens_label = crate::humanize_token_count(usage.input_tokens);
|
||||
let output_tokens_label = crate::humanize_token_count(usage.output_tokens);
|
||||
|
||||
let progress_ratio = if usage.max_tokens > 0 {
|
||||
usage.used_tokens as f32 / usage.max_tokens as f32
|
||||
@@ -3468,10 +3466,9 @@ impl ThreadView {
|
||||
.and_then(|thread| thread.read(cx).model())
|
||||
.and_then(|model| model.max_output_tokens())
|
||||
.unwrap_or(0);
|
||||
let input_max_label = crate::text_thread_editor::humanize_token_count(
|
||||
usage.max_tokens.saturating_sub(max_output_tokens),
|
||||
);
|
||||
let output_max_label = crate::text_thread_editor::humanize_token_count(max_output_tokens);
|
||||
let input_max_label =
|
||||
crate::humanize_token_count(usage.max_tokens.saturating_sub(max_output_tokens));
|
||||
let output_max_label = crate::humanize_token_count(max_output_tokens);
|
||||
|
||||
let build_tooltip = {
|
||||
move |_window: &mut Window, cx: &mut App| {
|
||||
@@ -4808,12 +4805,9 @@ impl ThreadView {
|
||||
.last_turn_tokens
|
||||
.filter(|&tokens| tokens > TOKEN_THRESHOLD)
|
||||
.map(|tokens| {
|
||||
Label::new(format!(
|
||||
"{} tokens",
|
||||
crate::text_thread_editor::humanize_token_count(tokens)
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
Label::new(format!("{} tokens", crate::humanize_token_count(tokens)))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
@@ -5096,7 +5090,7 @@ impl ThreadView {
|
||||
self.turn_fields
|
||||
.turn_tokens
|
||||
.filter(|&tokens| tokens > TOKEN_THRESHOLD)
|
||||
.map(|tokens| crate::text_thread_editor::humanize_token_count(tokens))
|
||||
.map(|tokens| crate::humanize_token_count(tokens))
|
||||
})
|
||||
.flatten();
|
||||
|
||||
@@ -8775,15 +8769,6 @@ pub(crate) fn open_link(
|
||||
});
|
||||
}
|
||||
}
|
||||
MentionUri::TextThread { path, .. } => {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_saved_text_thread(path.as_path().into(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
MentionUri::Rule { id, .. } => {
|
||||
let PromptId::User { uuid } = id else {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
use anyhow::Result;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use language::{Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, ToOffset};
|
||||
use project::{DiagnosticSummary, Project};
|
||||
use rope::Point;
|
||||
use std::{fmt::Write, ops::RangeInclusive, path::Path};
|
||||
use text::OffsetRangeExt;
|
||||
use util::ResultExt;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
pub fn codeblock_fence_for_path(
|
||||
path: Option<&str>,
|
||||
row_range: Option<RangeInclusive<u32>>,
|
||||
) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{path}").unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
|
||||
pub struct DiagnosticsOptions {
|
||||
pub include_errors: bool,
|
||||
pub include_warnings: bool,
|
||||
pub path_matcher: Option<PathMatcher>,
|
||||
}
|
||||
|
||||
/// Collects project diagnostics into a formatted string.
|
||||
///
|
||||
/// Returns `None` if no matching diagnostics were found.
|
||||
pub fn collect_diagnostics(
|
||||
project: Entity<Project>,
|
||||
options: DiagnosticsOptions,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<String>>> {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let glob_is_exact_file_match = if let Some(path) = options
|
||||
.path_matcher
|
||||
.as_ref()
|
||||
.and_then(|pm| pm.sources().next())
|
||||
{
|
||||
project
|
||||
.read(cx)
|
||||
.find_project_path(Path::new(path), cx)
|
||||
.is_some()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let project_handle = project.downgrade();
|
||||
let diagnostic_summaries: Vec<_> = project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(false, cx)
|
||||
.flat_map(|(path, _, summary)| {
|
||||
let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
|
||||
let full_path = worktree.read(cx).root_name().join(&path.path);
|
||||
Some((path, full_path, summary))
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let error_source = if let Some(path_matcher) = &options.path_matcher {
|
||||
debug_assert_eq!(path_matcher.sources().count(), 1);
|
||||
Some(path_matcher.sources().next().unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut text = String::new();
|
||||
if let Some(error_source) = error_source.as_ref() {
|
||||
writeln!(text, "diagnostics: {}", error_source).unwrap();
|
||||
} else {
|
||||
writeln!(text, "diagnostics").unwrap();
|
||||
}
|
||||
|
||||
let mut found_any_diagnostics = false;
|
||||
let mut project_summary = DiagnosticSummary::default();
|
||||
for (project_path, path, summary) in diagnostic_summaries {
|
||||
if let Some(path_matcher) = &options.path_matcher
|
||||
&& !path_matcher.is_match(&path)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let has_errors = options.include_errors && summary.error_count > 0;
|
||||
let has_warnings = options.include_warnings && summary.warning_count > 0;
|
||||
if !has_errors && !has_warnings {
|
||||
continue;
|
||||
}
|
||||
|
||||
if options.include_errors {
|
||||
project_summary.error_count += summary.error_count;
|
||||
}
|
||||
if options.include_warnings {
|
||||
project_summary.warning_count += summary.warning_count;
|
||||
}
|
||||
|
||||
let file_path = path.display(path_style).to_string();
|
||||
if !glob_is_exact_file_match {
|
||||
writeln!(&mut text, "{file_path}").unwrap();
|
||||
}
|
||||
|
||||
if let Some(buffer) = project_handle
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());
|
||||
if collect_buffer_diagnostics(
|
||||
&mut text,
|
||||
&snapshot,
|
||||
options.include_warnings,
|
||||
options.include_errors,
|
||||
) {
|
||||
found_any_diagnostics = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_any_diagnostics {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut label = String::new();
|
||||
label.push_str("Diagnostics");
|
||||
if let Some(source) = error_source {
|
||||
write!(label, " ({})", source).unwrap();
|
||||
}
|
||||
|
||||
if project_summary.error_count > 0 || project_summary.warning_count > 0 {
|
||||
label.push(':');
|
||||
|
||||
if project_summary.error_count > 0 {
|
||||
write!(label, " {} errors", project_summary.error_count).unwrap();
|
||||
if project_summary.warning_count > 0 {
|
||||
label.push(',');
|
||||
}
|
||||
}
|
||||
|
||||
if project_summary.warning_count > 0 {
|
||||
write!(label, " {} warnings", project_summary.warning_count).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend the summary label to the output.
|
||||
text.insert_str(0, &format!("{label}\n"));
|
||||
|
||||
Ok(Some(text))
|
||||
})
|
||||
}
|
||||
|
||||
/// Collects diagnostics from a buffer snapshot into the text output.
|
||||
///
|
||||
/// Returns `true` if any diagnostics were written.
|
||||
fn collect_buffer_diagnostics(
|
||||
text: &mut String,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
include_errors: bool,
|
||||
) -> bool {
|
||||
let mut found_any = false;
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
if collect_diagnostic(text, entry, snapshot, include_warnings, include_errors) {
|
||||
found_any = true;
|
||||
}
|
||||
}
|
||||
found_any
|
||||
}
|
||||
|
||||
/// Formats a single diagnostic entry as a code excerpt with the diagnostic message.
|
||||
///
|
||||
/// Returns `true` if the diagnostic was written (i.e. it matched severity filters).
|
||||
fn collect_diagnostic(
|
||||
text: &mut String,
|
||||
entry: &DiagnosticEntryRef<'_, Anchor>,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
include_errors: bool,
|
||||
) -> bool {
|
||||
const EXCERPT_EXPANSION_SIZE: u32 = 2;
|
||||
const MAX_MESSAGE_LENGTH: usize = 2000;
|
||||
|
||||
let ty = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::WARNING => {
|
||||
if !include_warnings {
|
||||
return false;
|
||||
}
|
||||
"warning"
|
||||
}
|
||||
DiagnosticSeverity::ERROR => {
|
||||
if !include_errors {
|
||||
return false;
|
||||
}
|
||||
"error"
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let range = entry.range.to_point(snapshot);
|
||||
let diagnostic_row_number = range.start.row + 1;
|
||||
|
||||
let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
|
||||
let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
|
||||
let excerpt_range =
|
||||
Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot);
|
||||
|
||||
text.push_str("```");
|
||||
if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
|
||||
text.push_str(&language_name);
|
||||
}
|
||||
text.push('\n');
|
||||
|
||||
let mut buffer_text = String::new();
|
||||
for chunk in snapshot.text_for_range(excerpt_range) {
|
||||
buffer_text.push_str(chunk);
|
||||
}
|
||||
|
||||
for (i, line) in buffer_text.lines().enumerate() {
|
||||
let line_number = start_row + i as u32 + 1;
|
||||
writeln!(text, "{}", line).unwrap();
|
||||
|
||||
if line_number == diagnostic_row_number {
|
||||
text.push_str("//");
|
||||
let marker_start = text.len();
|
||||
write!(text, " {}: ", ty).unwrap();
|
||||
let padding = text.len() - marker_start;
|
||||
|
||||
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
|
||||
.replace('\n', format!("\n//{:padding$}", "").as_str());
|
||||
|
||||
writeln!(text, "{message}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(text, "```").unwrap();
|
||||
true
|
||||
}
|
||||
@@ -257,12 +257,8 @@ impl InlineAssistant {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(inline_assist_target) = Self::resolve_inline_assist_target(
|
||||
workspace,
|
||||
workspace.panel::<AgentPanel>(cx),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
let Some(inline_assist_target) = Self::resolve_inline_assist_target(workspace, window, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1570,7 +1566,6 @@ impl InlineAssistant {
|
||||
|
||||
fn resolve_inline_assist_target(
|
||||
workspace: &mut Workspace,
|
||||
agent_panel: Option<Entity<AgentPanel>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<InlineAssistTarget> {
|
||||
@@ -1588,20 +1583,7 @@ impl InlineAssistant {
|
||||
return Some(InlineAssistTarget::Terminal(terminal_view));
|
||||
}
|
||||
|
||||
let text_thread_editor = agent_panel
|
||||
.and_then(|panel| panel.read(cx).active_text_thread_editor())
|
||||
.and_then(|editor| {
|
||||
let editor = &editor.read(cx).editor().clone();
|
||||
if editor.read(cx).is_focused(window) {
|
||||
Some(editor.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(text_thread_editor) = text_thread_editor {
|
||||
Some(InlineAssistTarget::Editor(text_thread_editor))
|
||||
} else if let Some(workspace_editor) = workspace
|
||||
if let Some(workspace_editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::diagnostics::{DiagnosticsOptions, codeblock_fence_for_path, collect_diagnostics};
|
||||
use acp_thread::{MentionUri, selection_name};
|
||||
use agent::{ThreadStore, outline};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_commands::{codeblock_fence_for_path, collect_diagnostics_output};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
|
||||
@@ -131,9 +131,6 @@ impl MentionSet {
|
||||
MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, http_client, cx),
|
||||
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
|
||||
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
|
||||
MentionUri::TextThread { .. } => {
|
||||
Task::ready(Err(anyhow!("Text thread mentions are no longer supported")))
|
||||
}
|
||||
MentionUri::File { abs_path } => {
|
||||
self.confirm_mention_for_file(abs_path, supports_images, cx)
|
||||
}
|
||||
@@ -276,9 +273,6 @@ impl MentionSet {
|
||||
}
|
||||
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
|
||||
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
|
||||
MentionUri::TextThread { .. } => {
|
||||
Task::ready(Err(anyhow!("Text thread mentions are no longer supported")))
|
||||
}
|
||||
MentionUri::File { abs_path } => {
|
||||
self.confirm_mention_for_file(abs_path, supports_images, cx)
|
||||
}
|
||||
@@ -589,9 +583,9 @@ impl MentionSet {
|
||||
return Task::ready(Err(anyhow!("project not found")));
|
||||
};
|
||||
|
||||
let diagnostics_task = collect_diagnostics_output(
|
||||
let diagnostics_task = collect_diagnostics(
|
||||
project,
|
||||
assistant_slash_commands::Options {
|
||||
DiagnosticsOptions {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
path_matcher: None,
|
||||
@@ -599,9 +593,8 @@ impl MentionSet {
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |_, _| {
|
||||
let output = diagnostics_task.await?;
|
||||
let content = output
|
||||
.map(|output| output.text)
|
||||
let content = diagnostics_task
|
||||
.await?
|
||||
.unwrap_or_else(|| "No diagnostics found.".into());
|
||||
Ok(Mention::Text {
|
||||
content,
|
||||
|
||||
@@ -1308,62 +1308,6 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_terminal_crease(
|
||||
&mut self,
|
||||
text: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let line_count = text.lines().count() as u32;
|
||||
let mention_uri = MentionUri::TerminalSelection { line_count };
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
|
||||
let (excerpt_id, text_anchor, content_len) = self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
|
||||
let text_anchor = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor
|
||||
.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
mention_uri.name().into(),
|
||||
mention_uri.icon_path(cx),
|
||||
mention_uri.tooltip_text(),
|
||||
Some(mention_uri.clone()),
|
||||
Some(self.workspace.clone()),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = Task::ready(Ok(Mention::Text {
|
||||
content: text,
|
||||
tracked_buffers: vec![],
|
||||
}))
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _| {
|
||||
mention_set.insert_mention(crease_id, mention_uri, mention_task);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_branch_diff_crease(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
use crate::text_thread_editor::TextThreadEditor;
|
||||
use anyhow::Result;
|
||||
pub use assistant_slash_command::SlashCommand;
|
||||
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::{
|
||||
CompletionDisplayOptions, CompletionIntent, CompletionSource,
|
||||
lsp_store::CompletionDocumentation,
|
||||
};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct SlashCommandCompletionProvider {
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakEntity<TextThreadEditor>>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
}
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakEntity<TextThreadEditor>>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
slash_commands,
|
||||
editor,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_command_name(
|
||||
&self,
|
||||
command_name: &str,
|
||||
command_range: Range<Anchor>,
|
||||
name_range: Range<Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<project::CompletionResponse>>> {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let candidates = slash_commands
|
||||
.command_names(cx)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, def)| StringMatchCandidate::new(ix, &def))
|
||||
.collect::<Vec<_>>();
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
window.spawn(cx, async move |cx| {
|
||||
let matches = match_strings(
|
||||
&candidates,
|
||||
&command_name,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|_, cx| {
|
||||
let completions = matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = slash_commands.command(&mat.string, cx)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
let requires_argument = command.requires_argument();
|
||||
let accepts_arguments = command.accepts_arguments();
|
||||
if requires_argument || accepts_arguments {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
Arc::new(
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
if !requires_argument
|
||||
&& (!accepts_arguments || intent.is_complete())
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&[],
|
||||
true,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
requires_argument || accepts_arguments
|
||||
}
|
||||
},
|
||||
) as Arc<_>
|
||||
});
|
||||
|
||||
Some(project::Completion {
|
||||
replace_range: name_range.clone(),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
command.description().into(),
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
icon_path: None,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
vec![project::CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn complete_command_argument(
|
||||
&self,
|
||||
command_name: &str,
|
||||
arguments: &[String],
|
||||
command_range: Range<Anchor>,
|
||||
argument_range: Range<Anchor>,
|
||||
last_argument_range: Range<Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<project::CompletionResponse>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
flag.store(true, SeqCst);
|
||||
*flag = new_cancel_flag.clone();
|
||||
if let Some(command) = self.slash_commands.command(command_name, cx) {
|
||||
let completions = command.complete_argument(
|
||||
arguments,
|
||||
new_cancel_flag,
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let command_name: Arc<str> = command_name.into();
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let arguments = arguments.to_vec();
|
||||
cx.background_spawn(async move {
|
||||
let completions = completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|new_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let mut completed_arguments = arguments.clone();
|
||||
if new_argument.replace_previous_arguments {
|
||||
completed_arguments.clear();
|
||||
} else {
|
||||
completed_arguments.pop();
|
||||
}
|
||||
completed_arguments.push(new_argument.new_text.clone());
|
||||
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
if new_argument.after_completion.run()
|
||||
|| intent.is_complete()
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&completed_arguments,
|
||||
true,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
!new_argument.after_completion.run()
|
||||
}
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
|
||||
let mut new_text = new_argument.new_text.clone();
|
||||
if new_argument.after_completion == AfterCompletion::Continue {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
project::Completion {
|
||||
replace_range: if new_argument.replace_previous_arguments {
|
||||
argument_range.clone()
|
||||
} else {
|
||||
last_argument_range.clone()
|
||||
},
|
||||
label: new_argument.label,
|
||||
icon_path: None,
|
||||
new_text,
|
||||
documentation: None,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
confirm,
|
||||
insert_text_mode: None,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(vec![project::CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
// TODO: Could have slash commands indicate whether their completions are incomplete.
|
||||
is_incomplete: true,
|
||||
}])
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: true,
|
||||
}]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
_excerpt_id: ExcerptId,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<project::CompletionResponse>>> {
|
||||
let Some((name, arguments, command_range, last_argument_range)) =
|
||||
buffer.update(cx, |buffer, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
let line = lines.next()?;
|
||||
let call = SlashCommandLine::parse(line)?;
|
||||
|
||||
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
|
||||
let command_range_end = Point::new(
|
||||
position.row,
|
||||
call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
|
||||
);
|
||||
let command_range = buffer.anchor_before(command_range_start)
|
||||
..buffer.anchor_after(command_range_end);
|
||||
|
||||
let name = line[call.name.clone()].to_string();
|
||||
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
|
||||
{
|
||||
let last_arg_start =
|
||||
buffer.anchor_before(Point::new(position.row, argument.start as u32));
|
||||
let first_arg_start = call.arguments.first().expect("we have the last element");
|
||||
let first_arg_start = buffer
|
||||
.anchor_before(Point::new(position.row, first_arg_start.start as u32));
|
||||
let arguments = call
|
||||
.arguments
|
||||
.into_iter()
|
||||
.filter_map(|argument| Some(line.get(argument)?.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let argument_range = first_arg_start..buffer_position;
|
||||
(
|
||||
Some((arguments, argument_range)),
|
||||
last_arg_start..buffer_position,
|
||||
)
|
||||
} else {
|
||||
let start =
|
||||
buffer.anchor_before(Point::new(position.row, call.name.start as u32));
|
||||
(None, start..buffer_position)
|
||||
};
|
||||
|
||||
Some((name, arguments, command_range, last_argument_range))
|
||||
})
|
||||
else {
|
||||
return Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]));
|
||||
};
|
||||
|
||||
if let Some((arguments, argument_range)) = arguments {
|
||||
self.complete_command_argument(
|
||||
&name,
|
||||
&arguments,
|
||||
command_range,
|
||||
argument_range,
|
||||
last_argument_range,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
self.complete_command_name(&name, command_range, last_argument_range, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
if let Some(line) = lines.next() {
|
||||
SlashCommandLine::parse(line).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_completions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
use crate::text_thread_editor::TextThreadEditor;
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use std::sync::Arc;
|
||||
use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct SlashCommandSelector<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakEntity<TextThreadEditor>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SlashCommandInfo {
|
||||
name: SharedString,
|
||||
description: SharedString,
|
||||
args: Option<SharedString>,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SlashCommandEntry {
|
||||
Info(SlashCommandInfo),
|
||||
Advert {
|
||||
name: SharedString,
|
||||
renderer: fn(&mut Window, &mut App) -> AnyElement,
|
||||
on_confirm: fn(&mut Window, &mut App),
|
||||
},
|
||||
}
|
||||
|
||||
impl AsRef<str> for SlashCommandEntry {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
|
||||
| SlashCommandEntry::Advert { name, .. } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandDelegate {
|
||||
all_commands: Vec<SlashCommandEntry>,
|
||||
filtered_commands: Vec<SlashCommandEntry>,
|
||||
active_context_editor: WeakEntity<TextThreadEditor>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl<T, TT> SlashCommandSelector<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakEntity<TextThreadEditor>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
) -> Self {
|
||||
SlashCommandSelector {
|
||||
working_set,
|
||||
active_context_editor,
|
||||
trigger,
|
||||
tooltip,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for SlashCommandDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_commands.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Select a command...".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_commands = self.all_commands.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_commands = cx
|
||||
.background_spawn(async move {
|
||||
if query.is_empty() {
|
||||
all_commands
|
||||
} else {
|
||||
all_commands
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
.as_ref()
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_commands = filtered_commands;
|
||||
this.delegate.set_selected_index(0, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn separators_after_indices(&self) -> Vec<usize> {
|
||||
let mut ret = vec![];
|
||||
let mut previous_is_advert = false;
|
||||
|
||||
for (index, command) in self.filtered_commands.iter().enumerate() {
|
||||
if previous_is_advert {
|
||||
if let SlashCommandEntry::Info(_) = command {
|
||||
previous_is_advert = false;
|
||||
debug_assert_ne!(
|
||||
index, 0,
|
||||
"index cannot be zero, as we can never have a separator at 0th position"
|
||||
);
|
||||
ret.push(index - 1);
|
||||
}
|
||||
} else if let SlashCommandEntry::Advert { .. } = command {
|
||||
previous_is_advert = true;
|
||||
if index != 0 {
|
||||
ret.push(index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||
match command {
|
||||
SlashCommandEntry::Info(info) => {
|
||||
self.active_context_editor
|
||||
.update(cx, |text_thread_editor, cx| {
|
||||
text_thread_editor.insert_command(&info.name, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
SlashCommandEntry::Advert { on_confirm, .. } => {
|
||||
on_confirm(window, cx);
|
||||
}
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let command_info = self.filtered_commands.get(ix)?;
|
||||
|
||||
match command_info {
|
||||
SlashCommandEntry::Info(info) => Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.tooltip({
|
||||
let description = info.description.clone();
|
||||
move |_, cx| cx.new(|_| Tooltip::new(description.clone())).into()
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.group(format!("command-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.py_0p5()
|
||||
.min_w(px(250.))
|
||||
.max_w(px(400.))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(info.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child({
|
||||
let mut label = format!("{}", info.name);
|
||||
if let Some(args) = info.args.as_ref().filter(|_| selected)
|
||||
{
|
||||
label.push_str(args);
|
||||
}
|
||||
Label::new(label)
|
||||
.single_line()
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
})
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
div()
|
||||
.child(
|
||||
Label::new(args)
|
||||
.single_line()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
))
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SlashCommandEntry::Advert { renderer, .. } => Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.child(renderer(window, cx)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TT> RenderOnce for SlashCommandSelector<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let all_models = self
|
||||
.working_set
|
||||
.featured_command_names(cx)
|
||||
.into_iter()
|
||||
.filter_map(|command_name| {
|
||||
let command = self.working_set.command(&command_name, cx)?;
|
||||
let menu_text = SharedString::from(Arc::from(command.menu_text()));
|
||||
let label = command.label(cx);
|
||||
let args = label.filter_range.end.ne(&label.text.len()).then(|| {
|
||||
SharedString::from(
|
||||
label.text[label.filter_range.end..label.text.len()].to_owned(),
|
||||
)
|
||||
});
|
||||
Some(SlashCommandEntry::Info(SlashCommandInfo {
|
||||
name: command_name.into(),
|
||||
description: menu_text,
|
||||
args,
|
||||
icon: command.icon(),
|
||||
}))
|
||||
})
|
||||
.chain([SlashCommandEntry::Advert {
|
||||
name: "create-your-command".into(),
|
||||
renderer: |_, cx| {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.font_buffer(cx)
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("create-your-command")
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Create your custom command")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
on_confirm: |_, cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
}])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let delegate = SlashCommandDelegate {
|
||||
all_commands: all_models.clone(),
|
||||
active_context_editor: self.active_context_editor.clone(),
|
||||
filtered_commands: all_models,
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker_view = cx.new(|cx| {
|
||||
Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
let handle = self
|
||||
.active_context_editor
|
||||
.read_with(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_window, _cx| Some(picker_view.clone()))
|
||||
.trigger_with_tooltip(self.trigger, self.tooltip)
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
.when_some(handle, |this, handle| this.with_handle(handle))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,736 +0,0 @@
|
||||
use crate::{RemoveHistory, RemoveSelectedThread};
|
||||
use assistant_text_thread::{SavedTextThreadMetadata, TextThreadStore};
|
||||
use chrono::{Datelike, Local, NaiveDate, TimeDelta, Utc};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, Task, UniformListScrollHandle, Window,
|
||||
uniform_list,
|
||||
};
|
||||
use std::{fmt::Display, ops::Range};
|
||||
use text::Bias;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
|
||||
|
||||
fn thread_title(entry: &SavedTextThreadMetadata) -> &SharedString {
|
||||
if entry.title.is_empty() {
|
||||
DEFAULT_TITLE
|
||||
} else {
|
||||
&entry.title
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextThreadHistory {
|
||||
pub(crate) text_thread_store: Entity<TextThreadStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
search_editor: Entity<Editor>,
|
||||
search_query: SharedString,
|
||||
visible_items: Vec<ListItemType>,
|
||||
local_timezone: UtcOffset,
|
||||
confirming_delete_history: bool,
|
||||
_update_task: Task<()>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
enum ListItemType {
|
||||
BucketSeparator(TimeBucket),
|
||||
Entry {
|
||||
entry: SavedTextThreadMetadata,
|
||||
format: EntryTimeFormat,
|
||||
},
|
||||
SearchResult {
|
||||
entry: SavedTextThreadMetadata,
|
||||
positions: Vec<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ListItemType {
|
||||
fn history_entry(&self) -> Option<&SavedTextThreadMetadata> {
|
||||
match self {
|
||||
ListItemType::Entry { entry, .. } => Some(entry),
|
||||
ListItemType::SearchResult { entry, .. } => Some(entry),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TextThreadHistoryEvent {
|
||||
Open(SavedTextThreadMetadata),
|
||||
}
|
||||
|
||||
impl EventEmitter<TextThreadHistoryEvent> for TextThreadHistory {}
|
||||
|
||||
impl TextThreadHistory {
|
||||
pub(crate) fn new(
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let search_editor_subscription =
|
||||
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||
if let EditorEvent::BufferEdited = event {
|
||||
let query = search_editor.read(cx).text(cx);
|
||||
if this.search_query != query {
|
||||
this.search_query = query.into();
|
||||
this.update_visible_items(false, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let store_subscription = cx.observe(&text_thread_store, |this, _, cx| {
|
||||
this.update_visible_items(true, cx);
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
|
||||
let mut this = Self {
|
||||
text_thread_store,
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
visible_items: Default::default(),
|
||||
search_editor,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
search_query: SharedString::default(),
|
||||
confirming_delete_history: false,
|
||||
_subscriptions: vec![search_editor_subscription, store_subscription],
|
||||
_update_task: Task::ready(()),
|
||||
};
|
||||
this.update_visible_items(false, cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.visible_items.is_empty()
|
||||
}
|
||||
|
||||
fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
|
||||
let entries = self.text_thread_store.update(cx, |store, _| {
|
||||
store.ordered_text_threads().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let new_list_items = if self.search_query.is_empty() {
|
||||
self.add_list_separators(entries, cx)
|
||||
} else {
|
||||
self.filter_search_results(entries, cx)
|
||||
};
|
||||
let selected_history_entry = if preserve_selected_item {
|
||||
self.selected_history_entry().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self._update_task = cx.spawn(async move |this, cx| {
|
||||
let new_visible_items = new_list_items.await;
|
||||
this.update(cx, |this, cx| {
|
||||
let new_selected_index = if let Some(history_entry) = selected_history_entry {
|
||||
new_visible_items
|
||||
.iter()
|
||||
.position(|visible_entry| {
|
||||
visible_entry
|
||||
.history_entry()
|
||||
.is_some_and(|entry| entry.path == history_entry.path)
|
||||
})
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
this.visible_items = new_visible_items;
|
||||
this.set_selected_index(new_selected_index, Bias::Right, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn add_list_separators(
|
||||
&self,
|
||||
entries: Vec<SavedTextThreadMetadata>,
|
||||
cx: &App,
|
||||
) -> Task<Vec<ListItemType>> {
|
||||
cx.background_spawn(async move {
|
||||
let mut items = Vec::with_capacity(entries.len() + 1);
|
||||
let mut bucket = None;
|
||||
let today = Local::now().naive_local().date();
|
||||
|
||||
for entry in entries.into_iter() {
|
||||
let entry_date = entry.mtime.naive_local().date();
|
||||
let entry_bucket = TimeBucket::from_dates(today, entry_date);
|
||||
|
||||
if Some(entry_bucket) != bucket {
|
||||
bucket = Some(entry_bucket);
|
||||
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||
}
|
||||
|
||||
items.push(ListItemType::Entry {
|
||||
entry,
|
||||
format: entry_bucket.into(),
|
||||
});
|
||||
}
|
||||
items
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_search_results(
|
||||
&self,
|
||||
entries: Vec<SavedTextThreadMetadata>,
|
||||
cx: &App,
|
||||
) -> Task<Vec<ListItemType>> {
|
||||
let query = self.search_query.clone();
|
||||
cx.background_spawn({
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
let mut candidates = Vec::with_capacity(entries.len());
|
||||
|
||||
for (idx, entry) in entries.iter().enumerate() {
|
||||
candidates.push(StringMatchCandidate::new(idx, thread_title(entry)));
|
||||
}
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|search_match| ListItemType::SearchResult {
|
||||
entry: entries[search_match.candidate_id].clone(),
|
||||
positions: search_match.positions,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn search_produced_no_matches(&self) -> bool {
|
||||
self.visible_items.is_empty() && !self.search_query.is_empty()
|
||||
}
|
||||
|
||||
fn selected_history_entry(&self) -> Option<&SavedTextThreadMetadata> {
|
||||
self.get_history_entry(self.selected_index)
|
||||
}
|
||||
|
||||
fn get_history_entry(&self, visible_items_ix: usize) -> Option<&SavedTextThreadMetadata> {
|
||||
self.visible_items.get(visible_items_ix)?.history_entry()
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
|
||||
if self.visible_items.is_empty() {
|
||||
self.selected_index = 0;
|
||||
return;
|
||||
}
|
||||
while matches!(
|
||||
self.visible_items.get(index),
|
||||
None | Some(ListItemType::BucketSeparator(..))
|
||||
) {
|
||||
index = match bias {
|
||||
Bias::Left => {
|
||||
if index == 0 {
|
||||
self.visible_items.len() - 1
|
||||
} else {
|
||||
index - 1
|
||||
}
|
||||
}
|
||||
Bias::Right => {
|
||||
if index == self.visible_items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
index + 1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
self.selected_index = index;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_index == self.visible_items.len() - 1 {
|
||||
self.set_selected_index(0, Bias::Right, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_index == 0 {
|
||||
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.set_selected_index(0, Bias::Right, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirm_entry(self.selected_index, cx);
|
||||
}
|
||||
|
||||
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.get_history_entry(ix) else {
|
||||
return;
|
||||
};
|
||||
cx.emit(TextThreadHistoryEvent::Open(entry.clone()));
|
||||
}
|
||||
|
||||
fn remove_selected_thread(
|
||||
&mut self,
|
||||
_: &RemoveSelectedThread,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.remove_thread(self.selected_index, cx)
|
||||
}
|
||||
|
||||
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.get_history_entry(visible_item_ix) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let task = self
|
||||
.text_thread_store
|
||||
.update(cx, |store, cx| store.delete_local(entry.path.clone(), cx));
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.text_thread_store.update(cx, |store, cx| {
|
||||
store.delete_all_local(cx).detach_and_log_err(cx)
|
||||
});
|
||||
self.confirming_delete_history = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirming_delete_history = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirming_delete_history = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_list_items(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
self.visible_items
|
||||
.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
match item {
|
||||
ListItemType::Entry { entry, format } => self
|
||||
.render_history_entry(entry, *format, ix, Vec::default(), cx)
|
||||
.into_any(),
|
||||
ListItemType::SearchResult { entry, positions } => self.render_history_entry(
|
||||
entry,
|
||||
EntryTimeFormat::DateAndTime,
|
||||
ix,
|
||||
positions.clone(),
|
||||
cx,
|
||||
),
|
||||
ListItemType::BucketSeparator(bucket) => div()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.child(
|
||||
Label::new(bucket.to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_history_entry(
|
||||
&self,
|
||||
entry: &SavedTextThreadMetadata,
|
||||
format: EntryTimeFormat,
|
||||
ix: usize,
|
||||
highlight_positions: Vec<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let entry_time = entry.mtime.with_timezone(&Utc);
|
||||
let timestamp = entry_time.timestamp();
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = thread_title(entry).clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pb_1()
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.rounded()
|
||||
.toggle_state(selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
HighlightedLabel::new(thread_title(entry), highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(display_text)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
} else if this.hovered_index == Some(ix) {
|
||||
this.hovered_index = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
.end_slot::<IconButton>(if hovered {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.remove_thread(ix, cx);
|
||||
cx.stop_propagation()
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.confirm_entry(ix, cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextThreadHistory {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_no_history = !self.text_thread_store.read(cx).has_saved_text_threads();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.key_context("ThreadHistory")
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(|this, _: &RemoveSelectedThread, window, cx| {
|
||||
this.remove_selected_thread(&RemoveSelectedThread, window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
|
||||
this.remove_history(window, cx);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.h(Tab::container_height(cx))
|
||||
.w_full()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::MagnifyingGlass)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(self.search_editor.clone()),
|
||||
)
|
||||
.child({
|
||||
let view = v_flex()
|
||||
.id("list-container")
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.flex_grow();
|
||||
|
||||
if has_no_history {
|
||||
view.justify_center().items_center().child(
|
||||
Label::new("You don't have any past text threads yet.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else if self.search_produced_no_matches() {
|
||||
view.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new("No threads match your search.").size(LabelSize::Small))
|
||||
} else {
|
||||
view.child(
|
||||
uniform_list(
|
||||
"text-thread-history",
|
||||
self.visible_items.len(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.render_list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.pr_4()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.flex_grow(),
|
||||
)
|
||||
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
|
||||
}
|
||||
})
|
||||
.when(!has_no_history, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.when(!self.confirming_delete_history, |this| {
|
||||
this.child(
|
||||
Button::new("delete_history", "Delete All History")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.prompt_delete_history(window, cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(self.confirming_delete_history, |this| {
|
||||
this.w_full()
|
||||
.gap_2()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Delete all text threads?")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new("You won't be able to recover them later.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel_delete", "Cancel")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.cancel_delete_history(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("confirm_delete", "Delete")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.color(Color::Error)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(RemoveHistory),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for TextThreadHistory {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.search_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EntryTimeFormat {
|
||||
DateAndTime,
|
||||
TimeOnly,
|
||||
}
|
||||
|
||||
impl EntryTimeFormat {
|
||||
fn format_timestamp(self, timestamp: i64, timezone: UtcOffset) -> String {
|
||||
let datetime = OffsetDateTime::from_unix_timestamp(timestamp)
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc())
|
||||
.to_offset(timezone);
|
||||
|
||||
match self {
|
||||
EntryTimeFormat::DateAndTime => datetime.format(&time::macros::format_description!(
|
||||
"[month repr:short] [day], [year]"
|
||||
)),
|
||||
EntryTimeFormat::TimeOnly => {
|
||||
datetime.format(&time::macros::format_description!("[hour]:[minute]"))
|
||||
}
|
||||
}
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimeBucket> for EntryTimeFormat {
|
||||
fn from(bucket: TimeBucket) -> Self {
|
||||
match bucket {
|
||||
TimeBucket::Today => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::All => EntryTimeFormat::DateAndTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
enum TimeBucket {
|
||||
Today,
|
||||
Yesterday,
|
||||
ThisWeek,
|
||||
PastWeek,
|
||||
All,
|
||||
}
|
||||
|
||||
impl TimeBucket {
|
||||
fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
|
||||
if date == reference {
|
||||
return TimeBucket::Today;
|
||||
}
|
||||
|
||||
if date == reference - TimeDelta::days(1) {
|
||||
return TimeBucket::Yesterday;
|
||||
}
|
||||
|
||||
let week = date.iso_week();
|
||||
|
||||
if reference.iso_week() == week {
|
||||
return TimeBucket::ThisWeek;
|
||||
}
|
||||
|
||||
let last_week = (reference - TimeDelta::days(7)).iso_week();
|
||||
|
||||
if week == last_week {
|
||||
return TimeBucket::PastWeek;
|
||||
}
|
||||
|
||||
TimeBucket::All
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TimeBucket {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TimeBucket::Today => write!(f, "Today"),
|
||||
TimeBucket::Yesterday => write!(f, "Yesterday"),
|
||||
TimeBucket::ThisWeek => write!(f, "This Week"),
|
||||
TimeBucket::PastWeek => write!(f, "Past Week"),
|
||||
TimeBucket::All => write!(f, "All"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_time_bucket_from_dates() {
|
||||
let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
|
||||
|
||||
let date = today;
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::{AgentPanel, AgentType};
|
||||
use crate::{Agent, agent_panel::AgentPanel};
|
||||
|
||||
macro_rules! acp_onboarding_event {
|
||||
($name:expr) => {
|
||||
@@ -38,7 +38,7 @@ impl AcpOnboardingModal {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
AgentType::Custom {
|
||||
Agent::Custom {
|
||||
id: GEMINI_ID.into(),
|
||||
},
|
||||
window,
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::{AgentPanel, AgentType};
|
||||
use crate::{Agent, agent_panel::AgentPanel};
|
||||
|
||||
macro_rules! claude_agent_onboarding_event {
|
||||
($name:expr) => {
|
||||
@@ -38,7 +38,7 @@ impl ClaudeCodeOnboardingModal {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
AgentType::Custom {
|
||||
Agent::Custom {
|
||||
id: CLAUDE_AGENT_ID.into(),
|
||||
},
|
||||
window,
|
||||
|
||||
@@ -178,7 +178,6 @@ fn open_mention_uri(
|
||||
MentionUri::Thread { id, name } => {
|
||||
open_thread(workspace, id, name, window, cx);
|
||||
}
|
||||
MentionUri::TextThread { .. } => {}
|
||||
MentionUri::Rule { id, .. } => {
|
||||
open_rule(workspace, id, window, cx);
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
[package]
|
||||
name = "assistant_slash_command"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_slash_command.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
extension.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,617 +0,0 @@
|
||||
mod extension_slash_command;
|
||||
mod slash_command_registry;
|
||||
mod slash_command_working_set;
|
||||
|
||||
pub use crate::extension_slash_command::*;
|
||||
pub use crate::slash_command_registry::*;
|
||||
pub use crate::slash_command_working_set::*;
|
||||
use anyhow::Result;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::CodeLabelBuilder;
|
||||
use language::HighlightId;
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::ActiveTheme;
|
||||
use workspace::{Workspace, ui::IconName};
|
||||
|
||||
/// Deserializes IconName, falling back to Code for unknown variants.
|
||||
/// This handles old saved data that may contain removed or renamed icon variants.
|
||||
fn deserialize_icon_with_fallback<'de, D>(deserializer: D) -> Result<IconName, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(String::deserialize(deserializer)
|
||||
.ok()
|
||||
.and_then(|string| serde_json::from_value(serde_json::Value::String(string)).ok())
|
||||
.unwrap_or(IconName::Code))
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
extension_slash_command::init(cx);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AfterCompletion {
|
||||
/// Run the command
|
||||
Run,
|
||||
/// Continue composing the current argument, doesn't add a space
|
||||
Compose,
|
||||
/// Continue the command composition, adds a space
|
||||
Continue,
|
||||
}
|
||||
|
||||
impl From<bool> for AfterCompletion {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
AfterCompletion::Run
|
||||
} else {
|
||||
AfterCompletion::Continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AfterCompletion {
|
||||
pub fn run(&self) -> bool {
|
||||
match self {
|
||||
AfterCompletion::Run => true,
|
||||
AfterCompletion::Compose | AfterCompletion::Continue => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArgumentCompletion {
|
||||
/// The label to display for this completion.
|
||||
pub label: CodeLabel,
|
||||
/// The new text that should be inserted into the command when this completion is accepted.
|
||||
pub new_text: String,
|
||||
/// Whether the command should be run when accepting this completion.
|
||||
pub after_completion: AfterCompletion,
|
||||
/// Whether to replace the all arguments, or whether to treat this as an independent argument.
|
||||
pub replace_previous_arguments: bool,
|
||||
}
|
||||
|
||||
pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent>>>;
|
||||
|
||||
pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn name(&self) -> String;
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Slash
|
||||
}
|
||||
fn label(&self, _cx: &App) -> CodeLabel {
|
||||
CodeLabel::plain(self.name(), None)
|
||||
}
|
||||
fn description(&self) -> String;
|
||||
fn menu_text(&self) -> String;
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
self.requires_argument()
|
||||
}
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
||||
// what the extension API is already expecting.
|
||||
//
|
||||
// It may be that `LspAdapterDelegate` needs a more general name, or
|
||||
// perhaps another kind of delegate is needed here.
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult>;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum SlashCommandContent {
|
||||
Text {
|
||||
text: String,
|
||||
run_commands_in_text: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for SlashCommandContent {
|
||||
fn from(text: &'a str) -> Self {
|
||||
Self::Text {
|
||||
text: text.into(),
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum SlashCommandEvent {
|
||||
StartMessage {
|
||||
role: Role,
|
||||
merge_same_roles: bool,
|
||||
},
|
||||
StartSection {
|
||||
icon: IconName,
|
||||
label: SharedString,
|
||||
metadata: Option<serde_json::Value>,
|
||||
},
|
||||
Content(SlashCommandContent),
|
||||
EndSection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Clone)]
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
impl SlashCommandOutput {
|
||||
pub fn ensure_valid_section_ranges(&mut self) {
|
||||
for section in &mut self.sections {
|
||||
section.range.start = section.range.start.min(self.text.len());
|
||||
section.range.end = section.range.end.min(self.text.len());
|
||||
while !self.text.is_char_boundary(section.range.start) {
|
||||
section.range.start -= 1;
|
||||
}
|
||||
while !self.text.is_char_boundary(section.range.end) {
|
||||
section.range.end += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s.
|
||||
pub fn into_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
|
||||
self.ensure_valid_section_ranges();
|
||||
|
||||
let mut events = Vec::new();
|
||||
|
||||
let mut section_endpoints = Vec::new();
|
||||
for section in self.sections {
|
||||
section_endpoints.push((
|
||||
section.range.start,
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: section.icon,
|
||||
label: section.label,
|
||||
metadata: section.metadata,
|
||||
},
|
||||
));
|
||||
section_endpoints.push((section.range.end, SlashCommandEvent::EndSection));
|
||||
}
|
||||
section_endpoints.sort_by_key(|(offset, _)| *offset);
|
||||
|
||||
let mut content_offset = 0;
|
||||
for (endpoint_offset, endpoint) in section_endpoints {
|
||||
if content_offset < endpoint_offset {
|
||||
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: self.text[content_offset..endpoint_offset].to_string(),
|
||||
run_commands_in_text: self.run_commands_in_text,
|
||||
})));
|
||||
content_offset = endpoint_offset;
|
||||
}
|
||||
|
||||
events.push(Ok(endpoint));
|
||||
}
|
||||
|
||||
if content_offset < self.text.len() {
|
||||
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: self.text[content_offset..].to_string(),
|
||||
run_commands_in_text: self.run_commands_in_text,
|
||||
})));
|
||||
}
|
||||
|
||||
stream::iter(events).boxed()
|
||||
}
|
||||
|
||||
pub async fn from_event_stream(
|
||||
mut events: BoxStream<'static, Result<SlashCommandEvent>>,
|
||||
) -> Result<SlashCommandOutput> {
|
||||
let mut output = SlashCommandOutput::default();
|
||||
let mut section_stack = Vec::new();
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
match event? {
|
||||
SlashCommandEvent::StartSection {
|
||||
icon,
|
||||
label,
|
||||
metadata,
|
||||
} => {
|
||||
let start = output.text.len();
|
||||
section_stack.push(SlashCommandOutputSection {
|
||||
range: start..start,
|
||||
icon,
|
||||
label,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text,
|
||||
run_commands_in_text,
|
||||
}) => {
|
||||
output.text.push_str(&text);
|
||||
output.run_commands_in_text = run_commands_in_text;
|
||||
|
||||
if let Some(section) = section_stack.last_mut() {
|
||||
section.range.end = output.text.len();
|
||||
}
|
||||
}
|
||||
SlashCommandEvent::EndSection => {
|
||||
if let Some(section) = section_stack.pop() {
|
||||
output.sections.push(section);
|
||||
}
|
||||
}
|
||||
SlashCommandEvent::StartMessage { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(section) = section_stack.pop() {
|
||||
output.sections.push(section);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SlashCommandOutputSection<T> {
|
||||
pub range: Range<T>,
|
||||
#[serde(deserialize_with = "deserialize_icon_with_fallback")]
|
||||
pub icon: IconName,
|
||||
pub label: SharedString,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl SlashCommandOutputSection<language::Anchor> {
|
||||
pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
|
||||
self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SlashCommandLine {
|
||||
/// The range within the line containing the command name.
|
||||
pub name: Range<usize>,
|
||||
/// Ranges within the line containing the command arguments.
|
||||
pub arguments: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl SlashCommandLine {
|
||||
pub fn parse(line: &str) -> Option<Self> {
|
||||
let mut call: Option<Self> = None;
|
||||
let mut ix = 0;
|
||||
for c in line.chars() {
|
||||
let next_ix = ix + c.len_utf8();
|
||||
if let Some(call) = &mut call {
|
||||
// The command arguments start at the first non-whitespace character
|
||||
// after the command name, and continue until the end of the line.
|
||||
if let Some(argument) = call.arguments.last_mut() {
|
||||
if c.is_whitespace() {
|
||||
if (*argument).is_empty() {
|
||||
argument.start = next_ix;
|
||||
argument.end = next_ix;
|
||||
} else {
|
||||
argument.end = ix;
|
||||
call.arguments.push(next_ix..next_ix);
|
||||
}
|
||||
} else {
|
||||
argument.end = next_ix;
|
||||
}
|
||||
}
|
||||
// The command name ends at the first whitespace character.
|
||||
else if !call.name.is_empty() {
|
||||
if c.is_whitespace() {
|
||||
call.arguments = vec![next_ix..next_ix];
|
||||
} else {
|
||||
call.name.end = next_ix;
|
||||
}
|
||||
}
|
||||
// The command name must begin with a letter.
|
||||
else if c.is_alphabetic() {
|
||||
call.name.end = next_ix;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// Commands start with a slash.
|
||||
else if c == '/' {
|
||||
call = Some(SlashCommandLine {
|
||||
name: next_ix..next_ix,
|
||||
arguments: Vec::new(),
|
||||
});
|
||||
}
|
||||
// The line can't contain anything before the slash except for whitespace.
|
||||
else if !c.is_whitespace() {
|
||||
return None;
|
||||
}
|
||||
ix = next_ix;
|
||||
}
|
||||
call
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
label.push_str(command_name, None);
|
||||
label.respan_filter_range(None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slash_command_output_to_events_round_trip() {
|
||||
// Test basic output consisting of a single section.
|
||||
{
|
||||
let text = "Hello, world!".to_string();
|
||||
let range = 0..text.len();
|
||||
let output = SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::Code,
|
||||
label: "Section 1".into(),
|
||||
metadata: None,
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
};
|
||||
|
||||
let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
|
||||
let events = events
|
||||
.into_iter()
|
||||
.filter_map(|event| event.ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: IconName::Code,
|
||||
label: "Section 1".into(),
|
||||
metadata: None
|
||||
},
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Hello, world!".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::EndSection
|
||||
]
|
||||
);
|
||||
|
||||
let new_output =
|
||||
SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(new_output, output);
|
||||
}
|
||||
|
||||
// Test output where the sections do not comprise all of the text.
|
||||
{
|
||||
let text = "Apple\nCucumber\nBanana\n".to_string();
|
||||
let output = SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![
|
||||
SlashCommandOutputSection {
|
||||
range: 0..6,
|
||||
icon: IconName::Check,
|
||||
label: "Fruit".into(),
|
||||
metadata: None,
|
||||
},
|
||||
SlashCommandOutputSection {
|
||||
range: 15..22,
|
||||
icon: IconName::Check,
|
||||
label: "Fruit".into(),
|
||||
metadata: None,
|
||||
},
|
||||
],
|
||||
run_commands_in_text: false,
|
||||
};
|
||||
|
||||
let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
|
||||
let events = events
|
||||
.into_iter()
|
||||
.filter_map(|event| event.ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: IconName::Check,
|
||||
label: "Fruit".into(),
|
||||
metadata: None
|
||||
},
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Apple\n".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::EndSection,
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Cucumber\n".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: IconName::Check,
|
||||
label: "Fruit".into(),
|
||||
metadata: None
|
||||
},
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Banana\n".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::EndSection
|
||||
]
|
||||
);
|
||||
|
||||
let new_output =
|
||||
SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(new_output, output);
|
||||
}
|
||||
|
||||
// Test output consisting of multiple sections.
|
||||
{
|
||||
let text = "Line 1\nLine 2\nLine 3\nLine 4\n".to_string();
|
||||
let output = SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![
|
||||
SlashCommandOutputSection {
|
||||
range: 0..6,
|
||||
icon: IconName::FileCode,
|
||||
label: "Section 1".into(),
|
||||
metadata: Some(json!({ "a": true })),
|
||||
},
|
||||
SlashCommandOutputSection {
|
||||
range: 7..13,
|
||||
icon: IconName::FileDoc,
|
||||
label: "Section 2".into(),
|
||||
metadata: Some(json!({ "b": true })),
|
||||
},
|
||||
SlashCommandOutputSection {
|
||||
range: 14..20,
|
||||
icon: IconName::FileGit,
|
||||
label: "Section 3".into(),
|
||||
metadata: Some(json!({ "c": true })),
|
||||
},
|
||||
SlashCommandOutputSection {
|
||||
range: 21..27,
|
||||
icon: IconName::FileToml,
|
||||
label: "Section 4".into(),
|
||||
metadata: Some(json!({ "d": true })),
|
||||
},
|
||||
],
|
||||
run_commands_in_text: false,
|
||||
};
|
||||
|
||||
let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
|
||||
let events = events
|
||||
.into_iter()
|
||||
.filter_map(|event| event.ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileCode,
|
||||
label: "Section 1".into(),
|
||||
metadata: Some(json!({ "a": true }))
|
||||
},
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Line 1".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::EndSection,
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileDoc,
|
||||
label: "Section 2".into(),
|
||||
metadata: Some(json!({ "b": true }))
|
||||
},
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Line 2".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::EndSection,
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileGit,
|
||||
label: "Section 3".into(),
|
||||
metadata: Some(json!({ "c": true }))
|
||||
},
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Line 3".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::EndSection,
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileToml,
|
||||
label: "Section 4".into(),
|
||||
metadata: Some(json!({ "d": true }))
|
||||
},
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "Line 4".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
SlashCommandEvent::EndSection,
|
||||
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
let new_output =
|
||||
SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(new_output, output);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_with_valid_icon_pascal_case() {
|
||||
// Test that PascalCase icons (serde default) deserialize correctly
|
||||
let json = json!({
|
||||
"range": {
|
||||
"start": 0,
|
||||
"end": 5
|
||||
},
|
||||
"icon": "AcpRegistry",
|
||||
"label": "Test",
|
||||
"metadata": null
|
||||
});
|
||||
let section: SlashCommandOutputSection<usize> = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(section.icon, IconName::AcpRegistry);
|
||||
}
|
||||
#[test]
|
||||
fn test_deserialize_with_unknown_icon() {
|
||||
// Test that unknown icon variants fall back to Code
|
||||
let json = json!({
|
||||
"range": {
|
||||
"start": 0,
|
||||
"end": 5
|
||||
},
|
||||
"icon": "removed_icon",
|
||||
"label": "Old Icon",
|
||||
"metadata": null
|
||||
});
|
||||
let section: SlashCommandOutputSection<usize> = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(section.icon, IconName::Code);
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::prelude::*;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandRegistry, SlashCommandResult,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let proxy = ExtensionHostProxy::default_global(cx);
|
||||
proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
|
||||
slash_command_registry: SlashCommandRegistry::global(cx),
|
||||
});
|
||||
}
|
||||
|
||||
struct SlashCommandRegistryProxy {
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
}
|
||||
|
||||
impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
|
||||
fn register_slash_command(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
command: extension::SlashCommand,
|
||||
) {
|
||||
self.slash_command_registry
|
||||
.register_command(ExtensionSlashCommand::new(extension, command), false)
|
||||
}
|
||||
|
||||
fn unregister_slash_command(&self, command_name: Arc<str>) {
|
||||
self.slash_command_registry
|
||||
.unregister_command_by_name(&command_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
|
||||
struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);
|
||||
|
||||
#[async_trait]
|
||||
impl WorktreeDelegate for WorktreeDelegateAdapter {
|
||||
fn id(&self) -> u64 {
|
||||
self.0.worktree_id().to_proto()
|
||||
}
|
||||
|
||||
fn root_path(&self) -> String {
|
||||
self.0.worktree_root_path().to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
async fn read_text_file(&self, path: &RelPath) -> Result<String> {
|
||||
self.0.read_text_file(path).await
|
||||
}
|
||||
|
||||
async fn which(&self, binary_name: String) -> Option<String> {
|
||||
self.0
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
async fn shell_env(&self) -> Vec<(String, String)> {
|
||||
self.0.shell_env().await.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtensionSlashCommand {
|
||||
extension: Arc<dyn Extension>,
|
||||
command: extension::SlashCommand,
|
||||
}
|
||||
|
||||
impl ExtensionSlashCommand {
|
||||
pub fn new(extension: Arc<dyn Extension>, command: extension::SlashCommand) -> Self {
|
||||
Self { extension, command }
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for ExtensionSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
self.command.name.clone()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
self.command.description.clone()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.command.tooltip_text.clone()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
self.command.requires_argument
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
cx.background_spawn(async move {
|
||||
let completions = self
|
||||
.extension
|
||||
.complete_slash_command_argument(command, arguments)
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|completion| ArgumentCompletion {
|
||||
label: completion.label.into(),
|
||||
new_text: completion.new_text,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: completion.run_command.into(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
let output = cx.background_spawn(async move {
|
||||
let delegate =
|
||||
delegate.map(|delegate| Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _);
|
||||
let output = self
|
||||
.extension
|
||||
.run_slash_command(command, arguments, delegate)
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(output)
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let output = output.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text: output.text,
|
||||
sections: output
|
||||
.sections
|
||||
.into_iter()
|
||||
.map(|section| SlashCommandOutputSection {
|
||||
range: section.range,
|
||||
icon: IconName::Code,
|
||||
label: section.label.into(),
|
||||
metadata: None,
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.into_event_stream())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::Global;
|
||||
use gpui::{App, ReadGlobal};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::SlashCommand;
|
||||
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
struct GlobalSlashCommandRegistry(Arc<SlashCommandRegistry>);
|
||||
|
||||
impl Global for GlobalSlashCommandRegistry {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SlashCommandRegistryState {
|
||||
commands: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
|
||||
featured_commands: BTreeSet<Arc<str>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SlashCommandRegistry {
|
||||
state: RwLock<SlashCommandRegistryState>,
|
||||
}
|
||||
|
||||
impl SlashCommandRegistry {
|
||||
/// Returns the global [`SlashCommandRegistry`].
|
||||
pub fn global(cx: &App) -> Arc<Self> {
|
||||
GlobalSlashCommandRegistry::global(cx).0.clone()
|
||||
}
|
||||
|
||||
/// Returns the global [`SlashCommandRegistry`].
|
||||
///
|
||||
/// Inserts a default [`SlashCommandRegistry`] if one does not yet exist.
|
||||
pub fn default_global(cx: &mut App) -> Arc<Self> {
|
||||
cx.default_global::<GlobalSlashCommandRegistry>().0.clone()
|
||||
}
|
||||
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
state: RwLock::new(SlashCommandRegistryState {
|
||||
commands: HashMap::default(),
|
||||
featured_commands: BTreeSet::default(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers the provided [`SlashCommand`].
|
||||
pub fn register_command(&self, command: impl SlashCommand, is_featured: bool) {
|
||||
let mut state = self.state.write();
|
||||
let command_name: Arc<str> = command.name().into();
|
||||
if is_featured {
|
||||
state.featured_commands.insert(command_name.clone());
|
||||
}
|
||||
state.commands.insert(command_name, Arc::new(command));
|
||||
}
|
||||
|
||||
/// Unregisters the provided [`SlashCommand`].
|
||||
pub fn unregister_command(&self, command: impl SlashCommand) {
|
||||
self.unregister_command_by_name(command.name().as_str())
|
||||
}
|
||||
|
||||
/// Unregisters the command with the given name.
|
||||
pub fn unregister_command_by_name(&self, command_name: &str) {
|
||||
let mut state = self.state.write();
|
||||
state.featured_commands.remove(command_name);
|
||||
state.commands.remove(command_name);
|
||||
}
|
||||
|
||||
/// Returns the names of registered [`SlashCommand`]s.
|
||||
pub fn command_names(&self) -> Vec<Arc<str>> {
|
||||
self.state.read().commands.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Returns the names of registered, featured [`SlashCommand`]s.
|
||||
pub fn featured_command_names(&self) -> Vec<Arc<str>> {
|
||||
self.state
|
||||
.read()
|
||||
.featured_commands
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the [`SlashCommand`] with the given name.
|
||||
pub fn command(&self, name: &str) -> Option<Arc<dyn SlashCommand>> {
|
||||
self.state.read().commands.get(name).cloned()
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::App;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{SlashCommand, SlashCommandRegistry};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub struct SlashCommandId(usize);
|
||||
|
||||
/// A working set of slash commands for use in one instance of the Assistant Panel.
|
||||
#[derive(Default)]
|
||||
pub struct SlashCommandWorkingSet {
|
||||
state: Mutex<WorkingSetState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WorkingSetState {
|
||||
context_server_commands_by_id: HashMap<SlashCommandId, Arc<dyn SlashCommand>>,
|
||||
context_server_commands_by_name: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
|
||||
next_command_id: SlashCommandId,
|
||||
}
|
||||
|
||||
impl SlashCommandWorkingSet {
|
||||
pub fn command(&self, name: &str, cx: &App) -> Option<Arc<dyn SlashCommand>> {
|
||||
self.state
|
||||
.lock()
|
||||
.context_server_commands_by_name
|
||||
.get(name)
|
||||
.cloned()
|
||||
.or_else(|| SlashCommandRegistry::global(cx).command(name))
|
||||
}
|
||||
|
||||
pub fn command_names(&self, cx: &App) -> Vec<Arc<str>> {
|
||||
let mut command_names = SlashCommandRegistry::global(cx).command_names();
|
||||
command_names.extend(
|
||||
self.state
|
||||
.lock()
|
||||
.context_server_commands_by_name
|
||||
.keys()
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
command_names
|
||||
}
|
||||
|
||||
pub fn featured_command_names(&self, cx: &App) -> Vec<Arc<str>> {
|
||||
SlashCommandRegistry::global(cx).featured_command_names()
|
||||
}
|
||||
|
||||
pub fn insert(&self, command: Arc<dyn SlashCommand>) -> SlashCommandId {
|
||||
let mut state = self.state.lock();
|
||||
let command_id = state.next_command_id;
|
||||
state.next_command_id.0 += 1;
|
||||
state
|
||||
.context_server_commands_by_id
|
||||
.insert(command_id, command.clone());
|
||||
state.slash_commands_changed();
|
||||
command_id
|
||||
}
|
||||
|
||||
pub fn remove(&self, command_ids_to_remove: &[SlashCommandId]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.context_server_commands_by_id
|
||||
.retain(|id, _| !command_ids_to_remove.contains(id));
|
||||
state.slash_commands_changed();
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkingSetState {
|
||||
fn slash_commands_changed(&mut self) {
|
||||
self.context_server_commands_by_name.clear();
|
||||
self.context_server_commands_by_name.extend(
|
||||
self.context_server_commands_by_id
|
||||
.values()
|
||||
.map(|command| (command.name().into(), command.clone())),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "assistant_slash_commands"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_slash_commands.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
rope.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
text.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
multi_buffer = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,25 +0,0 @@
|
||||
mod default_command;
|
||||
mod delta_command;
|
||||
mod diagnostics_command;
|
||||
mod fetch_command;
|
||||
mod file_command;
|
||||
mod now_command;
|
||||
mod prompt_command;
|
||||
mod selection_command;
|
||||
mod streaming_example_command;
|
||||
mod symbols_command;
|
||||
mod tab_command;
|
||||
|
||||
pub use crate::default_command::*;
|
||||
pub use crate::delta_command::*;
|
||||
pub use crate::diagnostics_command::*;
|
||||
pub use crate::fetch_command::*;
|
||||
pub use crate::file_command::*;
|
||||
pub use crate::now_command::*;
|
||||
pub use crate::prompt_command::*;
|
||||
pub use crate::selection_command::*;
|
||||
pub use crate::streaming_example_command::*;
|
||||
pub use crate::symbols_command::*;
|
||||
pub use crate::tab_command::*;
|
||||
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
@@ -1,91 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use prompt_store::PromptStore;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct DefaultSlashCommand;
|
||||
|
||||
impl SlashCommand for DefaultSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"default".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert default prompt".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let store = store.await?;
|
||||
let prompts = store.read_with(cx, |store, _cx| store.default_prompt_metadata());
|
||||
|
||||
let mut text = String::new();
|
||||
text.push('\n');
|
||||
for prompt in prompts {
|
||||
if let Some(title) = prompt.title {
|
||||
writeln!(text, "/prompt {}", title).unwrap();
|
||||
}
|
||||
}
|
||||
text.pop();
|
||||
|
||||
if text.is_empty() {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
icon: IconName::Library,
|
||||
label: "Default".into(),
|
||||
metadata: None,
|
||||
}],
|
||||
text,
|
||||
run_commands_in_text: true,
|
||||
}
|
||||
.into_event_stream())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
use crate::file_command::{FileCommandMetadata, FileSlashCommand};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::future;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct DeltaSlashCommand;
|
||||
|
||||
impl SlashCommand for DeltaSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"delta".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Re-insert changed files".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Diff
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut paths = HashSet::default();
|
||||
let mut file_command_old_outputs = Vec::new();
|
||||
let mut file_command_new_outputs = Vec::new();
|
||||
|
||||
for section in context_slash_command_output_sections.iter().rev() {
|
||||
if let Some(metadata) = section
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|value| serde_json::from_value::<FileCommandMetadata>(value.clone()).ok())
|
||||
&& paths.insert(metadata.path.clone())
|
||||
{
|
||||
file_command_old_outputs.push(
|
||||
context_buffer
|
||||
.as_rope()
|
||||
.slice(section.range.to_offset(&context_buffer)),
|
||||
);
|
||||
file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
|
||||
std::slice::from_ref(&metadata.path),
|
||||
context_slash_command_output_sections,
|
||||
context_buffer.clone(),
|
||||
workspace.clone(),
|
||||
delegate.clone(),
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut output = SlashCommandOutput::default();
|
||||
let mut changes_detected = false;
|
||||
|
||||
let file_command_new_outputs = future::join_all(file_command_new_outputs).await;
|
||||
for (old_text, new_output) in file_command_old_outputs
|
||||
.into_iter()
|
||||
.zip(file_command_new_outputs)
|
||||
{
|
||||
if let Ok(new_output) = new_output
|
||||
&& let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await
|
||||
&& let Some(file_command_range) = new_output.sections.first()
|
||||
{
|
||||
let new_text = &new_output.text[file_command_range.range.clone()];
|
||||
if old_text.chars().ne(new_text.chars()) {
|
||||
changes_detected = true;
|
||||
output
|
||||
.sections
|
||||
.extend(new_output.sections.into_iter().map(|section| {
|
||||
SlashCommandOutputSection {
|
||||
range: output.text.len() + section.range.start
|
||||
..output.text.len() + section.range.end,
|
||||
icon: section.icon,
|
||||
label: section.label,
|
||||
metadata: section.metadata,
|
||||
}
|
||||
}));
|
||||
output.text.push_str(&new_output.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::ensure!(changes_detected, "no new changes detected");
|
||||
Ok(output.into_event_stream())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use fuzzy::{PathMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{
|
||||
Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, LspAdapterDelegate,
|
||||
OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::paths::{PathMatcher, PathStyle};
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::create_label_for_command;
|
||||
|
||||
pub struct DiagnosticsSlashCommand;
|
||||
|
||||
impl DiagnosticsSlashCommand {
|
||||
fn search_paths(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<RelPath> = RelPath::empty().into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(entry, _)| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: entry.path,
|
||||
path_prefix: path_prefix.clone(),
|
||||
is_dir: false, // Diagnostics can't be produced for directories
|
||||
distance_to_relative_ancestor: 0,
|
||||
})
|
||||
.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()
|
||||
.is_some_and(|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 SlashCommand for DiagnosticsSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"diagnostics".into()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &App) -> language::CodeLabel {
|
||||
create_label_for_command("diagnostics", &[INCLUDE_WARNINGS_ARGUMENT], cx)
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert diagnostics".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::XCircle
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
let path_style = workspace.read(cx).project().read(cx).path_style(cx);
|
||||
let query = arguments.last().cloned().unwrap_or_default();
|
||||
|
||||
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_spawn(async move {
|
||||
let mut matches: Vec<String> = paths
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path_match| {
|
||||
path_match
|
||||
.path_prefix
|
||||
.join(&path_match.path)
|
||||
.display(path_style)
|
||||
.to_string()
|
||||
})
|
||||
.collect();
|
||||
|
||||
matches.extend(
|
||||
fuzzy::match_strings(
|
||||
&Options::match_candidates_for_args(),
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
10,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|candidate| candidate.string),
|
||||
);
|
||||
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|completion| ArgumentCompletion {
|
||||
label: completion.clone().into(),
|
||||
new_text: completion,
|
||||
after_completion: assistant_slash_command::AfterCompletion::Run,
|
||||
replace_previous_arguments: false,
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project();
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let options = Options::parse(arguments, path_style);
|
||||
|
||||
let task = collect_diagnostics_output(project.clone(), options, cx);
|
||||
|
||||
window.spawn(cx, async move |_| {
|
||||
task.await?
|
||||
.map(|output| output.into_event_stream())
|
||||
.context("No diagnostics found")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Options {
|
||||
pub include_errors: bool,
|
||||
pub include_warnings: bool,
|
||||
pub path_matcher: Option<PathMatcher>,
|
||||
}
|
||||
|
||||
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
|
||||
|
||||
impl Options {
|
||||
fn parse(arguments: &[String], path_style: PathStyle) -> Self {
|
||||
let mut include_warnings = false;
|
||||
let mut path_matcher = None;
|
||||
for arg in arguments {
|
||||
if arg == INCLUDE_WARNINGS_ARGUMENT {
|
||||
include_warnings = true;
|
||||
} else {
|
||||
path_matcher = PathMatcher::new(&[arg.to_owned()], path_style).log_err();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
include_errors: true,
|
||||
include_warnings,
|
||||
path_matcher,
|
||||
}
|
||||
}
|
||||
|
||||
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
|
||||
[StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_diagnostics_output(
|
||||
project: Entity<Project>,
|
||||
options: Options,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<SlashCommandOutput>>> {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let glob_is_exact_file_match = if let Some(path) = options
|
||||
.path_matcher
|
||||
.as_ref()
|
||||
.and_then(|pm| pm.sources().next())
|
||||
{
|
||||
project
|
||||
.read(cx)
|
||||
.find_project_path(Path::new(path), cx)
|
||||
.is_some()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let project_handle = project.downgrade();
|
||||
let diagnostic_summaries: Vec<_> = project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(false, cx)
|
||||
.flat_map(|(path, _, summary)| {
|
||||
let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
|
||||
let full_path = worktree.read(cx).root_name().join(&path.path);
|
||||
Some((path, full_path, summary))
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let error_source = if let Some(path_matcher) = &options.path_matcher {
|
||||
debug_assert_eq!(path_matcher.sources().count(), 1);
|
||||
Some(path_matcher.sources().next().unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut output = SlashCommandOutput::default();
|
||||
|
||||
if let Some(error_source) = error_source.as_ref() {
|
||||
writeln!(output.text, "diagnostics: {}", error_source).unwrap();
|
||||
} else {
|
||||
writeln!(output.text, "diagnostics").unwrap();
|
||||
}
|
||||
|
||||
let mut project_summary = DiagnosticSummary::default();
|
||||
for (project_path, path, summary) in diagnostic_summaries {
|
||||
if let Some(path_matcher) = &options.path_matcher
|
||||
&& !path_matcher.is_match(&path)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let has_errors = options.include_errors && summary.error_count > 0;
|
||||
let has_warnings = options.include_warnings && summary.warning_count > 0;
|
||||
if !has_errors && !has_warnings {
|
||||
continue;
|
||||
}
|
||||
|
||||
if options.include_errors {
|
||||
project_summary.error_count += summary.error_count;
|
||||
}
|
||||
if options.include_warnings {
|
||||
project_summary.warning_count += summary.warning_count;
|
||||
}
|
||||
|
||||
let last_end = output.text.len();
|
||||
let file_path = path.display(path_style).to_string();
|
||||
if !glob_is_exact_file_match {
|
||||
writeln!(&mut output.text, "{file_path}").unwrap();
|
||||
}
|
||||
|
||||
if let Some(buffer) = project_handle
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());
|
||||
collect_buffer_diagnostics(
|
||||
&mut output,
|
||||
&snapshot,
|
||||
options.include_warnings,
|
||||
options.include_errors,
|
||||
);
|
||||
}
|
||||
|
||||
if !glob_is_exact_file_match {
|
||||
output.sections.push(SlashCommandOutputSection {
|
||||
range: last_end..output.text.len().saturating_sub(1),
|
||||
icon: IconName::File,
|
||||
label: file_path.into(),
|
||||
metadata: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// No diagnostics found
|
||||
if output.sections.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut label = String::new();
|
||||
label.push_str("Diagnostics");
|
||||
if let Some(source) = error_source {
|
||||
write!(label, " ({})", source).unwrap();
|
||||
}
|
||||
|
||||
if project_summary.error_count > 0 || project_summary.warning_count > 0 {
|
||||
label.push(':');
|
||||
|
||||
if project_summary.error_count > 0 {
|
||||
write!(label, " {} errors", project_summary.error_count).unwrap();
|
||||
if project_summary.warning_count > 0 {
|
||||
label.push_str(",");
|
||||
}
|
||||
}
|
||||
|
||||
if project_summary.warning_count > 0 {
|
||||
write!(label, " {} warnings", project_summary.warning_count).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
output.sections.insert(
|
||||
0,
|
||||
SlashCommandOutputSection {
|
||||
range: 0..output.text.len(),
|
||||
icon: IconName::Warning,
|
||||
label: label.into(),
|
||||
metadata: None,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Some(output))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collect_buffer_diagnostics(
|
||||
output: &mut SlashCommandOutput,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
include_errors: bool,
|
||||
) {
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
collect_diagnostic(output, entry, snapshot, include_warnings, include_errors)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_diagnostic(
|
||||
output: &mut SlashCommandOutput,
|
||||
entry: &DiagnosticEntryRef<'_, Anchor>,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
include_errors: bool,
|
||||
) {
|
||||
const EXCERPT_EXPANSION_SIZE: u32 = 2;
|
||||
const MAX_MESSAGE_LENGTH: usize = 2000;
|
||||
|
||||
let (ty, icon) = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::WARNING => {
|
||||
if !include_warnings {
|
||||
return;
|
||||
}
|
||||
("warning", IconName::Warning)
|
||||
}
|
||||
DiagnosticSeverity::ERROR => {
|
||||
if !include_errors {
|
||||
return;
|
||||
}
|
||||
("error", IconName::XCircle)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
let prev_len = output.text.len();
|
||||
|
||||
let range = entry.range.to_point(snapshot);
|
||||
let diagnostic_row_number = range.start.row + 1;
|
||||
|
||||
let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
|
||||
let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
|
||||
let excerpt_range =
|
||||
Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot);
|
||||
|
||||
output.text.push_str("```");
|
||||
if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
|
||||
output.text.push_str(&language_name);
|
||||
}
|
||||
output.text.push('\n');
|
||||
|
||||
let mut buffer_text = String::new();
|
||||
for chunk in snapshot.text_for_range(excerpt_range) {
|
||||
buffer_text.push_str(chunk);
|
||||
}
|
||||
|
||||
for (i, line) in buffer_text.lines().enumerate() {
|
||||
let line_number = start_row + i as u32 + 1;
|
||||
writeln!(output.text, "{}", line).unwrap();
|
||||
|
||||
if line_number == diagnostic_row_number {
|
||||
output.text.push_str("//");
|
||||
let prev_len = output.text.len();
|
||||
write!(output.text, " {}: ", ty).unwrap();
|
||||
let padding = output.text.len() - prev_len;
|
||||
|
||||
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
|
||||
.replace('\n', format!("\n//{:padding$}", "").as_str());
|
||||
|
||||
writeln!(output.text, "{message}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output.text, "```").unwrap();
|
||||
output.sections.push(SlashCommandOutputSection {
|
||||
range: prev_len..output.text.len().saturating_sub(1),
|
||||
icon,
|
||||
label: entry.diagnostic.message.clone().into(),
|
||||
metadata: None,
|
||||
});
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
Html,
|
||||
Plaintext,
|
||||
Json,
|
||||
}
|
||||
|
||||
pub struct FetchSlashCommand;
|
||||
|
||||
impl FetchSlashCommand {
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
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")?;
|
||||
|
||||
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 = if content_type.starts_with("text/html") {
|
||||
ContentType::Html
|
||||
} else if content_type.starts_with("text/plain") {
|
||||
ContentType::Plaintext
|
||||
} else if content_type.starts_with("application/json") {
|
||||
ContentType::Json
|
||||
} else {
|
||||
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)?
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for FetchSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"fetch".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert fetched URL contents".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ToolWeb
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(argument) = arguments.first() else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
};
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let url = argument.to_string();
|
||||
|
||||
let text = cx.background_spawn({
|
||||
let url = url.clone();
|
||||
async move { Self::build_message(http_client, &url).await }
|
||||
});
|
||||
|
||||
let url = SharedString::from(url);
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
if text.trim().is_empty() {
|
||||
bail!("no textual content found");
|
||||
}
|
||||
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::ToolWeb,
|
||||
label: format!("fetch {}", url).into(),
|
||||
metadata: None,
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.into_event_stream())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,713 +0,0 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use futures::Stream;
|
||||
use futures::channel::mpsc;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabelBuilder, HighlightId, LineEnding, LspAdapterDelegate};
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
ops::{Range, RangeInclusive},
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
use workspace::Workspace;
|
||||
use worktree::ChildEntriesOptions;
|
||||
|
||||
pub struct FileSlashCommand;
|
||||
|
||||
impl FileSlashCommand {
|
||||
fn search_paths(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
|
||||
let entries = entries
|
||||
.into_iter()
|
||||
.map(|entries| (entries.0, false))
|
||||
.chain(project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let id = worktree.id();
|
||||
let options = ChildEntriesOptions {
|
||||
include_files: true,
|
||||
include_dirs: true,
|
||||
include_ignored: false,
|
||||
};
|
||||
let entries = worktree.child_entries_with_options(RelPath::empty(), options);
|
||||
entries.map(move |entry| {
|
||||
(
|
||||
project::ProjectPath {
|
||||
worktree_id: id,
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
entry.kind.is_dir(),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_prefix: Arc<RelPath> = RelPath::empty().into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|(entry, is_dir)| {
|
||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||
let full_path = worktree.read(cx).root_name().join(&entry.path);
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: full_path,
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir,
|
||||
})
|
||||
})
|
||||
.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()
|
||||
.is_some_and(|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 SlashCommand for FileSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"file".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert file and/or directory".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::File
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
|
||||
let paths = self.search_paths(
|
||||
arguments.last().cloned().unwrap_or_default(),
|
||||
cancellation_flag,
|
||||
&workspace,
|
||||
cx,
|
||||
);
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
cx.background_spawn(async move {
|
||||
Ok(paths
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|path_match| {
|
||||
let text = path_match
|
||||
.path_prefix
|
||||
.join(&path_match.path)
|
||||
.display(path_style)
|
||||
.to_string();
|
||||
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
let file_name = path_match.path.file_name()?;
|
||||
let label_text = if path_match.is_dir {
|
||||
format!("{}/ ", file_name)
|
||||
} else {
|
||||
format!("{} ", file_name)
|
||||
};
|
||||
|
||||
label.push_str(label_text.as_str(), None);
|
||||
label.push_str(&text, comment_id);
|
||||
label.respan_filter_range(Some(file_name));
|
||||
|
||||
Some(ArgumentCompletion {
|
||||
label: label.build(),
|
||||
new_text: text,
|
||||
after_completion: AfterCompletion::Compose,
|
||||
replace_previous_arguments: false,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
if arguments.is_empty() {
|
||||
return Task::ready(Err(anyhow!("missing path")));
|
||||
};
|
||||
|
||||
Task::ready(Ok(collect_files(
|
||||
workspace.read(cx).project().clone(),
|
||||
arguments,
|
||||
cx,
|
||||
)
|
||||
.boxed()))
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_files(
|
||||
project: Entity<Project>,
|
||||
glob_inputs: &[String],
|
||||
cx: &mut App,
|
||||
) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> {
|
||||
let Ok(matchers) = glob_inputs
|
||||
.iter()
|
||||
.map(|glob_input| {
|
||||
util::paths::PathMatcher::new(&[glob_input.to_owned()], project.read(cx).path_style(cx))
|
||||
.with_context(|| format!("invalid path {glob_input}"))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<util::paths::PathMatcher>>>()
|
||||
else {
|
||||
return futures::stream::once(async {
|
||||
anyhow::bail!("invalid path");
|
||||
})
|
||||
.boxed();
|
||||
};
|
||||
|
||||
let project_handle = project.downgrade();
|
||||
let snapshots = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
cx.spawn(async move |cx| {
|
||||
for snapshot in snapshots {
|
||||
let worktree_id = snapshot.id();
|
||||
let path_style = snapshot.path_style();
|
||||
let mut directory_stack: Vec<Arc<RelPath>> = Vec::new();
|
||||
let mut folded_directory_path: Option<Arc<RelPath>> = None;
|
||||
let mut folded_directory_names: Arc<RelPath> = RelPath::empty().into();
|
||||
let mut is_top_level_directory = true;
|
||||
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
let path_including_worktree_name = snapshot.root_name().join(&entry.path);
|
||||
|
||||
if !matchers
|
||||
.iter()
|
||||
.any(|matcher| matcher.is_match(&path_including_worktree_name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
while let Some(dir) = directory_stack.last() {
|
||||
if entry.path.starts_with(dir) {
|
||||
break;
|
||||
}
|
||||
directory_stack.pop().unwrap();
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
}
|
||||
|
||||
if let Some(folded_path) = &folded_directory_path {
|
||||
if !entry.path.starts_with(folded_path) {
|
||||
folded_directory_names = RelPath::empty().into();
|
||||
folded_directory_path = None;
|
||||
if directory_stack.is_empty() {
|
||||
is_top_level_directory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filename = entry.path.file_name().unwrap_or_default().to_string();
|
||||
|
||||
if entry.is_dir() {
|
||||
// Auto-fold directories that contain no files
|
||||
let mut child_entries = snapshot.child_entries(&entry.path);
|
||||
if let Some(child) = child_entries.next() {
|
||||
if child_entries.next().is_none() && child.kind.is_dir() {
|
||||
if is_top_level_directory {
|
||||
is_top_level_directory = false;
|
||||
folded_directory_names =
|
||||
folded_directory_names.join(&path_including_worktree_name);
|
||||
} else {
|
||||
folded_directory_names =
|
||||
folded_directory_names.join(RelPath::unix(&filename).unwrap());
|
||||
}
|
||||
folded_directory_path = Some(entry.path.clone());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Skip empty directories
|
||||
folded_directory_names = RelPath::empty().into();
|
||||
folded_directory_path = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Render the directory (either folded or normal)
|
||||
if folded_directory_names.is_empty() {
|
||||
let label = if is_top_level_directory {
|
||||
is_top_level_directory = false;
|
||||
path_including_worktree_name.display(path_style).to_string()
|
||||
} else {
|
||||
filename
|
||||
};
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::Folder,
|
||||
label: label.clone().into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: label.to_string(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
directory_stack.push(entry.path.clone());
|
||||
} else {
|
||||
let entry_name =
|
||||
folded_directory_names.join(RelPath::unix(&filename).unwrap());
|
||||
let entry_name = entry_name.display(path_style);
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::Folder,
|
||||
label: entry_name.to_string().into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: entry_name.to_string(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
directory_stack.push(entry.path.clone());
|
||||
folded_directory_names = RelPath::empty().into();
|
||||
folded_directory_path = None;
|
||||
}
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
} else if entry.is_file() {
|
||||
let Some(open_buffer_task) = project_handle
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, entry.path.clone()), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(buffer) = open_buffer_task.await.log_err() {
|
||||
let mut output = SlashCommandOutput::default();
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
append_buffer_to_output(
|
||||
&snapshot,
|
||||
Some(path_including_worktree_name.display(path_style).as_ref()),
|
||||
&mut output,
|
||||
)
|
||||
.log_err();
|
||||
let mut buffer_events = output.into_event_stream();
|
||||
while let Some(event) = buffer_events.next().await {
|
||||
events_tx.unbounded_send(event)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while directory_stack.pop().is_some() {
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
events_rx.boxed()
|
||||
}
|
||||
|
||||
pub fn codeblock_fence_for_path(
|
||||
path: Option<&str>,
|
||||
row_range: Option<RangeInclusive<u32>>,
|
||||
) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{path}").unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FileCommandMetadata {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub fn build_entry_output_section(
|
||||
range: Range<usize>,
|
||||
path: Option<&str>,
|
||||
is_directory: bool,
|
||||
line_range: Option<Range<u32>>,
|
||||
) -> SlashCommandOutputSection<usize> {
|
||||
let mut label = if let Some(path) = path {
|
||||
path.to_string()
|
||||
} else {
|
||||
"untitled".to_string()
|
||||
};
|
||||
if let Some(line_range) = line_range {
|
||||
write!(label, ":{}-{}", line_range.start, line_range.end).unwrap();
|
||||
}
|
||||
|
||||
let icon = if is_directory {
|
||||
IconName::Folder
|
||||
} else {
|
||||
IconName::File
|
||||
};
|
||||
|
||||
SlashCommandOutputSection {
|
||||
range,
|
||||
icon,
|
||||
label: label.into(),
|
||||
metadata: if is_directory {
|
||||
None
|
||||
} else {
|
||||
path.and_then(|path| {
|
||||
serde_json::to_value(FileCommandMetadata {
|
||||
path: path.to_string(),
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_buffer_to_output(
|
||||
buffer: &BufferSnapshot,
|
||||
path: Option<&str>,
|
||||
output: &mut SlashCommandOutput,
|
||||
) -> Result<()> {
|
||||
let prev_len = output.text.len();
|
||||
|
||||
let mut content = buffer.text();
|
||||
LineEnding::normalize(&mut content);
|
||||
output.text.push_str(&codeblock_fence_for_path(path, None));
|
||||
output.text.push_str(&content);
|
||||
if !output.text.ends_with('\n') {
|
||||
output.text.push('\n');
|
||||
}
|
||||
output.text.push_str("```");
|
||||
output.text.push('\n');
|
||||
|
||||
let section_ix = output.sections.len();
|
||||
output.sections.insert(
|
||||
section_ix,
|
||||
build_entry_output_section(prev_len..output.text.len(), path, false, None),
|
||||
);
|
||||
|
||||
output.text.push('\n');
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use assistant_slash_command::SlashCommandOutput;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
use util::path;
|
||||
|
||||
use super::collect_files;
|
||||
|
||||
pub fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
zlog::init_test();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
// release_channel::init(SemanticVersion::default(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_exact_matching(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"dir": {
|
||||
"subdir": {
|
||||
"file_0": "0"
|
||||
},
|
||||
"file_1": "1",
|
||||
"file_2": "2",
|
||||
"file_3": "3",
|
||||
},
|
||||
"dir.rs": "4"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let result_1 =
|
||||
cx.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx));
|
||||
let result_1 = SlashCommandOutput::from_event_stream(result_1.boxed())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result_1.text.starts_with(path!("root/dir")));
|
||||
// 4 files + 2 directories
|
||||
assert_eq!(result_1.sections.len(), 6);
|
||||
|
||||
let result_2 =
|
||||
cx.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx));
|
||||
let result_2 = SlashCommandOutput::from_event_stream(result_2.boxed())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result_1, result_2);
|
||||
|
||||
let result =
|
||||
cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
|
||||
let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
|
||||
|
||||
assert!(result.text.starts_with(path!("root/dir")));
|
||||
// 5 files + 2 directories
|
||||
assert_eq!(result.sections.len(), 7);
|
||||
|
||||
// Ensure that the project lasts until after the last await
|
||||
drop(project);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_sub_directory_rendering(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/zed"),
|
||||
json!({
|
||||
"assets": {
|
||||
"dir1": {
|
||||
".gitkeep": ""
|
||||
},
|
||||
"dir2": {
|
||||
".gitkeep": ""
|
||||
},
|
||||
"themes": {
|
||||
"ayu": {
|
||||
"LICENSE": "1",
|
||||
},
|
||||
"andromeda": {
|
||||
"LICENSE": "2",
|
||||
},
|
||||
"summercamp": {
|
||||
"LICENSE": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/zed").as_ref()], cx).await;
|
||||
|
||||
let result =
|
||||
cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
|
||||
let result = SlashCommandOutput::from_event_stream(result.boxed())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Sanity check
|
||||
assert!(result.text.starts_with(path!("zed/assets/themes\n")));
|
||||
assert_eq!(result.sections.len(), 7);
|
||||
|
||||
// Ensure that full file paths are included in the real output
|
||||
assert!(
|
||||
result
|
||||
.text
|
||||
.contains(path!("zed/assets/themes/andromeda/LICENSE"))
|
||||
);
|
||||
assert!(result.text.contains(path!("zed/assets/themes/ayu/LICENSE")));
|
||||
assert!(
|
||||
result
|
||||
.text
|
||||
.contains(path!("zed/assets/themes/summercamp/LICENSE"))
|
||||
);
|
||||
|
||||
assert_eq!(result.sections[5].label, "summercamp");
|
||||
|
||||
// Ensure that things are in descending order, with properly relativized paths
|
||||
assert_eq!(
|
||||
result.sections[0].label,
|
||||
path!("zed/assets/themes/andromeda/LICENSE")
|
||||
);
|
||||
assert_eq!(result.sections[1].label, "andromeda");
|
||||
assert_eq!(
|
||||
result.sections[2].label,
|
||||
path!("zed/assets/themes/ayu/LICENSE")
|
||||
);
|
||||
assert_eq!(result.sections[3].label, "ayu");
|
||||
assert_eq!(
|
||||
result.sections[4].label,
|
||||
path!("zed/assets/themes/summercamp/LICENSE")
|
||||
);
|
||||
|
||||
// Ensure that the project lasts until after the last await
|
||||
drop(project);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_deep_sub_directory_rendering(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/zed"),
|
||||
json!({
|
||||
"assets": {
|
||||
"themes": {
|
||||
"LICENSE": "1",
|
||||
"summercamp": {
|
||||
"LICENSE": "1",
|
||||
"subdir": {
|
||||
"LICENSE": "1",
|
||||
"subsubdir": {
|
||||
"LICENSE": "3",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/zed").as_ref()], cx).await;
|
||||
|
||||
let result =
|
||||
cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
|
||||
let result = SlashCommandOutput::from_event_stream(result.boxed())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.text.starts_with(path!("zed/assets/themes\n")));
|
||||
assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE"));
|
||||
assert_eq!(
|
||||
result.sections[1].label,
|
||||
path!("zed/assets/themes/summercamp/LICENSE")
|
||||
);
|
||||
assert_eq!(
|
||||
result.sections[2].label,
|
||||
path!("zed/assets/themes/summercamp/subdir/LICENSE")
|
||||
);
|
||||
assert_eq!(
|
||||
result.sections[3].label,
|
||||
path!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
|
||||
);
|
||||
assert_eq!(result.sections[4].label, "subsubdir");
|
||||
assert_eq!(result.sections[5].label, "subdir");
|
||||
assert_eq!(result.sections[6].label, "summercamp");
|
||||
assert_eq!(result.sections[7].label, path!("zed/assets/themes"));
|
||||
|
||||
assert_eq!(
|
||||
result.text,
|
||||
path!(
|
||||
"zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"
|
||||
)
|
||||
);
|
||||
|
||||
// Ensure that the project lasts until after the last await
|
||||
drop(project);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use chrono::Local;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct NowSlashCommand;
|
||||
|
||||
impl SlashCommand for NowSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"now".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert current date and time".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let now = Local::now();
|
||||
let text = format!("Today is {now}.", now = now.to_rfc2822());
|
||||
let range = 0..text.len();
|
||||
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::CountdownTimer,
|
||||
label: now.to_rfc2822().into(),
|
||||
metadata: None,
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.into_event_stream()))
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use prompt_store::{PromptMetadata, PromptStore};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct PromptSlashCommand;
|
||||
|
||||
impl SlashCommand for PromptSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"prompt".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert prompt from library".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Library
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.to_owned().join(" ");
|
||||
cx.spawn(async move |cx| {
|
||||
let cancellation_flag = Arc::new(AtomicBool::default());
|
||||
let prompts: Vec<PromptMetadata> = store
|
||||
.await?
|
||||
.read_with(cx, |store, cx| store.search(query, cancellation_flag, cx))
|
||||
.await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.filter_map(|prompt| {
|
||||
let prompt_title = prompt.title?.to_string();
|
||||
Some(ArgumentCompletion {
|
||||
label: prompt_title.clone().into(),
|
||||
new_text: prompt_title,
|
||||
after_completion: true.into(),
|
||||
replace_previous_arguments: true,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let title = arguments.to_owned().join(" ");
|
||||
if title.trim().is_empty() {
|
||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||
};
|
||||
|
||||
let store = PromptStore::global(cx);
|
||||
let title = SharedString::from(title);
|
||||
let prompt = cx.spawn({
|
||||
let title = title.clone();
|
||||
async move |cx| {
|
||||
let store = store.await?;
|
||||
let body = store
|
||||
.read_with(cx, |store, cx| {
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
anyhow::Ok(store.load(prompt_id, cx))
|
||||
})?
|
||||
.await?;
|
||||
anyhow::Ok(body)
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let mut prompt = prompt.await?;
|
||||
|
||||
if prompt.starts_with('/') {
|
||||
// Prevent an edge case where the inserted prompt starts with a slash command (that leads to funky rendering).
|
||||
prompt.insert(0, '\n');
|
||||
}
|
||||
if prompt.is_empty() {
|
||||
prompt.push('\n');
|
||||
}
|
||||
let range = 0..prompt.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text: prompt,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::Library,
|
||||
label: title,
|
||||
metadata: None,
|
||||
}],
|
||||
run_commands_in_text: true,
|
||||
}
|
||||
.into_event_stream())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use editor::{BufferOffset, Editor, MultiBufferSnapshot};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
|
||||
use rope::Point;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use ui::IconName;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::file_command::codeblock_fence_for_path;
|
||||
|
||||
pub struct SelectionCommand;
|
||||
|
||||
impl SlashCommand for SelectionCommand {
|
||||
fn name(&self) -> String {
|
||||
"selection".into()
|
||||
}
|
||||
|
||||
fn label(&self, _cx: &App) -> CodeLabel {
|
||||
CodeLabel::plain(self.name(), None)
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert editor selection".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Quote
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut events = vec![];
|
||||
|
||||
let Some(creases) = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selection_ranges = editor
|
||||
.selections
|
||||
.all_adjusted(&editor.display_snapshot(cx))
|
||||
.iter()
|
||||
.map(|selection| selection.range())
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
Some(selections_creases(selection_ranges, snapshot, cx))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
events.push(Err(e));
|
||||
None
|
||||
})
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("no active selection")));
|
||||
};
|
||||
|
||||
for (text, title) in creases {
|
||||
events.push(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::TextSnippet,
|
||||
label: SharedString::from(title),
|
||||
metadata: None,
|
||||
}));
|
||||
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text,
|
||||
run_commands_in_text: false,
|
||||
})));
|
||||
events.push(Ok(SlashCommandEvent::EndSection));
|
||||
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||
text: "\n".to_string(),
|
||||
run_commands_in_text: false,
|
||||
})));
|
||||
}
|
||||
|
||||
let result = futures::stream::iter(events).boxed();
|
||||
|
||||
Task::ready(Ok(result))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selections_creases(
|
||||
selection_ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<(String, String)> {
|
||||
let mut creases = Vec::new();
|
||||
for range in selection_ranges {
|
||||
let buffer_ranges = snapshot.range_to_buffer_ranges(range.clone());
|
||||
|
||||
if buffer_ranges.is_empty() {
|
||||
creases.extend(crease_for_range(range, &snapshot, cx));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (buffer_snapshot, buffer_range, _excerpt_id) in buffer_ranges {
|
||||
creases.extend(crease_for_buffer_range(buffer_snapshot, buffer_range, cx));
|
||||
}
|
||||
}
|
||||
creases
|
||||
}
|
||||
|
||||
/// Creates a crease for a range within a specific buffer (excerpt).
|
||||
/// This is used when we know the exact buffer and range within it.
|
||||
fn crease_for_buffer_range(
|
||||
buffer: &BufferSnapshot,
|
||||
Range { start, end }: Range<BufferOffset>,
|
||||
cx: &App,
|
||||
) -> Option<(String, String)> {
|
||||
let selected_text: String = buffer.text_for_range(start.0..end.0).collect();
|
||||
|
||||
if selected_text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_point = buffer.offset_to_point(start.0);
|
||||
let end_point = buffer.offset_to_point(end.0);
|
||||
let start_buffer_row = start_point.row;
|
||||
let end_buffer_row = end_point.row;
|
||||
|
||||
let language = buffer.language_at(start.0);
|
||||
let language_name_arc = language.map(|l| l.code_fence_block_name());
|
||||
let language_name = language_name_arc.as_deref().unwrap_or_default();
|
||||
|
||||
let filename = buffer
|
||||
.file()
|
||||
.map(|file| file.full_path(cx).to_string_lossy().into_owned());
|
||||
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = buffer.symbols_containing(start, None);
|
||||
let end_symbols = buffer.symbols_containing(end, None);
|
||||
|
||||
let outline_text = if !start_symbols.is_empty() && !end_symbols.is_empty() {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix =
|
||||
language.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence =
|
||||
codeblock_fence_for_path(filename.as_deref(), Some(start_buffer_row..=end_buffer_row));
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text) {
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
} else {
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = start_buffer_row + 1;
|
||||
let end_line = end_buffer_row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{path}, Line {start_line}")
|
||||
} else {
|
||||
format!("{path}, Lines {start_line} to {end_line}")
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
|
||||
Some((text, crease_title))
|
||||
}
|
||||
|
||||
/// Fallback function to create a crease from a multibuffer range when we can't split by excerpt.
|
||||
fn crease_for_range(
|
||||
range: Range<Point>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
cx: &App,
|
||||
) -> Option<(String, String)> {
|
||||
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
if selected_text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get actual file line numbers (not multibuffer row numbers)
|
||||
let start_buffer_row = snapshot
|
||||
.point_to_buffer_point(range.start)
|
||||
.map(|(_, point, _)| point.row)
|
||||
.unwrap_or(range.start.row);
|
||||
let end_buffer_row = snapshot
|
||||
.point_to_buffer_point(range.end)
|
||||
.map(|(_, point, _)| point.row)
|
||||
.unwrap_or(range.end.row);
|
||||
|
||||
let start_language = snapshot.language_at(range.start);
|
||||
let end_language = snapshot.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
start_language.map(|language| language.code_fence_block_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
|
||||
let filename = snapshot
|
||||
.file_at(range.start)
|
||||
.map(|file| file.full_path(cx).to_string_lossy().into_owned());
|
||||
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = snapshot
|
||||
.symbols_containing(range.start, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
let end_symbols = snapshot
|
||||
.symbols_containing(range.end, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
|
||||
let outline_text =
|
||||
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix =
|
||||
start_language.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence =
|
||||
codeblock_fence_for_path(filename.as_deref(), Some(start_buffer_row..=end_buffer_row));
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text) {
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
} else {
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = start_buffer_row + 1;
|
||||
let end_line = end_buffer_row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{path}, Line {start_line}")
|
||||
} else {
|
||||
format!("{path}, Lines {start_line} to {end_line}")
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
|
||||
Some((text, crease_title))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use multi_buffer::MultiBuffer;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_selections_creases_single_excerpt(cx: &mut TestAppContext) {
|
||||
let buffer = cx.update(|cx| {
|
||||
MultiBuffer::build_multi(
|
||||
[("a\nb\nc\n", vec![Point::new(0, 0)..Point::new(3, 0)])],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let creases = cx.update(|cx| {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
selections_creases(vec![Point::new(0, 0)..Point::new(2, 1)], snapshot, cx)
|
||||
});
|
||||
assert_eq!(creases.len(), 1);
|
||||
assert_eq!(creases[0].0, "```untitled:1-3\na\nb\nc\n```");
|
||||
assert_eq!(creases[0].1, "Quoted selection");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_selections_creases_spans_multiple_excerpts(cx: &mut TestAppContext) {
|
||||
let buffer = cx.update(|cx| {
|
||||
MultiBuffer::build_multi(
|
||||
[
|
||||
("aaa\nbbb\n", vec![Point::new(0, 0)..Point::new(2, 0)]),
|
||||
("111\n222\n", vec![Point::new(0, 0)..Point::new(2, 0)]),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let creases = cx.update(|cx| {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let end = snapshot.offset_to_point(snapshot.len());
|
||||
selections_creases(vec![Point::new(0, 0)..end], snapshot, cx)
|
||||
});
|
||||
assert_eq!(creases.len(), 2);
|
||||
assert!(creases[0].0.contains("aaa") && !creases[0].0.contains("111"));
|
||||
assert!(creases[1].0.contains("111") && !creases[1].0.contains("aaa"));
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use feature_flags::FeatureFlag;
|
||||
use futures::channel::mpsc;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use smol::stream::StreamExt;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct StreamingExampleSlashCommandFeatureFlag;
|
||||
|
||||
impl FeatureFlag for StreamingExampleSlashCommandFeatureFlag {
|
||||
const NAME: &'static str = "streaming-example-slash-command";
|
||||
}
|
||||
|
||||
pub struct StreamingExampleSlashCommand;
|
||||
|
||||
impl SlashCommand for StreamingExampleSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"streaming-example".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"An example slash command that showcases streaming.".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_spawn(async move {
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileRust,
|
||||
label: "Section 1".into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "Hello".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
|
||||
|
||||
executor.timer(Duration::from_secs(1)).await;
|
||||
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileRust,
|
||||
label: "Section 2".into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "World".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
|
||||
|
||||
for n in 1..=10 {
|
||||
executor.timer(Duration::from_secs(1)).await;
|
||||
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::StarFilled,
|
||||
label: format!("Section {n}").into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "lorem ipsum ".repeat(n).trim().into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Task::ready(Ok(events_rx.boxed()))
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext as _, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use ui::{App, IconName, SharedString, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct OutlineSlashCommand;
|
||||
|
||||
impl SlashCommand for OutlineSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"symbols".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert symbols for active tab".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ListTree
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let Some(active_item) = workspace.active_item(cx) else {
|
||||
return Task::ready(Err(anyhow!("no active tab")));
|
||||
};
|
||||
let Some(buffer) = active_item
|
||||
.downcast::<Editor>()
|
||||
.and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("active tab is not an editor")));
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(true, cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let outline = snapshot.outline(None);
|
||||
|
||||
let path = path.as_deref().unwrap_or("untitled");
|
||||
let mut outline_text = format!("Symbols for {path}:\n");
|
||||
for item in &outline.path_candidates {
|
||||
outline_text.push_str("- ");
|
||||
outline_text.push_str(&item.string);
|
||||
outline_text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..outline_text.len(),
|
||||
icon: IconName::ListTree,
|
||||
label: SharedString::new(path),
|
||||
metadata: None,
|
||||
}],
|
||||
text: outline_text,
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.into_event_stream())
|
||||
})
|
||||
});
|
||||
|
||||
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::Editor;
|
||||
use futures::future::join_all;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, CodeLabelBuilder, HighlightId, LspAdapterDelegate};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::{ActiveTheme, App, Window, prelude::*};
|
||||
use util::{ResultExt, paths::PathStyle};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::file_command::append_buffer_to_output;
|
||||
|
||||
pub struct TabSlashCommand;
|
||||
|
||||
const ALL_TABS_COMPLETION_ITEM: &str = "all";
|
||||
|
||||
impl SlashCommand for TabSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"tab".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert open tabs (active tab by default)".to_owned()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileTree
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let mut has_all_tabs_completion_item = false;
|
||||
let argument_set = arguments
|
||||
.iter()
|
||||
.filter(|argument| {
|
||||
if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {
|
||||
has_all_tabs_completion_item = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
if has_all_tabs_completion_item {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no workspace")));
|
||||
};
|
||||
|
||||
let active_item_path = workspace.update(cx, |workspace, cx| {
|
||||
let snapshot = active_item_buffer(workspace, cx).ok()?;
|
||||
snapshot.resolve_file_path(true, cx)
|
||||
});
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
|
||||
let current_query = arguments.last().cloned().unwrap_or_default();
|
||||
let tab_items_search = tab_items_for_queries(
|
||||
Some(workspace.downgrade()),
|
||||
&[current_query],
|
||||
cancel,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
window.spawn(cx, async move |_| {
|
||||
let tab_items = tab_items_search.await?;
|
||||
let run_command = tab_items.len() == 1;
|
||||
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
|
||||
let path = path?;
|
||||
if argument_set.contains(&path) {
|
||||
return None;
|
||||
}
|
||||
if active_item_path.as_ref() == Some(&path) {
|
||||
return None;
|
||||
}
|
||||
let label = create_tab_completion_label(&path, path_style, comment_id);
|
||||
Some(ArgumentCompletion {
|
||||
label,
|
||||
new_text: path,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: run_command.into(),
|
||||
})
|
||||
});
|
||||
|
||||
let active_item_completion = active_item_path
|
||||
.as_deref()
|
||||
.map(|active_item_path| {
|
||||
let path_string = active_item_path.to_string();
|
||||
let label =
|
||||
create_tab_completion_label(active_item_path, path_style, comment_id);
|
||||
ArgumentCompletion {
|
||||
label,
|
||||
new_text: path_string,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: run_command.into(),
|
||||
}
|
||||
})
|
||||
.filter(|completion| !argument_set.contains(&completion.new_text));
|
||||
|
||||
Ok(active_item_completion
|
||||
.into_iter()
|
||||
.chain(Some(ArgumentCompletion {
|
||||
label: ALL_TABS_COMPLETION_ITEM.into(),
|
||||
new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
|
||||
replace_previous_arguments: false,
|
||||
after_completion: true.into(),
|
||||
}))
|
||||
.chain(tab_completion_items)
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let tab_items_search = tab_items_for_queries(
|
||||
Some(workspace),
|
||||
arguments,
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut output = SlashCommandOutput::default();
|
||||
for (full_path, buffer, _) in tab_items_search.await? {
|
||||
append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
|
||||
}
|
||||
Ok(output.into_event_stream())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_items_for_queries(
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
queries: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
strict_match: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<Vec<(Option<String>, BufferSnapshot, usize)>>> {
|
||||
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
|
||||
let queries = queries.to_owned();
|
||||
window.spawn(cx, async move |cx| {
|
||||
let mut open_buffers =
|
||||
workspace
|
||||
.context("no workspace")?
|
||||
.update(cx, |workspace, cx| {
|
||||
if strict_match && empty_query {
|
||||
let snapshot = active_item_buffer(workspace, cx)?;
|
||||
let full_path = snapshot.resolve_file_path(true, cx);
|
||||
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
|
||||
}
|
||||
|
||||
let mut timestamps_by_entity_id = HashMap::default();
|
||||
let mut visited_buffers = HashSet::default();
|
||||
let mut open_buffers = Vec::new();
|
||||
|
||||
for pane in workspace.panes() {
|
||||
let pane = pane.read(cx);
|
||||
for entry in pane.activation_history() {
|
||||
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
for editor in workspace.items_of_type::<Editor>(cx) {
|
||||
if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
|
||||
&& let Some(timestamp) =
|
||||
timestamps_by_entity_id.get(&editor.entity_id())
|
||||
&& visited_buffers.insert(buffer.read(cx).remote_id())
|
||||
{
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let full_path = snapshot.resolve_file_path(true, cx);
|
||||
open_buffers.push((full_path, snapshot, *timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(open_buffers)
|
||||
})??;
|
||||
|
||||
let background_executor = cx.background_executor().clone();
|
||||
cx.background_spawn(async move {
|
||||
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
|
||||
if empty_query
|
||||
|| queries
|
||||
.iter()
|
||||
.any(|query| query == ALL_TABS_COMPLETION_ITEM)
|
||||
{
|
||||
return Ok(open_buffers);
|
||||
}
|
||||
|
||||
let matched_items = if strict_match {
|
||||
let match_candidates = open_buffers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| Some((id, full_path.clone()?)))
|
||||
.fold(HashMap::default(), |mut candidates, (id, path_string)| {
|
||||
candidates
|
||||
.entry(path_string)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(id);
|
||||
candidates
|
||||
});
|
||||
|
||||
queries
|
||||
.iter()
|
||||
.filter_map(|query| match_candidates.get(query))
|
||||
.flatten()
|
||||
.copied()
|
||||
.filter_map(|id| open_buffers.get(id))
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
let match_candidates = open_buffers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
Some(fuzzy::StringMatchCandidate::new(id, full_path.as_ref()?))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut processed_matches = HashSet::default();
|
||||
let file_queries = queries.iter().map(|query| {
|
||||
fuzzy::match_strings(
|
||||
&match_candidates,
|
||||
query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&cancel,
|
||||
background_executor.clone(),
|
||||
)
|
||||
});
|
||||
|
||||
join_all(file_queries)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|string_match| processed_matches.insert(string_match.candidate_id))
|
||||
.filter_map(|string_match| open_buffers.get(string_match.candidate_id))
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
Ok(matched_items)
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn active_item_buffer(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> anyhow::Result<BufferSnapshot> {
|
||||
let active_editor = workspace
|
||||
.active_item(cx)
|
||||
.context("no active item")?
|
||||
.downcast::<Editor>()
|
||||
.context("active item is not an editor")?;
|
||||
let snapshot = active_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.context("active editor is not a singleton buffer")?
|
||||
.read(cx)
|
||||
.snapshot();
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn create_tab_completion_label(
|
||||
path: &str,
|
||||
path_style: PathStyle,
|
||||
comment_id: Option<HighlightId>,
|
||||
) -> CodeLabel {
|
||||
let (parent_path, file_name) = path_style.split(path);
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
label.push_str(file_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(parent_path.unwrap_or_default(), comment_id);
|
||||
label.respan_filter_range(Some(file_name));
|
||||
label.build()
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
[package]
|
||||
name = "assistant_text_thread"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_text_thread.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
open_ai.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
regex.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
text.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant_slash_commands.workspace = true
|
||||
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
unindent.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,16 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod assistant_text_thread_tests;
|
||||
mod context_server_command;
|
||||
mod text_thread;
|
||||
mod text_thread_store;
|
||||
|
||||
pub use crate::text_thread::*;
|
||||
pub use crate::text_thread_store::*;
|
||||
|
||||
use client::Client;
|
||||
use gpui::App;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn init(client: Arc<Client>, _: &mut App) {
|
||||
text_thread_store::init(&client.into());
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,251 +0,0 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use context_server::{ContextServerId, types::Prompt};
|
||||
use gpui::{App, Entity, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use project::context_server_store::ContextServerStore;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use text::LineEnding;
|
||||
use ui::{IconName, SharedString};
|
||||
use workspace::Workspace;
|
||||
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
|
||||
pub struct ContextServerSlashCommand {
|
||||
store: Entity<ContextServerStore>,
|
||||
server_id: ContextServerId,
|
||||
prompt: Prompt,
|
||||
}
|
||||
|
||||
impl ContextServerSlashCommand {
|
||||
pub fn new(store: Entity<ContextServerStore>, id: ContextServerId, prompt: Prompt) -> Self {
|
||||
Self {
|
||||
server_id: id,
|
||||
prompt,
|
||||
store,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for ContextServerSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
self.prompt.name.clone()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &App) -> language::CodeLabel {
|
||||
let mut parts = vec![self.prompt.name.as_str()];
|
||||
if let Some(args) = &self.prompt.arguments
|
||||
&& let Some(arg) = args.first()
|
||||
{
|
||||
parts.push(arg.name.as_str());
|
||||
}
|
||||
create_label_for_command(parts[0], &parts[1..], cx)
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
match &self.prompt.description {
|
||||
Some(desc) => desc.clone(),
|
||||
None => format!("Run '{}' from {}", self.prompt.name, self.server_id),
|
||||
}
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
match &self.prompt.description {
|
||||
Some(desc) => desc.clone(),
|
||||
None => format!("Run '{}' from {}", self.prompt.name, self.server_id),
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
self.prompt
|
||||
.arguments
|
||||
.as_ref()
|
||||
.is_some_and(|args| args.iter().any(|arg| arg.required == Some(true)))
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
|
||||
return Task::ready(Err(anyhow!("Failed to complete argument")));
|
||||
};
|
||||
|
||||
let server_id = self.server_id.clone();
|
||||
let prompt_name = self.prompt.name.clone();
|
||||
|
||||
if let Some(server) = self.store.read(cx).get_running_server(&server_id) {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let protocol = server.client().context("Context server not initialized")?;
|
||||
|
||||
let response = protocol
|
||||
.request::<context_server::types::requests::CompletionComplete>(
|
||||
context_server::types::CompletionCompleteParams {
|
||||
reference: context_server::types::CompletionReference::Prompt(
|
||||
context_server::types::PromptReference {
|
||||
ty: context_server::types::PromptReferenceType::Prompt,
|
||||
name: prompt_name,
|
||||
},
|
||||
),
|
||||
argument: context_server::types::CompletionArgument {
|
||||
name: arg_name,
|
||||
value: arg_value,
|
||||
},
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let completions = response
|
||||
.completion
|
||||
.values
|
||||
.into_iter()
|
||||
.map(|value| ArgumentCompletion {
|
||||
label: CodeLabel::plain(value.clone(), None),
|
||||
new_text: value,
|
||||
after_completion: AfterCompletion::Continue,
|
||||
replace_previous_arguments: false,
|
||||
})
|
||||
.collect();
|
||||
Ok(completions)
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("Context server not found")))
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let server_id = self.server_id.clone();
|
||||
let prompt_name = self.prompt.name.clone();
|
||||
|
||||
let prompt_args = match prompt_arguments(&self.prompt, arguments) {
|
||||
Ok(args) => args,
|
||||
Err(e) => return Task::ready(Err(e)),
|
||||
};
|
||||
|
||||
let store = self.store.read(cx);
|
||||
if let Some(server) = store.get_running_server(&server_id) {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let protocol = server.client().context("Context server not initialized")?;
|
||||
let response = protocol
|
||||
.request::<context_server::types::requests::PromptsGet>(
|
||||
context_server::types::PromptsGetParams {
|
||||
name: prompt_name.clone(),
|
||||
arguments: Some(prompt_args),
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
response
|
||||
.messages
|
||||
.iter()
|
||||
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
|
||||
"Prompt contains non-user roles, which is not supported"
|
||||
);
|
||||
|
||||
// Extract text from user messages into a single prompt string
|
||||
let mut prompt = response
|
||||
.messages
|
||||
.into_iter()
|
||||
.filter_map(|msg| match msg.content {
|
||||
context_server::types::MessageContent::Text { text, .. } => Some(text),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n");
|
||||
|
||||
// We must normalize the line endings here, since servers might return CR characters.
|
||||
LineEnding::normalize(&mut prompt);
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..(prompt.len()),
|
||||
icon: IconName::ZedAssistant,
|
||||
label: SharedString::from(
|
||||
response
|
||||
.description
|
||||
.unwrap_or(format!("Result from {}", prompt_name)),
|
||||
),
|
||||
metadata: None,
|
||||
}],
|
||||
text: prompt,
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.into_event_stream())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("Context server not found")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> {
|
||||
anyhow::ensure!(!arguments.is_empty(), "No arguments given");
|
||||
|
||||
match &prompt.arguments {
|
||||
Some(args) if args.len() == 1 => {
|
||||
let arg_name = args[0].name.clone();
|
||||
let arg_value = arguments.join(" ");
|
||||
Ok((arg_name, arg_value))
|
||||
}
|
||||
Some(_) => anyhow::bail!("Prompt must have exactly one argument"),
|
||||
None => anyhow::bail!("Prompt has no arguments"),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<String, String>> {
|
||||
match &prompt.arguments {
|
||||
Some(args) if args.len() > 1 => {
|
||||
anyhow::bail!("Prompt has more than one argument, which is not supported");
|
||||
}
|
||||
Some(args) if args.len() == 1 => {
|
||||
if !arguments.is_empty() {
|
||||
let mut map = HashMap::default();
|
||||
map.insert(args[0].name.clone(), arguments.join(" "));
|
||||
Ok(map)
|
||||
} else if arguments.is_empty() && args[0].required == Some(false) {
|
||||
Ok(HashMap::default())
|
||||
} else {
|
||||
anyhow::bail!("Prompt expects argument but none given");
|
||||
}
|
||||
}
|
||||
Some(_) | None => {
|
||||
anyhow::ensure!(
|
||||
arguments.is_empty(),
|
||||
"Prompt expects no arguments but some were given"
|
||||
);
|
||||
Ok(HashMap::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP servers can return prompts with multiple arguments. Since we only
|
||||
/// support one argument, we ignore all others. This is the necessary predicate
|
||||
/// for this.
|
||||
pub fn acceptable_prompt(prompt: &Prompt) -> bool {
|
||||
match &prompt.arguments {
|
||||
None => true,
|
||||
Some(args) if args.len() <= 1 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -75,11 +75,6 @@ uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
|
||||
|
||||
|
||||
assistant_text_thread.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
buffer_diff.workspace = true
|
||||
|
||||
@@ -410,9 +410,6 @@ impl Server {
|
||||
.add_message_handler(update_followers)
|
||||
.add_message_handler(acknowledge_channel_message)
|
||||
.add_message_handler(acknowledge_buffer_version)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Stash>)
|
||||
@@ -442,8 +439,6 @@ impl Server {
|
||||
.add_request_handler(disallow_guest_request::<proto::GitRemoveWorktree>)
|
||||
.add_request_handler(disallow_guest_request::<proto::GitRenameWorktree>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>)
|
||||
.add_request_handler(share_agent_thread)
|
||||
@@ -2372,48 +2367,6 @@ async fn update_buffer(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_context(message: proto::UpdateContext, session: MessageContext) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(message.project_id);
|
||||
|
||||
let operation = message.operation.as_ref().context("invalid operation")?;
|
||||
let capability = match operation.variant.as_ref() {
|
||||
Some(proto::context_operation::Variant::BufferOperation(buffer_op)) => {
|
||||
if let Some(buffer_op) = buffer_op.operation.as_ref() {
|
||||
match buffer_op.variant {
|
||||
None | Some(proto::operation::Variant::UpdateSelections(_)) => {
|
||||
Capability::ReadOnly
|
||||
}
|
||||
_ => Capability::ReadWrite,
|
||||
}
|
||||
} else {
|
||||
Capability::ReadWrite
|
||||
}
|
||||
}
|
||||
Some(_) => Capability::ReadWrite,
|
||||
None => Capability::ReadOnly,
|
||||
};
|
||||
|
||||
let guard = session
|
||||
.db()
|
||||
.await
|
||||
.connections_for_buffer_update(project_id, session.connection_id, capability)
|
||||
.await?;
|
||||
|
||||
let (host, guests) = &*guard;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
guests.iter().chain([host]).copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, message.clone())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn forward_project_search_chunk(
|
||||
message: proto::FindSearchCandidatesChunk,
|
||||
response: Response<proto::FindSearchCandidatesChunk>,
|
||||
|
||||
@@ -3,8 +3,6 @@ use crate::{
|
||||
room_participants,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_text_thread::TextThreadStore;
|
||||
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks};
|
||||
use call::{ActiveCall, Room, room};
|
||||
use client::{RECEIVE_TIMEOUT, User};
|
||||
@@ -34,7 +32,6 @@ use project::{
|
||||
lsp_store::{FormatTrigger, LspFormatTarget, SymbolLocation},
|
||||
search::{SearchQuery, SearchResult},
|
||||
};
|
||||
use prompt_store::PromptBuilder;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
|
||||
@@ -7095,141 +7092,6 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_context_collaboration_with_reconnect(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a.fs().insert_tree("/a", Default::default()).await;
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Client A sees that a guest has joined.
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let text_thread_store_a = cx_a
|
||||
.update(|cx| {
|
||||
TextThreadStore::new(
|
||||
project_a.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let text_thread_store_b = cx_b
|
||||
.update(|cx| {
|
||||
TextThreadStore::new(
|
||||
project_b.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A creates a new chats.
|
||||
let text_thread_a = text_thread_store_a.update(cx_a, |store, cx| store.create(cx));
|
||||
executor.run_until_parked();
|
||||
|
||||
// Client B retrieves host's contexts and joins one.
|
||||
let text_thread_b = text_thread_store_b
|
||||
.update(cx_b, |store, cx| {
|
||||
let host_text_threads = store.host_text_threads().collect::<Vec<_>>();
|
||||
assert_eq!(host_text_threads.len(), 1);
|
||||
store.open_remote(host_text_threads[0].id.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Host and guest make changes
|
||||
text_thread_a.update(cx_a, |text_thread, cx| {
|
||||
text_thread.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Host change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
text_thread_b.update(cx_b, |text_thread, cx| {
|
||||
text_thread.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Guest change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
|
||||
"Guest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
|
||||
"Guest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Disconnect client A and make some changes while disconnected.
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
server.forbid_connections();
|
||||
text_thread_a.update(cx_a, |text_thread, cx| {
|
||||
text_thread.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Host offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
text_thread_b.update(cx_b, |text_thread, cx| {
|
||||
text_thread.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
|
||||
"Host offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
|
||||
"Guest offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Allow client A to reconnect and verify that contexts converge.
|
||||
server.allow_connections();
|
||||
executor.advance_clock(RECEIVE_TIMEOUT);
|
||||
assert_eq!(
|
||||
text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
|
||||
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
|
||||
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Client A disconnects without being able to reconnect. Context B becomes readonly.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
text_thread_b.read_with(cx_b, |text_thread, cx| {
|
||||
assert!(text_thread.buffer().read(cx).read_only());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_remote_git_branches(
|
||||
executor: BackgroundExecutor,
|
||||
|
||||
@@ -356,7 +356,6 @@ impl TestServer {
|
||||
settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(),
|
||||
);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
assistant_text_thread::init(client.clone(), cx);
|
||||
});
|
||||
|
||||
client
|
||||
|
||||
@@ -543,9 +543,9 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
|
||||
/// This will return the action name unmodified.
|
||||
///
|
||||
/// ```
|
||||
/// let action_as_str = "assistant::Assist";
|
||||
/// let action_as_str = "workspace::Save";
|
||||
/// let action_name = name_for_action(action_as_str);
|
||||
/// assert_eq!(action_name, "assistant::Assist");
|
||||
/// assert_eq!(action_name, "workspace::Save");
|
||||
/// ```
|
||||
///
|
||||
/// This will return the action name with any trailing options removed.
|
||||
|
||||
@@ -118,7 +118,6 @@ pub fn init(cx: &mut App) -> Arc<AgentCliAppState> {
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
|
||||
agent_ui::init(
|
||||
fs.clone(),
|
||||
client.clone(),
|
||||
prompt_builder,
|
||||
languages.clone(),
|
||||
true,
|
||||
|
||||
@@ -8,7 +8,7 @@ use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::{Extension, SlashCommand};
|
||||
use crate::Extension;
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalExtensionHostProxy(Arc<ExtensionHostProxy>);
|
||||
@@ -29,7 +29,6 @@ pub struct ExtensionHostProxy {
|
||||
language_proxy: RwLock<Option<Arc<dyn ExtensionLanguageProxy>>>,
|
||||
language_server_proxy: RwLock<Option<Arc<dyn ExtensionLanguageServerProxy>>>,
|
||||
snippet_proxy: RwLock<Option<Arc<dyn ExtensionSnippetProxy>>>,
|
||||
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
|
||||
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
|
||||
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
|
||||
language_model_provider_proxy: RwLock<Option<Arc<dyn ExtensionLanguageModelProviderProxy>>>,
|
||||
@@ -55,7 +54,6 @@ impl ExtensionHostProxy {
|
||||
language_proxy: RwLock::default(),
|
||||
language_server_proxy: RwLock::default(),
|
||||
snippet_proxy: RwLock::default(),
|
||||
slash_command_proxy: RwLock::default(),
|
||||
context_server_proxy: RwLock::default(),
|
||||
debug_adapter_provider_proxy: RwLock::default(),
|
||||
language_model_provider_proxy: RwLock::default(),
|
||||
@@ -82,10 +80,6 @@ impl ExtensionHostProxy {
|
||||
self.snippet_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) {
|
||||
self.slash_command_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) {
|
||||
self.context_server_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
@@ -356,30 +350,6 @@ impl ExtensionSnippetProxy for ExtensionHostProxy {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionSlashCommandProxy: Send + Sync + 'static {
|
||||
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand);
|
||||
|
||||
fn unregister_slash_command(&self, command_name: Arc<str>);
|
||||
}
|
||||
|
||||
impl ExtensionSlashCommandProxy for ExtensionHostProxy {
|
||||
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand) {
|
||||
let Some(proxy) = self.slash_command_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_slash_command(extension, command)
|
||||
}
|
||||
|
||||
fn unregister_slash_command(&self, command_name: Arc<str>) {
|
||||
let Some(proxy) = self.slash_command_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.unregister_slash_command(command_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionContextServerProxy: Send + Sync + 'static {
|
||||
fn register_context_server(
|
||||
&self,
|
||||
|
||||
@@ -17,8 +17,7 @@ use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::{
|
||||
ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents,
|
||||
ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy,
|
||||
ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy,
|
||||
ExtensionThemeProxy,
|
||||
ExtensionLanguageServerProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
|
||||
};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::future::join_all;
|
||||
@@ -1209,9 +1208,6 @@ impl ExtensionStore {
|
||||
for locator in extension.manifest.debug_locators.keys() {
|
||||
self.proxy.unregister_debug_locator(locator.clone());
|
||||
}
|
||||
for command_name in extension.manifest.slash_commands.keys() {
|
||||
self.proxy.unregister_slash_command(command_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.wasm_extensions
|
||||
@@ -1430,21 +1426,6 @@ impl ExtensionStore {
|
||||
}
|
||||
}
|
||||
|
||||
for (slash_command_name, slash_command) in &manifest.slash_commands {
|
||||
this.proxy.register_slash_command(
|
||||
extension.clone(),
|
||||
extension::SlashCommand {
|
||||
name: slash_command_name.to_string(),
|
||||
description: slash_command.description.to_string(),
|
||||
// We don't currently expose this as a configurable option, as it currently drives
|
||||
// the `menu_text` on the `SlashCommand` trait, which is not used for slash commands
|
||||
// defined in extensions, as they are not able to be added to the menu.
|
||||
tooltip_text: String::new(),
|
||||
requires_argument: slash_command.requires_argument,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for id in manifest.context_servers.keys() {
|
||||
this.proxy
|
||||
.register_context_server(extension.clone(), id.clone(), cx);
|
||||
|
||||
@@ -69,10 +69,6 @@ pub fn init(cx: &mut App) {
|
||||
ExtensionProvides::ContextServers
|
||||
}
|
||||
ExtensionCategoryFilter::AgentServers => ExtensionProvides::AgentServers,
|
||||
ExtensionCategoryFilter::SlashCommands => ExtensionProvides::SlashCommands,
|
||||
ExtensionCategoryFilter::IndexedDocsProviders => {
|
||||
ExtensionProvides::IndexedDocsProviders
|
||||
}
|
||||
ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets,
|
||||
ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters,
|
||||
});
|
||||
|
||||
@@ -236,7 +236,6 @@ pub enum IconName {
|
||||
Terminal,
|
||||
TerminalAlt,
|
||||
TextSnippet,
|
||||
TextThread,
|
||||
ThinkingMode,
|
||||
ThinkingModeOff,
|
||||
Thread,
|
||||
|
||||
@@ -472,9 +472,6 @@ pub struct EditPredictionSettings {
|
||||
/// Settings specific to Ollama.
|
||||
pub ollama: Option<OpenAiCompatibleEditPredictionSettings>,
|
||||
pub open_ai_compatible_api: Option<OpenAiCompatibleEditPredictionSettings>,
|
||||
/// Whether edit predictions are enabled in the assistant panel.
|
||||
/// This setting has no effect if globally disabled.
|
||||
pub enabled_in_text_threads: bool,
|
||||
pub examples_dir: Option<Arc<Path>>,
|
||||
}
|
||||
|
||||
@@ -820,8 +817,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
prompt_format: openai_compatible_settings.prompt_format.unwrap(),
|
||||
});
|
||||
|
||||
let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
|
||||
|
||||
let mut file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)> = FxHashMap::default();
|
||||
|
||||
for (language, patterns) in all_languages.file_types.iter().flatten() {
|
||||
@@ -859,7 +854,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
codestral: codestral_settings,
|
||||
ollama: ollama_settings,
|
||||
open_ai_compatible_api: openai_compatible_settings,
|
||||
enabled_in_text_threads,
|
||||
examples_dir: edit_predictions.examples_dir,
|
||||
},
|
||||
defaults: default_language_settings,
|
||||
|
||||
@@ -34,7 +34,6 @@ log.workspace = true
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
open_router.workspace = true
|
||||
parking_lot.workspace = true
|
||||
proto.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -857,16 +857,6 @@ pub enum ConfigurationViewTargetAgent {
|
||||
Other(SharedString),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum LanguageModelProviderTosView {
|
||||
/// When there are some past interactions in the Agent Panel.
|
||||
ThreadEmptyState,
|
||||
/// When there are no past interactions in the Agent Panel.
|
||||
ThreadFreshStart,
|
||||
TextThreadPopup,
|
||||
Configuration,
|
||||
}
|
||||
|
||||
pub trait LanguageModelProviderState: 'static {
|
||||
type ObservableEntity;
|
||||
|
||||
|
||||
@@ -10,23 +10,6 @@ pub enum Role {
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn from_proto(role: i32) -> Role {
|
||||
match proto::LanguageModelRole::from_i32(role) {
|
||||
Some(proto::LanguageModelRole::LanguageModelUser) => Role::User,
|
||||
Some(proto::LanguageModelRole::LanguageModelAssistant) => Role::Assistant,
|
||||
Some(proto::LanguageModelRole::LanguageModelSystem) => Role::System,
|
||||
None => Role::User,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(self) -> proto::LanguageModelRole {
|
||||
match self {
|
||||
Role::User => proto::LanguageModelRole::LanguageModelUser,
|
||||
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
|
||||
Role::System => proto::LanguageModelRole::LanguageModelSystem,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(self) -> Role {
|
||||
match self {
|
||||
Role::User => Role::Assistant,
|
||||
|
||||
@@ -316,3 +316,9 @@ pub(crate) mod m_2026_03_23 {
|
||||
|
||||
pub(crate) use keymap::KEYMAP_PATTERNS;
|
||||
}
|
||||
|
||||
pub(crate) mod m_2026_03_31 {
|
||||
mod settings;
|
||||
|
||||
pub(crate) use settings::remove_text_thread_settings;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::migrations::migrate_settings;
|
||||
|
||||
pub fn remove_text_thread_settings(value: &mut Value) -> Result<()> {
|
||||
migrate_settings(value, &mut migrate_one)
|
||||
}
|
||||
|
||||
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
|
||||
// Remove `agent.default_view`
|
||||
if let Some(agent) = obj.get_mut("agent") {
|
||||
if let Some(agent_obj) = agent.as_object_mut() {
|
||||
agent_obj.remove("default_view");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove `edit_predictions.enabled_in_text_threads`
|
||||
if let Some(edit_predictions) = obj.get_mut("edit_predictions") {
|
||||
if let Some(edit_predictions_obj) = edit_predictions.as_object_mut() {
|
||||
edit_predictions_obj.remove("enabled_in_text_threads");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove top-level `slash_commands`
|
||||
obj.remove("slash_commands");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -247,6 +247,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
|
||||
migrations::m_2026_03_16::SETTINGS_PATTERNS,
|
||||
&SETTINGS_QUERY_2026_03_16,
|
||||
),
|
||||
MigrationType::Json(migrations::m_2026_03_31::remove_text_thread_settings),
|
||||
];
|
||||
run_migrations(text, migrations)
|
||||
}
|
||||
@@ -940,8 +941,7 @@ mod tests {
|
||||
"foo": "bar"
|
||||
},
|
||||
"edit_predictions": {
|
||||
"enabled_in_text_threads": false,
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
);
|
||||
@@ -4480,4 +4480,109 @@ mod tests {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_text_thread_settings() {
|
||||
assert_migrate_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2026_03_31::remove_text_thread_settings,
|
||||
)],
|
||||
r#"{
|
||||
"agent": {
|
||||
"default_model": {
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet"
|
||||
},
|
||||
"default_view": "text_thread"
|
||||
},
|
||||
"edit_predictions": {
|
||||
"mode": "eager",
|
||||
"enabled_in_text_threads": true
|
||||
},
|
||||
"slash_commands": {
|
||||
"cargo_workspace": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
Some(
|
||||
r#"{
|
||||
"agent": {
|
||||
"default_model": {
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet"
|
||||
}
|
||||
},
|
||||
"edit_predictions": {
|
||||
"mode": "eager"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_text_thread_settings_only_default_view() {
|
||||
assert_migrate_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2026_03_31::remove_text_thread_settings,
|
||||
)],
|
||||
r#"{
|
||||
"agent": {
|
||||
"default_model": "claude-sonnet",
|
||||
"default_view": "thread"
|
||||
}
|
||||
}"#,
|
||||
Some(
|
||||
r#"{
|
||||
"agent": {
|
||||
"default_model": "claude-sonnet"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_text_thread_settings_only_slash_commands() {
|
||||
assert_migrate_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2026_03_31::remove_text_thread_settings,
|
||||
)],
|
||||
r#"{
|
||||
"slash_commands": {
|
||||
"cargo_workspace": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"vim_mode": true
|
||||
}"#,
|
||||
Some(
|
||||
r#"{
|
||||
"vim_mode": true
|
||||
}"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_text_thread_settings_none_present() {
|
||||
assert_migrate_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2026_03_31::remove_text_thread_settings,
|
||||
)],
|
||||
r#"{
|
||||
"agent": {
|
||||
"default_model": {
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet"
|
||||
}
|
||||
},
|
||||
"edit_predictions": {
|
||||
"mode": "eager"
|
||||
}
|
||||
}"#,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -310,30 +310,6 @@ pub fn snippets_dir() -> &'static PathBuf {
|
||||
SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets"))
|
||||
}
|
||||
|
||||
// Returns old path to contexts directory.
|
||||
// Fallback
|
||||
fn text_threads_dir_fallback() -> &'static PathBuf {
|
||||
static CONTEXTS_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
CONTEXTS_DIR.get_or_init(|| {
|
||||
if cfg!(target_os = "macos") {
|
||||
config_dir().join("conversations")
|
||||
} else {
|
||||
data_dir().join("conversations")
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Returns the path to the contexts directory.
|
||||
///
|
||||
/// This is where the saved contexts from the Assistant are stored.
|
||||
pub fn text_threads_dir() -> &'static PathBuf {
|
||||
let fallback_dir = text_threads_dir_fallback();
|
||||
if fallback_dir.exists() {
|
||||
return fallback_dir;
|
||||
}
|
||||
static CONTEXTS_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
CONTEXTS_DIR.get_or_init(|| state_dir().join("conversations"))
|
||||
}
|
||||
|
||||
/// Returns the path to the contexts directory.
|
||||
///
|
||||
/// This is where the prompts for use with the Assistant are stored.
|
||||
|
||||
@@ -1,171 +1,8 @@
|
||||
syntax = "proto3";
|
||||
package zed.messages;
|
||||
|
||||
import "buffer.proto";
|
||||
import "task.proto";
|
||||
|
||||
message Context {
|
||||
repeated ContextOperation operations = 1;
|
||||
}
|
||||
|
||||
message ContextMetadata {
|
||||
string context_id = 1;
|
||||
optional string summary = 2;
|
||||
}
|
||||
|
||||
message ContextMessageStatus {
|
||||
oneof variant {
|
||||
Done done = 1;
|
||||
Pending pending = 2;
|
||||
Error error = 3;
|
||||
Canceled canceled = 4;
|
||||
}
|
||||
|
||||
message Done {}
|
||||
|
||||
message Pending {}
|
||||
|
||||
message Error {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message Canceled {}
|
||||
}
|
||||
|
||||
message ContextMessage {
|
||||
LamportTimestamp id = 1;
|
||||
Anchor start = 2;
|
||||
LanguageModelRole role = 3;
|
||||
ContextMessageStatus status = 4;
|
||||
}
|
||||
|
||||
message SlashCommandOutputSection {
|
||||
AnchorRange range = 1;
|
||||
string icon_name = 2;
|
||||
string label = 3;
|
||||
optional string metadata = 4;
|
||||
}
|
||||
|
||||
message ThoughtProcessOutputSection {
|
||||
AnchorRange range = 1;
|
||||
}
|
||||
|
||||
message ContextOperation {
|
||||
oneof variant {
|
||||
InsertMessage insert_message = 1;
|
||||
UpdateMessage update_message = 2;
|
||||
UpdateSummary update_summary = 3;
|
||||
BufferOperation buffer_operation = 5;
|
||||
SlashCommandStarted slash_command_started = 6;
|
||||
SlashCommandOutputSectionAdded slash_command_output_section_added = 7;
|
||||
SlashCommandCompleted slash_command_completed = 8;
|
||||
ThoughtProcessOutputSectionAdded thought_process_output_section_added = 9;
|
||||
}
|
||||
|
||||
reserved 4;
|
||||
|
||||
message InsertMessage {
|
||||
ContextMessage message = 1;
|
||||
repeated VectorClockEntry version = 2;
|
||||
}
|
||||
|
||||
message UpdateMessage {
|
||||
LamportTimestamp message_id = 1;
|
||||
LanguageModelRole role = 2;
|
||||
ContextMessageStatus status = 3;
|
||||
LamportTimestamp timestamp = 4;
|
||||
repeated VectorClockEntry version = 5;
|
||||
}
|
||||
|
||||
message UpdateSummary {
|
||||
string summary = 1;
|
||||
bool done = 2;
|
||||
LamportTimestamp timestamp = 3;
|
||||
repeated VectorClockEntry version = 4;
|
||||
}
|
||||
|
||||
message SlashCommandStarted {
|
||||
LamportTimestamp id = 1;
|
||||
AnchorRange output_range = 2;
|
||||
string name = 3;
|
||||
repeated VectorClockEntry version = 4;
|
||||
}
|
||||
|
||||
message SlashCommandOutputSectionAdded {
|
||||
LamportTimestamp timestamp = 1;
|
||||
SlashCommandOutputSection section = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
}
|
||||
|
||||
message SlashCommandCompleted {
|
||||
LamportTimestamp id = 1;
|
||||
LamportTimestamp timestamp = 3;
|
||||
optional string error_message = 4;
|
||||
repeated VectorClockEntry version = 5;
|
||||
}
|
||||
|
||||
message ThoughtProcessOutputSectionAdded {
|
||||
LamportTimestamp timestamp = 1;
|
||||
ThoughtProcessOutputSection section = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
}
|
||||
|
||||
message BufferOperation {
|
||||
Operation operation = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message AdvertiseContexts {
|
||||
uint64 project_id = 1;
|
||||
repeated ContextMetadata contexts = 2;
|
||||
}
|
||||
|
||||
message OpenContext {
|
||||
uint64 project_id = 1;
|
||||
string context_id = 2;
|
||||
}
|
||||
|
||||
message OpenContextResponse {
|
||||
Context context = 1;
|
||||
}
|
||||
|
||||
message CreateContext {
|
||||
uint64 project_id = 1;
|
||||
}
|
||||
|
||||
message CreateContextResponse {
|
||||
string context_id = 1;
|
||||
Context context = 2;
|
||||
}
|
||||
|
||||
message UpdateContext {
|
||||
uint64 project_id = 1;
|
||||
string context_id = 2;
|
||||
ContextOperation operation = 3;
|
||||
}
|
||||
|
||||
message ContextVersion {
|
||||
string context_id = 1;
|
||||
repeated VectorClockEntry context_version = 2;
|
||||
repeated VectorClockEntry buffer_version = 3;
|
||||
}
|
||||
|
||||
message SynchronizeContexts {
|
||||
uint64 project_id = 1;
|
||||
repeated ContextVersion contexts = 2;
|
||||
}
|
||||
|
||||
message SynchronizeContextsResponse {
|
||||
repeated ContextVersion contexts = 1;
|
||||
}
|
||||
|
||||
enum LanguageModelRole {
|
||||
LanguageModelUser = 0;
|
||||
LanguageModelAssistant = 1;
|
||||
LanguageModelSystem = 2;
|
||||
reserved 3;
|
||||
}
|
||||
|
||||
message GetAgentServerCommand {
|
||||
uint64 project_id = 1;
|
||||
string name = 2;
|
||||
|
||||
@@ -370,9 +370,10 @@ message View {
|
||||
oneof variant {
|
||||
Editor editor = 3;
|
||||
ChannelView channel_view = 4;
|
||||
ContextEditor context_editor = 5;
|
||||
}
|
||||
|
||||
reserved 5;
|
||||
|
||||
message Editor {
|
||||
bool singleton = 1;
|
||||
optional string title = 2;
|
||||
@@ -390,11 +391,6 @@ message View {
|
||||
uint64 channel_id = 1;
|
||||
Editor editor = 2;
|
||||
}
|
||||
|
||||
message ContextEditor {
|
||||
string context_id = 1;
|
||||
Editor editor = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ExcerptInsertion {
|
||||
|
||||
@@ -223,15 +223,6 @@ message Envelope {
|
||||
LinkedEditingRange linked_editing_range = 209;
|
||||
LinkedEditingRangeResponse linked_editing_range_response = 210;
|
||||
|
||||
AdvertiseContexts advertise_contexts = 211;
|
||||
OpenContext open_context = 212;
|
||||
OpenContextResponse open_context_response = 213;
|
||||
CreateContext create_context = 232;
|
||||
CreateContextResponse create_context_response = 233;
|
||||
UpdateContext update_context = 214;
|
||||
SynchronizeContexts synchronize_contexts = 215;
|
||||
SynchronizeContextsResponse synchronize_contexts_response = 216;
|
||||
|
||||
GetSignatureHelp get_signature_help = 217;
|
||||
GetSignatureHelpResponse get_signature_help_response = 218;
|
||||
|
||||
@@ -502,6 +493,7 @@ message Envelope {
|
||||
reserved 332 to 333;
|
||||
reserved 394 to 396;
|
||||
reserved 429 to 430;
|
||||
reserved 211 to 216, 232 to 233;
|
||||
}
|
||||
|
||||
message Hello {
|
||||
|
||||
@@ -32,7 +32,6 @@ messages!(
|
||||
(AddProjectCollaborator, Foreground),
|
||||
(AddWorktree, Foreground),
|
||||
(AddWorktreeResponse, Foreground),
|
||||
(AdvertiseContexts, Foreground),
|
||||
(AllocateWorktreeId, Foreground),
|
||||
(AllocateWorktreeIdResponse, Foreground),
|
||||
(ApplyCodeAction, Background),
|
||||
@@ -58,8 +57,6 @@ messages!(
|
||||
(CreateFileForPeer, Foreground),
|
||||
(CreateChannel, Foreground),
|
||||
(CreateChannelResponse, Foreground),
|
||||
(CreateContext, Foreground),
|
||||
(CreateContextResponse, Foreground),
|
||||
(CreateProjectEntry, Foreground),
|
||||
(CreateRoom, Foreground),
|
||||
(CreateRoomResponse, Foreground),
|
||||
@@ -191,8 +188,6 @@ messages!(
|
||||
(OpenBufferResponse, Background),
|
||||
(OpenImageResponse, Background),
|
||||
(OpenCommitMessageBuffer, Background),
|
||||
(OpenContext, Foreground),
|
||||
(OpenContextResponse, Foreground),
|
||||
(OpenNewBuffer, Foreground),
|
||||
(OpenServerSettings, Foreground),
|
||||
(PerformRename, Background),
|
||||
@@ -258,8 +253,6 @@ messages!(
|
||||
(ToggleBreakpoint, Foreground),
|
||||
(SynchronizeBuffers, Foreground),
|
||||
(SynchronizeBuffersResponse, Foreground),
|
||||
(SynchronizeContexts, Foreground),
|
||||
(SynchronizeContextsResponse, Foreground),
|
||||
(TaskContext, Background),
|
||||
(TaskContextForLocation, Background),
|
||||
(Test, Foreground),
|
||||
@@ -278,7 +271,6 @@ messages!(
|
||||
(UpdateChannelMessage, Foreground),
|
||||
(UpdateChannels, Foreground),
|
||||
(UpdateContacts, Foreground),
|
||||
(UpdateContext, Foreground),
|
||||
(UpdateDiagnosticSummary, Foreground),
|
||||
(UpdateDiffBases, Foreground),
|
||||
(UpdateFollowers, Foreground),
|
||||
@@ -496,9 +488,6 @@ request_messages!(
|
||||
(LspQueryResponse, Ack),
|
||||
(RestartLanguageServers, Ack),
|
||||
(StopLanguageServers, Ack),
|
||||
(OpenContext, OpenContextResponse),
|
||||
(CreateContext, CreateContextResponse),
|
||||
(SynchronizeContexts, SynchronizeContextsResponse),
|
||||
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
|
||||
(LspExtGoToParentModule, LspExtGoToParentModuleResponse),
|
||||
(LspExtCancelFlycheck, Ack),
|
||||
@@ -684,11 +673,6 @@ entity_messages!(
|
||||
LspExtExpandMacro,
|
||||
LspExtOpenDocs,
|
||||
LspExtRunnables,
|
||||
AdvertiseContexts,
|
||||
OpenContext,
|
||||
CreateContext,
|
||||
UpdateContext,
|
||||
SynchronizeContexts,
|
||||
LspExtSwitchSourceHeader,
|
||||
LspExtGoToParentModule,
|
||||
LspExtCancelFlycheck,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{CompletionProvider, SelectionEffects};
|
||||
use editor::SelectionEffects;
|
||||
use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
|
||||
use gpui::{
|
||||
App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
|
||||
@@ -16,7 +16,6 @@ use platform_title_bar::PlatformTitleBar;
|
||||
use release_channel::ReleaseChannel;
|
||||
use rope::Rope;
|
||||
use settings::{ActionSequence, Settings};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
@@ -76,7 +75,6 @@ pub trait InlineAssistDelegate {
|
||||
pub fn open_rules_library(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
||||
make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
|
||||
prompt_to_select: Option<PromptId>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<WindowHandle<RulesLibrary>>> {
|
||||
@@ -141,7 +139,6 @@ pub fn open_rules_library(
|
||||
store,
|
||||
language_registry,
|
||||
inline_assist_delegate,
|
||||
make_completion_provider,
|
||||
prompt_to_select,
|
||||
window,
|
||||
cx,
|
||||
@@ -162,7 +159,6 @@ pub struct RulesLibrary {
|
||||
picker: Entity<Picker<RulePickerDelegate>>,
|
||||
pending_load: Task<()>,
|
||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
||||
make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -471,7 +467,6 @@ impl RulesLibrary {
|
||||
store: Entity<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
||||
make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
|
||||
rule_to_select: Option<PromptId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -514,7 +509,6 @@ impl RulesLibrary {
|
||||
active_rule_id: None,
|
||||
pending_load: Task::ready(()),
|
||||
inline_assist_delegate,
|
||||
make_completion_provider,
|
||||
_subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)],
|
||||
picker,
|
||||
}
|
||||
@@ -721,7 +715,6 @@ impl RulesLibrary {
|
||||
} else if let Some(rule_metadata) = self.store.read(cx).metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let rule = self.store.read(cx).load(prompt_id, cx);
|
||||
let make_completion_provider = self.make_completion_provider.clone();
|
||||
self.pending_load = cx.spawn_in(window, async move |this, cx| {
|
||||
let rule = rule.await;
|
||||
let markdown = language_registry.language_for_name("Markdown").await;
|
||||
@@ -756,7 +749,6 @@ impl RulesLibrary {
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_use_modal_editing(true);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Some(make_completion_provider()));
|
||||
if focus {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
}
|
||||
|
||||
@@ -509,7 +509,6 @@ impl VsCodeSettings {
|
||||
context_servers: self.context_servers(),
|
||||
context_server_timeout: None,
|
||||
load_direnv: None,
|
||||
slash_commands: None,
|
||||
git_hosting_providers: None,
|
||||
disable_ai: None,
|
||||
}
|
||||
|
||||
@@ -146,10 +146,6 @@ pub struct AgentSettingsContent {
|
||||
///
|
||||
/// Default: write
|
||||
pub default_profile: Option<Arc<str>>,
|
||||
/// Which view type to show by default in the agent panel.
|
||||
///
|
||||
/// Default: "thread"
|
||||
pub default_view: Option<DefaultAgentView>,
|
||||
/// Where new threads should start by default.
|
||||
///
|
||||
/// Default: "local_project"
|
||||
@@ -327,14 +323,6 @@ pub struct ContextServerPresetContent {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DefaultAgentView {
|
||||
#[default]
|
||||
Thread,
|
||||
TextThread,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
|
||||
@@ -180,9 +180,6 @@ pub struct EditPredictionSettingsContent {
|
||||
pub ollama: Option<OllamaEditPredictionSettingsContent>,
|
||||
/// Settings specific to using custom OpenAI-compatible servers for edit prediction.
|
||||
pub open_ai_compatible_api: Option<CustomEditPredictionProviderSettingsContent>,
|
||||
/// Whether edit predictions are enabled in the assistant prompt editor.
|
||||
/// This has no effect if globally disabled.
|
||||
pub enabled_in_text_threads: Option<bool>,
|
||||
/// The directory where manually captured edit prediction examples are stored.
|
||||
pub examples_dir: Option<Arc<Path>>,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use util::serde::default_true;
|
||||
|
||||
use crate::{
|
||||
AllLanguageSettingsContent, DelayMs, ExtendingVec, ParseStatus, ProjectTerminalSettingsContent,
|
||||
RootUserSettings, SaturatingBool, SlashCommandSettings, fallible_options,
|
||||
RootUserSettings, SaturatingBool, fallible_options,
|
||||
};
|
||||
|
||||
#[with_fallible_options]
|
||||
@@ -78,9 +78,6 @@ pub struct ProjectSettingsContent {
|
||||
/// Configuration for how direnv configuration should be loaded
|
||||
pub load_direnv: Option<DirenvSettings>,
|
||||
|
||||
/// Settings for slash commands.
|
||||
pub slash_commands: Option<SlashCommandSettings>,
|
||||
|
||||
/// The list of custom Git hosting providers.
|
||||
pub git_hosting_providers: Option<ExtendingVec<GitHostingProviderConfig>>,
|
||||
|
||||
|
||||
@@ -483,22 +483,6 @@ pub enum DockPosition {
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Settings for slash commands.
|
||||
#[with_fallible_options]
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
|
||||
pub struct SlashCommandSettings {
|
||||
/// Settings for the `/cargo-workspace` slash command.
|
||||
pub cargo_workspace: Option<CargoWorkspaceCommandSettings>,
|
||||
}
|
||||
|
||||
/// Settings for the `/cargo-workspace` slash command.
|
||||
#[with_fallible_options]
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
|
||||
pub struct CargoWorkspaceCommandSettings {
|
||||
/// Whether `/cargo-workspace` is enabled.
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
|
||||
|
||||
@@ -7470,61 +7470,33 @@ fn ai_page(cx: &App) -> SettingsPage {
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_prediction_display_sub_section() -> [SettingsPageItem; 2] {
|
||||
[
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Display Mode",
|
||||
description: "When to show edit predictions previews in buffer. The eager mode displays them inline, while the subtle mode displays them only when holding a modifier key.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("edit_prediction.display_mode"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.as_ref()?
|
||||
.mode
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert_default()
|
||||
.mode = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
fn edit_prediction_display_sub_section() -> [SettingsPageItem; 1] {
|
||||
[SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Display Mode",
|
||||
description: "When to show edit predictions previews in buffer. The eager mode displays them inline, while the subtle mode displays them only when holding a modifier key.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("edit_prediction.display_mode"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.as_ref()?
|
||||
.mode
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert_default()
|
||||
.mode = value;
|
||||
},
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Display In Text Threads",
|
||||
description: "Whether edit predictions are enabled when editing text threads in the agent panel.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("edit_prediction.in_text_threads"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.as_ref()?
|
||||
.enabled_in_text_threads
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert_default()
|
||||
.enabled_in_text_threads = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
]
|
||||
metadata: None,
|
||||
files: USER,
|
||||
})]
|
||||
}
|
||||
|
||||
SettingsPage {
|
||||
|
||||
@@ -49,7 +49,6 @@ zed_actions.workspace = true
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
agent_ui = { workspace = true, features = ["test-support"] }
|
||||
assistant_text_thread = { workspace = true, features = ["test-support"] }
|
||||
editor.workspace = true
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -5,7 +5,6 @@ use agent_ui::{
|
||||
test_support::{active_session_id, open_thread_with_connection, send_message},
|
||||
thread_metadata_store::ThreadMetadata,
|
||||
};
|
||||
use assistant_text_thread::TextThreadStore;
|
||||
use chrono::DateTime;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::FakeFs;
|
||||
@@ -1185,12 +1184,10 @@ async fn init_test_project_with_agent_panel(
|
||||
|
||||
fn add_agent_panel(
|
||||
workspace: &Entity<Workspace>,
|
||||
project: &Entity<project::Project>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) -> Entity<AgentPanel> {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
|
||||
let panel = cx.new(|cx| AgentPanel::test_new(workspace, text_thread_store, window, cx));
|
||||
let panel = cx.new(|cx| AgentPanel::test_new(workspace, window, cx));
|
||||
workspace.add_panel(panel.clone(), window, cx);
|
||||
panel
|
||||
})
|
||||
@@ -1198,12 +1195,11 @@ fn add_agent_panel(
|
||||
|
||||
fn setup_sidebar_with_agent_panel(
|
||||
multi_workspace: &Entity<MultiWorkspace>,
|
||||
project: &Entity<project::Project>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) -> (Entity<Sidebar>, Entity<AgentPanel>) {
|
||||
let sidebar = setup_sidebar(multi_workspace, cx);
|
||||
let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
|
||||
let panel = add_agent_panel(&workspace, project, cx);
|
||||
let panel = add_agent_panel(&workspace, cx);
|
||||
(sidebar, panel)
|
||||
}
|
||||
|
||||
@@ -1212,7 +1208,7 @@ async fn test_parallel_threads_shown_with_live_status(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
|
||||
@@ -1258,7 +1254,7 @@ async fn test_background_thread_completion_triggers_notification(cx: &mut TestAp
|
||||
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
|
||||
|
||||
@@ -1924,7 +1920,7 @@ async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext)
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
|
||||
@@ -1972,7 +1968,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
|
||||
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
|
||||
|
||||
@@ -1995,7 +1991,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
|
||||
let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.test_add_workspace(project_b.clone(), window, cx)
|
||||
});
|
||||
let panel_b = add_agent_panel(&workspace_b, &project_b, cx);
|
||||
let panel_b = add_agent_panel(&workspace_b, cx);
|
||||
cx.run_until_parked();
|
||||
|
||||
let workspace_a = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].clone());
|
||||
@@ -2199,7 +2195,7 @@ async fn test_new_thread_button_works_after_adding_folder(cx: &mut TestAppContex
|
||||
let fs = cx.update(|cx| <dyn fs::Fs>::global(cx));
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
|
||||
|
||||
@@ -2290,7 +2286,7 @@ async fn test_cmd_n_shows_new_thread_entry(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
|
||||
@@ -2341,7 +2337,7 @@ async fn test_draft_with_server_session_shows_as_draft(cx: &mut TestAppContext)
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
|
||||
@@ -2455,7 +2451,7 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp
|
||||
mw.test_add_workspace(worktree_project.clone(), window, cx)
|
||||
});
|
||||
|
||||
let worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
|
||||
let worktree_panel = add_agent_panel(&worktree_workspace, cx);
|
||||
|
||||
// Switch to the worktree workspace.
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
@@ -3128,7 +3124,7 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp
|
||||
|
||||
// Add an agent panel to the worktree workspace so we can run a
|
||||
// thread inside it.
|
||||
let worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
|
||||
let worktree_panel = add_agent_panel(&worktree_workspace, cx);
|
||||
|
||||
// Switch back to the main workspace before setting up the sidebar.
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
@@ -3240,7 +3236,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
|
||||
mw.test_add_workspace(worktree_project.clone(), window, cx)
|
||||
});
|
||||
|
||||
let worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
|
||||
let worktree_panel = add_agent_panel(&worktree_workspace, cx);
|
||||
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
let workspace = mw.workspaces()[0].clone();
|
||||
@@ -3998,7 +3994,7 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_t
|
||||
let cx_b = &mut gpui::VisualTestContext::from_window(multi_workspace_b.into(), cx);
|
||||
let sidebar_b = setup_sidebar(&multi_workspace_b_entity, cx_b);
|
||||
let workspace_b = multi_workspace_b_entity.read_with(cx_b, |mw, _| mw.workspace().clone());
|
||||
let _panel_b = add_agent_panel(&workspace_b, &project_b, cx_b);
|
||||
let _panel_b = add_agent_panel(&workspace_b, cx_b);
|
||||
|
||||
let session_id = acp::SessionId::new(Arc::from("archived-cross-window-with-sidebar"));
|
||||
|
||||
@@ -4204,8 +4200,8 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon
|
||||
let sidebar = setup_sidebar(&multi_workspace, cx);
|
||||
|
||||
let main_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspaces()[0].clone());
|
||||
let main_panel = add_agent_panel(&main_workspace, &main_project, cx);
|
||||
let _worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
|
||||
let main_panel = add_agent_panel(&main_workspace, cx);
|
||||
let _worktree_panel = add_agent_panel(&worktree_workspace, cx);
|
||||
|
||||
// Open Thread 2 in the main panel and keep it running.
|
||||
let connection = StubAgentConnection::new();
|
||||
@@ -4395,7 +4391,7 @@ async fn test_thread_switcher_ordering(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
|
||||
@@ -5070,7 +5066,7 @@ mod property_test {
|
||||
let workspace = multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.test_add_workspace(project.clone(), window, cx)
|
||||
});
|
||||
add_agent_panel(&workspace, &project, cx);
|
||||
add_agent_panel(&workspace, cx);
|
||||
let new_index = state.workspace_paths.len();
|
||||
state.workspace_paths.push(path);
|
||||
state.main_repo_indices.push(new_index);
|
||||
@@ -5087,7 +5083,7 @@ mod property_test {
|
||||
let workspace = multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.test_add_workspace(project.clone(), window, cx)
|
||||
});
|
||||
add_agent_panel(&workspace, &project, cx);
|
||||
add_agent_panel(&workspace, cx);
|
||||
state.workspace_paths.push(worktree.path);
|
||||
}
|
||||
Operation::RemoveWorkspace { index } => {
|
||||
@@ -5375,7 +5371,7 @@ mod property_test {
|
||||
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, _panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, _panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let mut state = TestState::new(fs, "/my-project".to_string());
|
||||
let mut executed: Vec<String> = Vec::new();
|
||||
|
||||
@@ -18,7 +18,6 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
breadcrumbs.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use crate::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::{Workspace, dock::Panel};
|
||||
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
|
||||
pub struct TerminalSlashCommand;
|
||||
|
||||
const LINE_COUNT_ARG: &str = "--line-count";
|
||||
|
||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||
|
||||
impl SlashCommand for TerminalSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"terminal".into()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &App) -> CodeLabel {
|
||||
create_label_for_command("terminal", &[LINE_COUNT_ARG], cx)
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert terminal output".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
|
||||
};
|
||||
|
||||
let line_count = arguments
|
||||
.get(0)
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(DEFAULT_CONTEXT_LINES);
|
||||
|
||||
let lines = active_terminal
|
||||
.read(cx)
|
||||
.entity()
|
||||
.read(cx)
|
||||
.last_n_non_empty_lines(line_count);
|
||||
|
||||
let mut text = String::new();
|
||||
text.push_str("Terminal output:\n");
|
||||
text.push_str(&lines.join("\n"));
|
||||
let range = 0..text.len();
|
||||
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::Terminal,
|
||||
label: "Terminal".into(),
|
||||
metadata: None,
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.into_event_stream()))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_active_terminal(
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<TerminalView>> {
|
||||
if let Some(terminal_view) = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<TerminalView>(cx))
|
||||
{
|
||||
return Some(terminal_view);
|
||||
}
|
||||
|
||||
let terminal_panel = workspace.read(cx).panel::<TerminalPanel>(cx)?;
|
||||
terminal_panel.read(cx).pane().and_then(|pane| {
|
||||
pane.read(cx)
|
||||
.active_item()
|
||||
.and_then(|t| t.downcast::<TerminalView>())
|
||||
})
|
||||
}
|
||||
@@ -3,9 +3,7 @@ pub mod terminal_element;
|
||||
pub mod terminal_panel;
|
||||
mod terminal_path_like_target;
|
||||
pub mod terminal_scrollbar;
|
||||
mod terminal_slash_command;
|
||||
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use editor::{
|
||||
Editor, EditorSettings, actions::SelectAll, blink_manager::BlinkManager,
|
||||
ui_scrollbar_settings_from_raw,
|
||||
@@ -47,7 +45,6 @@ use terminal_element::TerminalElement;
|
||||
use terminal_panel::TerminalPanel;
|
||||
use terminal_path_like_target::{hover_path_like_target, open_path_like_target};
|
||||
use terminal_scrollbar::TerminalScrollHandle;
|
||||
use terminal_slash_command::TerminalSlashCommand;
|
||||
use ui::{
|
||||
ContextMenu, Divider, ScrollAxes, Scrollbars, Tooltip, WithScrollbar,
|
||||
prelude::*,
|
||||
@@ -101,7 +98,6 @@ actions!(
|
||||
pub struct RenameTerminal;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
assistant_slash_command::init(cx);
|
||||
terminal_panel::init(cx);
|
||||
|
||||
register_serializable_item::<TerminalView>(cx);
|
||||
@@ -110,7 +106,6 @@ pub fn init(cx: &mut App) {
|
||||
workspace.register_action(TerminalView::deploy);
|
||||
})
|
||||
.detach();
|
||||
SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true);
|
||||
}
|
||||
|
||||
pub struct BlockProperties {
|
||||
|
||||
@@ -682,8 +682,7 @@ fn main() {
|
||||
);
|
||||
agent_ui::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
prompt_builder.clone(),
|
||||
prompt_builder,
|
||||
app_state.languages.clone(),
|
||||
is_new_install,
|
||||
false,
|
||||
@@ -819,7 +818,7 @@ fn main() {
|
||||
|
||||
let menus = app_menus(cx);
|
||||
cx.set_menus(menus);
|
||||
initialize_workspace(app_state.clone(), prompt_builder, cx);
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
|
||||
cx.activate(true);
|
||||
|
||||
|
||||
@@ -211,7 +211,6 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
|
||||
);
|
||||
agent_ui::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
prompt_builder,
|
||||
app_state.languages.clone(),
|
||||
true,
|
||||
@@ -2134,16 +2133,10 @@ fn run_agent_thread_view_test(
|
||||
})
|
||||
.context("Failed to get workspace handle")?;
|
||||
|
||||
let prompt_builder =
|
||||
cx.update(|cx| prompt_store::PromptBuilder::load(app_state.fs.clone(), false, cx));
|
||||
cx.background_executor.allow_parking();
|
||||
let panel = cx
|
||||
.foreground_executor
|
||||
.block_test(AgentPanel::load(
|
||||
weak_workspace,
|
||||
prompt_builder,
|
||||
async_window_cx,
|
||||
))
|
||||
.block_test(AgentPanel::load(weak_workspace, async_window_cx))
|
||||
.context("Failed to load AgentPanel")?;
|
||||
cx.background_executor.forbid_parking();
|
||||
|
||||
@@ -3296,52 +3289,40 @@ edition = "2021"
|
||||
})
|
||||
.context("Failed to get workspace handle for agent panel")?;
|
||||
|
||||
let prompt_builder =
|
||||
cx.update(|cx| prompt_store::PromptBuilder::load(app_state.fs.clone(), false, cx));
|
||||
|
||||
// Register an observer so that workspaces created by the worktree creation
|
||||
// flow get AgentPanel and ProjectPanel loaded automatically. Without this,
|
||||
// `workspace.panel::<AgentPanel>(cx)` returns None in the new workspace and
|
||||
// the creation flow's `focus_panel::<AgentPanel>` call is a no-op.
|
||||
let _workspace_observer = cx.update({
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
|cx| {
|
||||
cx.observe_new(move |workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else { return };
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
let panels_task = cx.spawn_in(window, async move |workspace_handle, cx| {
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let agent_panel =
|
||||
AgentPanel::load(workspace_handle.clone(), prompt_builder, cx.clone());
|
||||
if let Ok(panel) = project_panel.await {
|
||||
workspace_handle
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_panel(panel, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
if let Ok(panel) = agent_panel.await {
|
||||
workspace_handle
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_panel(panel, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
workspace.set_panels_task(panels_task);
|
||||
})
|
||||
}
|
||||
let _workspace_observer = cx.update(|cx| {
|
||||
cx.observe_new(move |workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else { return };
|
||||
let panels_task = cx.spawn_in(window, async move |workspace_handle, cx| {
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let agent_panel = AgentPanel::load(workspace_handle.clone(), cx.clone());
|
||||
if let Ok(panel) = project_panel.await {
|
||||
workspace_handle
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_panel(panel, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
if let Ok(panel) = agent_panel.await {
|
||||
workspace_handle
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_panel(panel, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
workspace.set_panels_task(panels_task);
|
||||
})
|
||||
});
|
||||
|
||||
cx.background_executor.allow_parking();
|
||||
let panel = cx
|
||||
.foreground_executor
|
||||
.block_test(AgentPanel::load(
|
||||
weak_workspace,
|
||||
prompt_builder,
|
||||
async_window_cx,
|
||||
))
|
||||
.block_test(AgentPanel::load(weak_workspace, async_window_cx))
|
||||
.context("Failed to load AgentPanel")?;
|
||||
cx.background_executor.forbid_parking();
|
||||
|
||||
|
||||
+10
-28
@@ -13,7 +13,7 @@ pub mod visual_tests;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod windows_only_instance;
|
||||
|
||||
use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
|
||||
use agent_ui::AgentDiffToolbar;
|
||||
use anyhow::Context as _;
|
||||
pub use app_menus::*;
|
||||
use assets::Assets;
|
||||
@@ -56,7 +56,6 @@ use paths::{
|
||||
};
|
||||
use project::{DirectoryLister, DisableAiSettings, ProjectItem};
|
||||
use project_panel::ProjectPanel;
|
||||
use prompt_store::PromptBuilder;
|
||||
use quick_action_bar::QuickActionBar;
|
||||
use recent_projects::open_remote_project;
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
@@ -355,11 +354,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_workspace(
|
||||
app_state: Arc<AppState>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
|
||||
let mut _on_close_subscription = bind_on_window_closed(cx);
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
// A 1.92 regression causes unused-assignment to trigger on this variable.
|
||||
@@ -524,7 +519,7 @@ pub fn initialize_workspace(
|
||||
status_bar.add_right_item(image_info, window, cx);
|
||||
});
|
||||
|
||||
let panels_task = initialize_panels(prompt_builder.clone(), window, cx);
|
||||
let panels_task = initialize_panels(window, cx);
|
||||
workspace.set_panels_task(panels_task);
|
||||
register_actions(app_state.clone(), workspace, window, cx);
|
||||
|
||||
@@ -647,11 +642,7 @@ fn show_software_emulation_warning_if_needed(
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_panels(
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
fn initialize_panels(window: &mut Window, cx: &mut Context<Workspace>) -> Task<anyhow::Result<()>> {
|
||||
cx.spawn_in(window, async move |workspace_handle, cx| {
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
|
||||
@@ -688,7 +679,7 @@ fn initialize_panels(
|
||||
add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
|
||||
add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()),
|
||||
add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
|
||||
initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()),
|
||||
initialize_agent_panel(workspace_handle, cx.clone()).map(|r| r.log_err()),
|
||||
);
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -733,24 +724,20 @@ fn setup_or_teardown_ai_panel<P: Panel>(
|
||||
|
||||
async fn initialize_agent_panel(
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
workspace_handle
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
|
||||
agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
|
||||
agent_ui::AgentPanel::load(workspace, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
workspace_handle.update_in(&mut cx, |workspace, window, cx| {
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
|
||||
agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
|
||||
agent_ui::AgentPanel::load(workspace, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
@@ -763,11 +750,6 @@ async fn initialize_agent_panel(
|
||||
//
|
||||
// Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
|
||||
if !cfg!(test) {
|
||||
<dyn AgentPanelDelegate>::set_global(
|
||||
Arc::new(agent_ui::ConcreteAssistantPanelDelegate),
|
||||
cx,
|
||||
);
|
||||
|
||||
workspace
|
||||
.register_action(agent_ui::AgentPanel::toggle_focus)
|
||||
.register_action(agent_ui::AgentPanel::toggle)
|
||||
@@ -2269,6 +2251,7 @@ mod tests {
|
||||
use languages::{markdown_lang, rust_lang};
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{Project, ProjectPath};
|
||||
use prompt_store::PromptBuilder;
|
||||
use semver::Version;
|
||||
use serde_json::json;
|
||||
use settings::{SaturatingBool, SettingsStore, watch_config_file};
|
||||
@@ -5045,8 +5028,7 @@ mod tests {
|
||||
);
|
||||
agent_ui::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
prompt_builder.clone(),
|
||||
prompt_builder,
|
||||
app_state.languages.clone(),
|
||||
true,
|
||||
false,
|
||||
@@ -5061,7 +5043,7 @@ mod tests {
|
||||
);
|
||||
project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
|
||||
debugger_ui::init(cx);
|
||||
initialize_workspace(app_state.clone(), prompt_builder, cx);
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
search::init(cx);
|
||||
cx.set_global(workspace::PaneSearchBarCallbacks {
|
||||
setup_search_bar: |languages, toolbar, window, cx| {
|
||||
|
||||
@@ -85,8 +85,6 @@ pub enum ExtensionCategoryFilter {
|
||||
LanguageServers,
|
||||
ContextServers,
|
||||
AgentServers,
|
||||
SlashCommands,
|
||||
IndexedDocsProviders,
|
||||
Snippets,
|
||||
DebugAdapters,
|
||||
}
|
||||
@@ -528,14 +526,6 @@ pub mod assistant {
|
||||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
[
|
||||
/// Shows the assistant configuration panel.
|
||||
ShowConfiguration
|
||||
]
|
||||
);
|
||||
|
||||
/// Opens the rules library for managing agent rules and prompts.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent, deprecated_aliases = ["assistant::OpenRulesLibrary", "assistant::DeployPromptLibrary"])]
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
- [External Agents](./ai/external-agents.md)
|
||||
- [Inline Assistant](./ai/inline-assistant.md)
|
||||
- [Edit Prediction](./ai/edit-prediction.md)
|
||||
- [Text Threads](./ai/text-threads.md)
|
||||
- [Rules](./ai/rules.md)
|
||||
- [Model Context Protocol](./ai/mcp.md)
|
||||
- [Configuration](./ai/configuration.md)
|
||||
@@ -163,7 +162,6 @@
|
||||
- [Theme Extensions](./extensions/themes.md)
|
||||
- [Icon Theme Extensions](./extensions/icon-themes.md)
|
||||
- [Snippets Extensions](./extensions/snippets.md)
|
||||
- [Slash Command Extensions](./extensions/slash-commands.md)
|
||||
- [Agent Server Extensions](./extensions/agent-servers.md)
|
||||
- [MCP Server Extensions](./extensions/mcp-extensions.md)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ The sections below cover what you can do from here.
|
||||
|
||||
By default, the Agent Panel uses Zed's first-party agent.
|
||||
|
||||
To choose another agent, go to the plus button in the top-right of the Agent Panel and pick either one of the [external agents](./external-agents.md) installed out of the box or a new [Text Thread](./text-threads.md).
|
||||
To choose another agent, go to the plus button in the top-right of the Agent Panel and pick one of the [external agents](./external-agents.md) installed out of the box.
|
||||
|
||||
### Editing Messages {#editing-messages}
|
||||
|
||||
@@ -222,15 +222,6 @@ All [Zed's hosted models](./models.md) support tool calling out-of-the-box.
|
||||
Similarly to the built-in tools, some models may not support all tools included in a given MCP Server.
|
||||
Zed's UI will inform you about this via a warning icon that appears close to the model selector.
|
||||
|
||||
## Text Threads {#text-threads}
|
||||
|
||||
["Text Threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text.
|
||||
With text threads, you have full control over the conversation data.
|
||||
You can remove and edit responses from the LLM, swap roles, and include more context earlier in the conversation.
|
||||
|
||||
Text threads are Zed's original assistant panel format, preserved for users who want direct control over conversation data.
|
||||
Autonomous code editing (where the agent writes to files) is only available in the default thread format, not text threads.
|
||||
|
||||
## Errors and Debugging {#errors-and-debugging}
|
||||
|
||||
If you hit an error or unusual LLM behavior, open the thread as Markdown with `agent: open thread as markdown` and attach it to your GitHub issue.
|
||||
|
||||
@@ -140,19 +140,6 @@ Specify a custom temperature for a provider and/or model:
|
||||
|
||||
Note that some of these settings are also surfaced in the Agent Panel's settings UI, which you can access either via the `agent: open settings` action or by the dropdown menu on the top-right corner of the panel.
|
||||
|
||||
### Default View
|
||||
|
||||
Use the `default_view` setting to change the default view of the Agent Panel.
|
||||
You can choose between `thread` (the default) and `text_thread`:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"agent": {
|
||||
"default_view": "text_thread"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Font Size
|
||||
|
||||
Use the `agent_ui_font_size` setting to change the font size of rendered agent responses in the panel.
|
||||
|
||||
@@ -12,7 +12,6 @@ AI features in Zed include:
|
||||
- [Agent Panel](./agent-panel.md)
|
||||
- [Edit Predictions](./edit-prediction.md)
|
||||
- [Inline Assist](./inline-assistant.md)
|
||||
- [Text Threads](./text-threads.md)
|
||||
- Auto Git Commit Message Generation
|
||||
|
||||
By default, Zed does not store your prompts or code context. This data is sent to your selected AI provider (e.g., Anthropic, OpenAI, Google, or xAI) to generate responses, then discarded. Zed will not use your data to evaluate or improve AI features unless you explicitly share it (see [AI Feedback with Ratings](#ai-feedback-with-ratings)) or you opt in to edit prediction training data collection (see [Edit Predictions](#edit-predictions)).
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Transform code inline with AI in Zed. Send selections to any LLM fo
|
||||
|
||||
## Usage Overview
|
||||
|
||||
Use {#kb assistant::InlineAssist} to open the Inline Assistant in editors, text threads, the rules library, channel notes, and the terminal panel.
|
||||
Use {#kb assistant::InlineAssist} to open the Inline Assistant in editors, the rules library, channel notes, and the terminal panel.
|
||||
|
||||
The Inline Assistant sends your current selection (or line) to a language model and replaces it with the response.
|
||||
|
||||
|
||||
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user