diff --git a/Cargo.lock b/Cargo.lock
index 1c0d673f2b..fb6ad1f833 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -339,7 +339,6 @@ dependencies = [
"anthropic",
"anyhow",
"assistant_slash_command",
- "async-watch",
"cargo_toml",
"chrono",
"client",
@@ -824,15 +823,6 @@ dependencies = [
"tungstenite 0.16.0",
]
-[[package]]
-name = "async-watch"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a078faf4e27c0c6cc0efb20e5da59dcccc04968ebf2801d8e0b2195124cdcdb2"
-dependencies = [
- "event-listener 2.5.3",
-]
-
[[package]]
name = "async_zip"
version = "0.0.17"
diff --git a/assets/icons/zed_assistant_filled.svg b/assets/icons/zed_assistant_filled.svg
new file mode 100644
index 0000000000..8d16fd9849
--- /dev/null
+++ b/assets/icons/zed_assistant_filled.svg
@@ -0,0 +1,5 @@
+
diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml
index 57ca921802..029da5d553 100644
--- a/crates/assistant/Cargo.toml
+++ b/crates/assistant/Cargo.toml
@@ -16,7 +16,6 @@ doctest = false
anyhow.workspace = true
anthropic = { workspace = true, features = ["schemars"] }
assistant_slash_command.workspace = true
-async-watch.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true
diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs
index 3f3ecbc862..d3a4b66532 100644
--- a/crates/assistant/src/assistant.rs
+++ b/crates/assistant/src/assistant.rs
@@ -19,14 +19,13 @@ use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use model_selector::*;
-use prompt_library::PromptStore;
pub(crate) use saved_conversation::*;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use slash_command::{
- active_command, fetch_command, file_command, project_command, prompt_command, rustdoc_command,
- search_command, tabs_command,
+ active_command, default_command, fetch_command, file_command, project_command, prompt_command,
+ rustdoc_command, search_command, tabs_command,
};
use std::{
fmt::{self, Display},
@@ -303,18 +302,10 @@ fn register_slash_commands(cx: &mut AppContext) {
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
+ slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
+ slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
-
- let store = PromptStore::global(cx);
- cx.background_executor()
- .spawn(async move {
- let store = store.await?;
- slash_command_registry
- .register_command(prompt_command::PromptSlashCommand::new(store), true);
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
}
#[cfg(test)]
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index b73d2a89c6..ea895f2021 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -1,11 +1,11 @@
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
codegen::{self, Codegen, CodegenKind},
- prompt_library::{open_prompt_library, PromptMetadata, PromptStore},
+ prompt_library::open_prompt_library,
prompts::generate_content_prompt,
search::*,
slash_command::{
- prompt_command::PromptPlaceholder, SlashCommandCompletionProvider, SlashCommandLine,
+ default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
SlashCommandRegistry,
},
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
@@ -14,7 +14,7 @@ use crate::{
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleModelSelector,
};
use anyhow::{anyhow, Result};
-use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection};
+use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use client::telemetry::Telemetry;
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
use editor::{actions::ShowCompletions, GutterDimensions};
@@ -40,10 +40,9 @@ use gpui::{
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
WeakModel, WeakView, WhiteSpace, WindowContext,
};
-use language::LspAdapterDelegate;
use language::{
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
- OffsetRangeExt as _, Point, ToOffset as _,
+ LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
@@ -118,14 +117,6 @@ pub struct AssistantPanel {
_watch_saved_conversations: Task>,
authentication_prompt: Option,
model_menu_handle: PopoverMenuHandle,
- default_prompt: DefaultPrompt,
- _watch_prompt_store: Task<()>,
-}
-
-#[derive(Default)]
-struct DefaultPrompt {
- text: String,
- sections: Vec>,
}
struct ActiveConversationEditor {
@@ -146,8 +137,6 @@ impl AssistantPanel {
.await
.log_err()
.unwrap_or_default();
- let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?;
- let default_prompts = prompt_store.load_default().await?;
// TODO: deserialize state.
let workspace_handle = workspace.clone();
@@ -173,22 +162,6 @@ impl AssistantPanel {
anyhow::Ok(())
});
- let _watch_prompt_store = cx.spawn(|this, mut cx| async move {
- let mut updates = prompt_store.updates();
- while updates.changed().await.is_ok() {
- let Some(prompts) = prompt_store.load_default().await.log_err() else {
- continue;
- };
-
- if this
- .update(&mut cx, |this, _cx| this.update_default_prompt(prompts))
- .is_err()
- {
- break;
- }
- }
- });
-
let toolbar = cx.new_view(|cx| {
let mut toolbar = Toolbar::new();
toolbar.set_can_navigate(false, cx);
@@ -216,7 +189,7 @@ impl AssistantPanel {
})
.detach();
- let mut this = Self {
+ Self {
workspace: workspace_handle,
active_conversation_editor: None,
show_saved_conversations: false,
@@ -239,11 +212,7 @@ impl AssistantPanel {
_watch_saved_conversations,
authentication_prompt: None,
model_menu_handle: PopoverMenuHandle::default(),
- default_prompt: DefaultPrompt::default(),
- _watch_prompt_store,
- };
- this.update_default_prompt(default_prompts);
- this
+ }
})
})
})
@@ -266,55 +235,6 @@ impl AssistantPanel {
cx.notify();
}
- fn update_default_prompt(&mut self, prompts: Vec<(PromptMetadata, String)>) {
- self.default_prompt.text.clear();
- self.default_prompt.sections.clear();
- if !prompts.is_empty() {
- self.default_prompt.text.push_str("Default Prompt:\n");
- }
-
- for (metadata, body) in prompts {
- let section_start = self.default_prompt.text.len();
- self.default_prompt.text.push_str(&body);
- let section_end = self.default_prompt.text.len();
- self.default_prompt
- .sections
- .push(SlashCommandOutputSection {
- range: section_start..section_end,
- render_placeholder: Arc::new(move |id, unfold, _cx| {
- PromptPlaceholder {
- title: metadata
- .title
- .clone()
- .unwrap_or_else(|| SharedString::from("Untitled")),
- id,
- unfold,
- }
- .into_any_element()
- }),
- });
- self.default_prompt.text.push('\n');
- }
- self.default_prompt.text.pop();
-
- if !self.default_prompt.text.is_empty() {
- self.default_prompt.sections.insert(
- 0,
- SlashCommandOutputSection {
- range: 0..self.default_prompt.text.len(),
- render_placeholder: Arc::new(move |id, unfold, _cx| {
- PromptPlaceholder {
- title: "Default".into(),
- id,
- unfold,
- }
- .into_any_element()
- }),
- },
- )
- }
- }
-
fn completion_provider_changed(
&mut self,
prev_settings_version: usize,
@@ -862,7 +782,6 @@ impl AssistantPanel {
let editor = cx.new_view(|cx| {
ConversationEditor::new(
- &self.default_prompt,
self.languages.clone(),
self.slash_commands.clone(),
self.fs.clone(),
@@ -1460,7 +1379,9 @@ enum ConversationEvent {
updated: Vec,
},
SlashCommandFinished {
+ output_range: Range,
sections: Vec>,
+ run_commands_in_output: bool,
},
}
@@ -1727,18 +1648,7 @@ impl Conversation {
buffer.line_len(row_range.end - 1),
));
- let start_ix = match self
- .pending_slash_commands
- .binary_search_by(|probe| probe.source_range.start.cmp(&start, buffer))
- {
- Ok(ix) | Err(ix) => ix,
- };
- let end_ix = match self.pending_slash_commands[start_ix..]
- .binary_search_by(|probe| probe.source_range.end.cmp(&end, buffer))
- {
- Ok(ix) => start_ix + ix + 1,
- Err(ix) => start_ix + ix,
- };
+ let old_range = self.pending_command_indices_for_range(start..end, cx);
let mut new_commands = Vec::new();
let mut lines = buffer.text_for_range(start..end).lines();
@@ -1773,9 +1683,7 @@ impl Conversation {
offset = lines.offset();
}
- let removed_commands = self
- .pending_slash_commands
- .splice(start_ix..end_ix, new_commands);
+ let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
removed.extend(removed_commands.map(|command| command.source_range));
}
@@ -1849,25 +1757,60 @@ impl Conversation {
cx: &mut ModelContext,
) -> Option<&mut PendingSlashCommand> {
let buffer = self.buffer.read(cx);
- let ix = self
+ match self
.pending_slash_commands
- .binary_search_by(|probe| {
- if probe.source_range.start.cmp(&position, buffer).is_gt() {
- Ordering::Less
- } else if probe.source_range.end.cmp(&position, buffer).is_lt() {
- Ordering::Greater
+ .binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
+ {
+ Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
+ Err(ix) => {
+ let cmd = self.pending_slash_commands.get_mut(ix)?;
+ if position.cmp(&cmd.source_range.start, buffer).is_ge()
+ && position.cmp(&cmd.source_range.end, buffer).is_le()
+ {
+ Some(cmd)
} else {
- Ordering::Equal
+ None
}
- })
- .ok()?;
- self.pending_slash_commands.get_mut(ix)
+ }
+ }
+ }
+
+ fn pending_commands_for_range(
+ &self,
+ range: Range,
+ cx: &AppContext,
+ ) -> &[PendingSlashCommand] {
+ let range = self.pending_command_indices_for_range(range, cx);
+ &self.pending_slash_commands[range]
+ }
+
+ fn pending_command_indices_for_range(
+ &self,
+ range: Range,
+ cx: &AppContext,
+ ) -> Range {
+ let buffer = self.buffer.read(cx);
+ let start_ix = match self
+ .pending_slash_commands
+ .binary_search_by(|probe| probe.source_range.end.cmp(&range.start, &buffer))
+ {
+ Ok(ix) | Err(ix) => ix,
+ };
+ let end_ix = match self
+ .pending_slash_commands
+ .binary_search_by(|probe| probe.source_range.start.cmp(&range.end, &buffer))
+ {
+ Ok(ix) => ix + 1,
+ Err(ix) => ix,
+ };
+ start_ix..end_ix
}
fn insert_command_output(
&mut self,
command_range: Range,
output: Task>,
+ insert_trailing_newline: bool,
cx: &mut ModelContext,
) {
self.reparse_slash_commands(cx);
@@ -1878,13 +1821,14 @@ impl Conversation {
let output = output.await;
this.update(&mut cx, |this, cx| match output {
Ok(mut output) => {
- if !output.text.ends_with('\n') {
+ if insert_trailing_newline {
output.text.push('\n');
}
- let sections = this.buffer.update(cx, |buffer, cx| {
+ let event = this.buffer.update(cx, |buffer, cx| {
let start = command_range.start.to_offset(buffer);
let old_end = command_range.end.to_offset(buffer);
+ let new_end = start + output.text.len();
buffer.edit([(start..old_end, output.text)], None, cx);
let mut sections = output
@@ -1897,9 +1841,14 @@ impl Conversation {
})
.collect::>();
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
- sections
+ ConversationEvent::SlashCommandFinished {
+ output_range: buffer.anchor_after(start)
+ ..buffer.anchor_before(new_end),
+ sections,
+ run_commands_in_output: output.run_commands_in_text,
+ }
});
- cx.emit(ConversationEvent::SlashCommandFinished { sections });
+ cx.emit(event);
}
Err(error) => {
if let Some(pending_command) =
@@ -2596,7 +2545,6 @@ pub struct ConversationEditor {
impl ConversationEditor {
fn new(
- default_prompt: &DefaultPrompt,
language_registry: Arc,
slash_command_registry: Arc,
fs: Arc,
@@ -2618,31 +2566,7 @@ impl ConversationEditor {
let mut this =
Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, cx);
-
- if !default_prompt.text.is_empty() {
- this.editor
- .update(cx, |editor, cx| editor.insert(&default_prompt.text, cx));
- let snapshot = this.conversation.read(cx).buffer.read(cx).text_snapshot();
- this.insert_slash_command_output_sections(
- default_prompt
- .sections
- .iter()
- .map(|section| SlashCommandOutputSection {
- range: snapshot.anchor_after(section.range.start)
- ..snapshot.anchor_before(section.range.end),
- render_placeholder: section.render_placeholder.clone(),
- }),
- cx,
- );
- this.split(&Split, cx);
- this.conversation.update(cx, |this, _cx| {
- this.messages_metadata
- .get_mut(&MessageId::default())
- .unwrap()
- .role = Role::System;
- });
- }
-
+ this.insert_default_prompt(cx);
this
}
@@ -2695,6 +2619,32 @@ impl ConversationEditor {
this
}
+ fn insert_default_prompt(&mut self, cx: &mut ViewContext) {
+ let command_name = DefaultSlashCommand.name();
+ self.editor.update(cx, |editor, cx| {
+ editor.insert(&format!("/{command_name}"), cx)
+ });
+ self.split(&Split, cx);
+ let command = self.conversation.update(cx, |conversation, cx| {
+ conversation
+ .messages_metadata
+ .get_mut(&MessageId::default())
+ .unwrap()
+ .role = Role::System;
+ conversation.reparse_slash_commands(cx);
+ conversation.pending_slash_commands[0].clone()
+ });
+
+ self.run_command(
+ command.source_range,
+ &command.name,
+ command.argument.as_deref(),
+ false,
+ self.workspace.clone(),
+ cx,
+ );
+ }
+
fn assist(&mut self, _: &Assist, cx: &mut ViewContext) {
let cursors = self.cursors(cx);
@@ -2817,6 +2767,7 @@ impl ConversationEditor {
command.source_range,
&command.name,
command.argument.as_deref(),
+ true,
workspace.clone(),
cx,
);
@@ -2830,6 +2781,7 @@ impl ConversationEditor {
command_range: Range,
name: &str,
argument: Option<&str>,
+ insert_trailing_newline: bool,
workspace: WeakView,
cx: &mut ViewContext,
) {
@@ -2838,7 +2790,12 @@ impl ConversationEditor {
let argument = argument.map(ToString::to_string);
let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
self.conversation.update(cx, |conversation, cx| {
- conversation.insert_command_output(command_range, output, cx)
+ conversation.insert_command_output(
+ command_range,
+ output,
+ insert_trailing_newline,
+ cx,
+ )
});
}
}
@@ -2938,6 +2895,7 @@ impl ConversationEditor {
command.source_range.clone(),
&command.name,
command.argument.as_deref(),
+ false,
workspace.clone(),
cx,
);
@@ -2991,8 +2949,32 @@ impl ConversationEditor {
);
})
}
- ConversationEvent::SlashCommandFinished { sections } => {
+ ConversationEvent::SlashCommandFinished {
+ output_range,
+ sections,
+ run_commands_in_output,
+ } => {
self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
+
+ if *run_commands_in_output {
+ let commands = self.conversation.update(cx, |conversation, cx| {
+ conversation.reparse_slash_commands(cx);
+ conversation
+ .pending_commands_for_range(output_range.clone(), cx)
+ .to_vec()
+ });
+
+ for command in commands {
+ self.run_command(
+ command.source_range,
+ &command.name,
+ command.argument.as_deref(),
+ false,
+ self.workspace.clone(),
+ cx,
+ );
+ }
+ }
}
}
}
diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs
index 6426a80aa7..fdeeb4ea85 100644
--- a/crates/assistant/src/prompt_library.rs
+++ b/crates/assistant/src/prompt_library.rs
@@ -1,33 +1,40 @@
+use crate::{
+ slash_command::SlashCommandLine, CompletionProvider, LanguageModelRequest,
+ LanguageModelRequestMessage, Role,
+};
use anyhow::{anyhow, Result};
+use assistant_slash_command::SlashCommandRegistry;
use chrono::{DateTime, Utc};
use collections::HashMap;
-use editor::{Editor, EditorEvent};
+use editor::{actions::Tab, Editor, EditorEvent};
use futures::{
future::{self, BoxFuture, Shared},
FutureExt,
};
-use fuzzy::StringMatchCandidate;
+use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions, point, size, AppContext, BackgroundExecutor, Bounds, DevicePixels, Empty,
- EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, View,
- WindowBounds, WindowHandle, WindowOptions,
+ actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
+ EventEmitter, Global, Model, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
+ View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{types::SerdeBincode, Database, RoTxn};
-use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
+use language::{
+ language_settings::SoftWrap, Buffer, Documentation, LanguageRegistry, LanguageServerId, Point,
+ ToPoint as _,
+};
use parking_lot::RwLock;
use picker::{Picker, PickerDelegate};
use rope::Rope;
use serde::{Deserialize, Serialize};
use std::{
- cmp::Reverse,
future::Future,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
time::Duration,
};
use ui::{
- div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
- SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
+ div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
+ ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
};
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -80,7 +87,7 @@ pub fn open_prompt_library(
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
- title: None,
+ title: Some("Prompt Library".into()),
appears_transparent: true,
traffic_light_position: Some(point(px(9.0), px(9.0))),
}),
@@ -106,6 +113,8 @@ pub struct PromptLibrary {
struct PromptEditor {
editor: View,
+ token_count: Option,
+ pending_token_count: Task