Detailed changes
@@ -434,10 +434,10 @@ dependencies = [
"anyhow",
"collections",
"derive_more",
- "futures 0.3.28",
"gpui",
"language",
"parking_lot",
+ "workspace",
]
[[package]]
@@ -3823,6 +3823,7 @@ dependencies = [
"wasmtime",
"wasmtime-wasi",
"wit-component",
+ "workspace",
]
[[package]]
@@ -0,0 +1 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 11L6 4L10.5 7.5L6 11Z" fill="currentColor"></path></svg>
@@ -211,7 +211,9 @@
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
- "ctrl-r": "assistant::CycleMessageRole"
+ "ctrl-r": "assistant::CycleMessageRole",
+ "enter": "assistant::ConfirmCommand",
+ "alt-enter": "editor::Newline"
}
},
{
@@ -227,7 +227,9 @@
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
- "ctrl-r": "assistant::CycleMessageRole"
+ "ctrl-r": "assistant::CycleMessageRole",
+ "enter": "assistant::ConfirmCommand",
+ "alt-enter": "editor::Newline"
}
},
{
@@ -1,30 +0,0 @@
-mod current_project;
-mod recent_buffers;
-
-pub use current_project::*;
-pub use recent_buffers::*;
-
-#[derive(Default)]
-pub struct AmbientContext {
- pub recent_buffers: RecentBuffersContext,
- pub current_project: CurrentProjectContext,
-}
-
-impl AmbientContext {
- pub fn snapshot(&self) -> AmbientContextSnapshot {
- AmbientContextSnapshot {
- recent_buffers: self.recent_buffers.snapshot.clone(),
- }
- }
-}
-
-#[derive(Clone, Default, Debug)]
-pub struct AmbientContextSnapshot {
- pub recent_buffers: RecentBuffersSnapshot,
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
-pub enum ContextUpdated {
- Updating,
- Disabled,
-}
@@ -1,180 +0,0 @@
-use std::fmt::Write;
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
-use std::time::Duration;
-
-use anyhow::{anyhow, Result};
-use fs::Fs;
-use gpui::{AsyncAppContext, ModelContext, Task, WeakModel};
-use project::{Project, ProjectPath};
-use util::ResultExt;
-
-use crate::ambient_context::ContextUpdated;
-use crate::assistant_panel::Conversation;
-use crate::{LanguageModelRequestMessage, Role};
-
-/// Ambient context about the current project.
-pub struct CurrentProjectContext {
- pub enabled: bool,
- pub message: String,
- pub pending_message: Option<Task<()>>,
-}
-
-#[allow(clippy::derivable_impls)]
-impl Default for CurrentProjectContext {
- fn default() -> Self {
- Self {
- enabled: false,
- message: String::new(),
- pending_message: None,
- }
- }
-}
-
-impl CurrentProjectContext {
- /// Returns the [`CurrentProjectContext`] as a message to the language model.
- pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
- self.enabled
- .then(|| LanguageModelRequestMessage {
- role: Role::System,
- content: self.message.clone(),
- })
- .filter(|message| !message.content.is_empty())
- }
-
- /// Updates the [`CurrentProjectContext`] for the given [`Project`].
- pub fn update(
- &mut self,
- fs: Arc<dyn Fs>,
- project: WeakModel<Project>,
- cx: &mut ModelContext<Conversation>,
- ) -> ContextUpdated {
- if !self.enabled {
- self.message.clear();
- self.pending_message = None;
- cx.notify();
- return ContextUpdated::Disabled;
- }
-
- self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
- const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
- cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
-
- let Some(path_to_cargo_toml) = Self::path_to_cargo_toml(project, &mut cx).log_err()
- else {
- return;
- };
-
- let Some(path_to_cargo_toml) = path_to_cargo_toml
- .ok_or_else(|| anyhow!("no Cargo.toml"))
- .log_err()
- else {
- return;
- };
-
- let message_task = cx
- .background_executor()
- .spawn(async move { Self::build_message(fs, &path_to_cargo_toml).await });
-
- if let Some(message) = message_task.await.log_err() {
- conversation
- .update(&mut cx, |conversation, cx| {
- conversation.ambient_context.current_project.message = message;
- conversation.count_remaining_tokens(cx);
- cx.notify();
- })
- .log_err();
- }
- }));
-
- ContextUpdated::Updating
- }
-
- async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
- let buffer = fs.load(path_to_cargo_toml).await?;
- let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
-
- let mut message = String::new();
- writeln!(message, "You are in a Rust project.")?;
-
- if let Some(workspace) = cargo_toml.workspace {
- writeln!(
- message,
- "The project is a Cargo workspace with the following members:"
- )?;
- for member in workspace.members {
- writeln!(message, "- {member}")?;
- }
-
- if !workspace.default_members.is_empty() {
- writeln!(message, "The default members are:")?;
- for member in workspace.default_members {
- writeln!(message, "- {member}")?;
- }
- }
-
- if !workspace.dependencies.is_empty() {
- writeln!(
- message,
- "The following workspace dependencies are installed:"
- )?;
- for dependency in workspace.dependencies.keys() {
- writeln!(message, "- {dependency}")?;
- }
- }
- } else if let Some(package) = cargo_toml.package {
- writeln!(
- message,
- "The project name is \"{name}\".",
- name = package.name
- )?;
-
- let description = package
- .description
- .as_ref()
- .and_then(|description| description.get().ok().cloned());
- if let Some(description) = description.as_ref() {
- writeln!(message, "It describes itself as \"{description}\".")?;
- }
-
- if !cargo_toml.dependencies.is_empty() {
- writeln!(message, "The following dependencies are installed:")?;
- for dependency in cargo_toml.dependencies.keys() {
- writeln!(message, "- {dependency}")?;
- }
- }
- }
-
- Ok(message)
- }
-
- fn path_to_cargo_toml(
- project: WeakModel<Project>,
- cx: &mut AsyncAppContext,
- ) -> Result<Option<PathBuf>> {
- cx.update(|cx| {
- let worktree = project.update(cx, |project, _cx| {
- project
- .worktrees()
- .next()
- .ok_or_else(|| anyhow!("no worktree"))
- })??;
-
- let path_to_cargo_toml = worktree.update(cx, |worktree, _cx| {
- let cargo_toml = worktree.entry_for_path("Cargo.toml")?;
- Some(ProjectPath {
- worktree_id: worktree.id(),
- path: cargo_toml.path.clone(),
- })
- });
- let path_to_cargo_toml = path_to_cargo_toml.and_then(|path| {
- project
- .update(cx, |project, cx| project.absolute_path(&path, cx))
- .ok()
- .flatten()
- });
-
- Ok(path_to_cargo_toml)
- })?
- }
-}
@@ -1,147 +0,0 @@
-use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
-use gpui::{ModelContext, Subscription, Task, WeakModel};
-use language::{Buffer, BufferSnapshot, Rope};
-use std::{fmt::Write, path::PathBuf, time::Duration};
-
-use super::ContextUpdated;
-
-pub struct RecentBuffersContext {
- pub enabled: bool,
- pub buffers: Vec<RecentBuffer>,
- pub snapshot: RecentBuffersSnapshot,
- pub pending_message: Option<Task<()>>,
-}
-
-pub struct RecentBuffer {
- pub buffer: WeakModel<Buffer>,
- pub _subscription: Subscription,
-}
-
-impl Default for RecentBuffersContext {
- fn default() -> Self {
- Self {
- enabled: true,
- buffers: Vec::new(),
- snapshot: RecentBuffersSnapshot::default(),
- pending_message: None,
- }
- }
-}
-
-impl RecentBuffersContext {
- pub fn update(&mut self, cx: &mut ModelContext<Conversation>) -> ContextUpdated {
- let source_buffers = self
- .buffers
- .iter()
- .filter_map(|recent| {
- let (full_path, snapshot) = recent
- .buffer
- .read_with(cx, |buffer, cx| {
- (
- buffer.file().map(|file| file.full_path(cx)),
- buffer.snapshot(),
- )
- })
- .ok()?;
- Some(SourceBufferSnapshot {
- full_path,
- model: recent.buffer.clone(),
- snapshot,
- })
- })
- .collect::<Vec<_>>();
-
- if !self.enabled || source_buffers.is_empty() {
- self.snapshot.message = Default::default();
- self.snapshot.source_buffers.clear();
- self.pending_message = None;
- cx.notify();
- ContextUpdated::Disabled
- } else {
- self.pending_message = Some(cx.spawn(|this, mut cx| async move {
- const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
- cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
-
- let message = if source_buffers.is_empty() {
- Rope::new()
- } else {
- cx.background_executor()
- .spawn({
- let source_buffers = source_buffers.clone();
- async move { message_for_recent_buffers(source_buffers) }
- })
- .await
- };
- this.update(&mut cx, |this, cx| {
- this.ambient_context.recent_buffers.snapshot.source_buffers = source_buffers;
- this.ambient_context.recent_buffers.snapshot.message = message;
- this.count_remaining_tokens(cx);
- cx.notify();
- })
- .ok();
- }));
-
- ContextUpdated::Updating
- }
- }
-
- /// Returns the [`RecentBuffersContext`] as a message to the language model.
- pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
- self.enabled
- .then(|| LanguageModelRequestMessage {
- role: Role::System,
- content: self.snapshot.message.to_string(),
- })
- .filter(|message| !message.content.is_empty())
- }
-}
-
-#[derive(Clone, Default, Debug)]
-pub struct RecentBuffersSnapshot {
- pub message: Rope,
- pub source_buffers: Vec<SourceBufferSnapshot>,
-}
-
-#[derive(Clone)]
-pub struct SourceBufferSnapshot {
- pub full_path: Option<PathBuf>,
- pub model: WeakModel<Buffer>,
- pub snapshot: BufferSnapshot,
-}
-
-impl std::fmt::Debug for SourceBufferSnapshot {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("SourceBufferSnapshot")
- .field("full_path", &self.full_path)
- .field("model (entity id)", &self.model.entity_id())
- .field("snapshot (text)", &self.snapshot.text())
- .finish()
- }
-}
-
-fn message_for_recent_buffers(buffers: Vec<SourceBufferSnapshot>) -> Rope {
- let mut message = String::new();
- writeln!(
- message,
- "The following is a list of recent buffers that the user has opened."
- )
- .unwrap();
-
- for buffer in buffers {
- if let Some(path) = buffer.full_path {
- writeln!(message, "```{}", path.display()).unwrap();
- } else {
- writeln!(message, "```untitled").unwrap();
- }
-
- for chunk in buffer.snapshot.chunks(0..buffer.snapshot.len(), false) {
- message.push_str(chunk.text);
- }
- if !message.ends_with('\n') {
- message.push('\n');
- }
- message.push_str("```\n");
- }
-
- Rope::from(message.as_str())
-}
@@ -1,17 +1,15 @@
-mod ambient_context;
pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
-mod omit_ranges;
mod prompts;
mod saved_conversation;
mod search;
mod slash_command;
mod streaming_diff;
-use ambient_context::AmbientContextSnapshot;
pub use assistant_panel::AssistantPanel;
+
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
@@ -38,7 +36,8 @@ actions!(
InsertActivePrompt,
ToggleIncludeConversation,
ToggleHistory,
- ApplyEdit
+ ApplyEdit,
+ ConfirmCommand
]
);
@@ -188,9 +187,6 @@ pub struct LanguageModelChoiceDelta {
struct MessageMetadata {
role: Role,
status: MessageStatus,
- // TODO: Delete this
- #[serde(skip)]
- ambient_context: AmbientContextSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -1,51 +1,48 @@
-use crate::ambient_context::{AmbientContext, ContextUpdated, RecentBuffer};
use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
use crate::{
- ambient_context::*,
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
codegen::{self, Codegen, CodegenKind},
- omit_ranges::text_in_range_omitting_ranges,
search::*,
slash_command::{
- current_file_command, file_command, prompt_command, SlashCommandCleanup,
+ active_command, file_command, project_command, prompt_command,
SlashCommandCompletionProvider, SlashCommandLine, SlashCommandRegistry,
},
- ApplyEdit, Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel,
- LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
- QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
- Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation,
+ ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
+ LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata,
+ MessageStatus, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
+ SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation,
};
use anyhow::{anyhow, Result};
+use assistant_slash_command::{RenderFoldPlaceholder, SlashCommandOutput};
use client::telemetry::Telemetry;
use collections::{hash_map, HashMap, HashSet, VecDeque};
-use editor::FoldPlaceholder;
use editor::{
actions::{FoldAt, MoveDown, MoveUp},
display_map::{
- BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, FlapId,
- ToDisplayPoint,
+ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint,
},
scroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, RowExt,
ToOffset as _, ToPoint,
};
+use editor::{display_map::FlapId, FoldPlaceholder};
use file_icons::FileIcons;
use fs::Fs;
use futures::StreamExt;
use gpui::{
canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
- AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty, Entity,
+ AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty,
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle,
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace,
WindowContext,
};
-use language::LspAdapterDelegate;
use language::{
- language_settings::SoftWrap, AutoindentMode, Buffer, BufferSnapshot, LanguageRegistry,
- OffsetRangeExt as _, Point, ToOffset as _, ToPoint as _,
+ language_settings::SoftWrap, AutoindentMode, Buffer, LanguageRegistry, OffsetRangeExt as _,
+ Point, ToOffset as _,
};
+use language::{LineEnding, LspAdapterDelegate};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
@@ -54,7 +51,7 @@ use settings::Settings;
use std::{
cmp::{self, Ordering},
fmt::Write,
- iter, mem,
+ iter,
ops::Range,
path::PathBuf,
sync::Arc,
@@ -71,13 +68,10 @@ use uuid::Uuid;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
searchable::Direction,
- Event as WorkspaceEvent, Save, Toast, ToggleZoom, Toolbar, Workspace,
+ Save, Toast, ToggleZoom, Toolbar, Workspace,
};
use workspace::{notifications::NotificationId, NewFile};
-const MAX_RECENT_BUFFERS: usize = 3;
-const SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(200);
-
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -205,7 +199,6 @@ impl AssistantPanel {
.detach();
let slash_command_registry = SlashCommandRegistry::global(cx);
- let window = cx.window_handle().downcast::<Workspace>();
slash_command_registry.register_command(file_command::FileSlashCommand::new(
workspace.project().clone(),
@@ -213,11 +206,8 @@ impl AssistantPanel {
slash_command_registry.register_command(
prompt_command::PromptSlashCommand::new(prompt_library.clone()),
);
- if let Some(window) = window {
- slash_command_registry.register_command(
- current_file_command::CurrentFileSlashCommand::new(window),
- );
- }
+ slash_command_registry.register_command(active_command::ActiveSlashCommand);
+ slash_command_registry.register_command(project_command::ProjectSlashCommand);
Self {
workspace: workspace_handle,
@@ -1145,7 +1135,6 @@ impl AssistantPanel {
languages,
slash_commands,
Some(telemetry),
- lsp_adapter_delegate,
&mut cx,
)
.await?;
@@ -1155,7 +1144,13 @@ impl AssistantPanel {
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))?;
let editor = cx.new_view(|cx| {
- ConversationEditor::for_conversation(conversation, fs, workspace, cx)
+ ConversationEditor::for_conversation(
+ conversation,
+ fs,
+ workspace,
+ lsp_adapter_delegate,
+ cx,
+ )
});
this.show_conversation(editor, cx);
anyhow::Ok(())
@@ -1456,9 +1451,14 @@ enum ConversationEvent {
SummaryChanged,
EditSuggestionsChanged,
StreamedCompletion,
- SlashCommandsChanged,
- SlashCommandOutputAdded(Range<language::Anchor>),
- SlashCommandOutputRemoved(Range<language::Anchor>),
+ PendingSlashCommandsUpdated {
+ removed: Vec<Range<language::Anchor>>,
+ updated: Vec<PendingSlashCommand>,
+ },
+ SlashCommandFinished {
+ output_range: Range<language::Anchor>,
+ render_placeholder: RenderFoldPlaceholder,
+ },
}
#[derive(Default)]
@@ -1467,12 +1467,27 @@ struct Summary {
done: bool,
}
+#[derive(Copy, Clone, Default, Eq, PartialEq, Hash)]
+pub struct SlashCommandInvocationId(usize);
+
+impl SlashCommandInvocationId {
+ fn post_inc(&mut self) -> Self {
+ let id = *self;
+ self.0 += 1;
+ id
+ }
+}
+
+struct SlashCommandInvocation {
+ _pending_output: Task<Option<()>>,
+}
+
pub struct Conversation {
id: Option<String>,
buffer: Model<Buffer>,
- pub(crate) ambient_context: AmbientContext,
edit_suggestions: Vec<EditSuggestion>,
- slash_command_calls: Vec<SlashCommandCall>,
+ pending_slash_commands: Vec<PendingSlashCommand>,
+ edits_since_last_slash_command_parse: language::Subscription,
message_anchors: Vec<MessageAnchor>,
messages_metadata: HashMap<MessageId, MessageMetadata>,
next_message_id: MessageId,
@@ -1484,14 +1499,14 @@ pub struct Conversation {
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
pending_edit_suggestion_parse: Option<Task<()>>,
- pending_command_invocation_parse: Option<Task<()>>,
pending_save: Task<Result<()>>,
path: Option<PathBuf>,
+ invocations: HashMap<SlashCommandInvocationId, SlashCommandInvocation>,
+ next_invocation_id: SlashCommandInvocationId,
_subscriptions: Vec<Subscription>,
telemetry: Option<Arc<Telemetry>>,
slash_command_registry: Arc<SlashCommandRegistry>,
language_registry: Arc<LanguageRegistry>,
- lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
}
impl EventEmitter<ConversationEvent> for Conversation {}
@@ -1502,7 +1517,6 @@ impl Conversation {
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
telemetry: Option<Arc<Telemetry>>,
- lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut ModelContext<Self>,
) -> Self {
let buffer = cx.new_model(|cx| {
@@ -1510,15 +1524,16 @@ impl Conversation {
buffer.set_language_registry(language_registry.clone());
buffer
});
-
+ let edits_since_last_slash_command_parse =
+ buffer.update(cx, |buffer, _| buffer.subscribe());
let mut this = Self {
id: Some(Uuid::new_v4().to_string()),
message_anchors: Default::default(),
messages_metadata: Default::default(),
next_message_id: Default::default(),
- ambient_context: AmbientContext::default(),
edit_suggestions: Vec::new(),
- slash_command_calls: Vec::new(),
+ pending_slash_commands: Vec::new(),
+ edits_since_last_slash_command_parse,
summary: None,
pending_summary: Task::ready(None),
completion_count: Default::default(),
@@ -1526,16 +1541,16 @@ impl Conversation {
token_count: None,
pending_token_count: Task::ready(None),
pending_edit_suggestion_parse: None,
- pending_command_invocation_parse: None,
+ next_invocation_id: SlashCommandInvocationId::default(),
+ invocations: HashMap::default(),
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
path: None,
buffer,
telemetry,
- slash_command_registry,
language_registry,
- lsp_adapter_delegate,
+ slash_command_registry,
};
let message = MessageAnchor {
@@ -1548,7 +1563,6 @@ impl Conversation {
MessageMetadata {
role: Role::User,
status: MessageStatus::Done,
- ambient_context: AmbientContextSnapshot::default(),
},
);
@@ -1587,7 +1601,6 @@ impl Conversation {
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
telemetry: Option<Arc<Telemetry>>,
- lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut AsyncAppContext,
) -> Result<Model<Self>> {
let id = match saved_conversation.id {
@@ -1620,14 +1633,16 @@ impl Conversation {
})?;
cx.new_model(move |cx| {
+ let edits_since_last_slash_command_parse =
+ buffer.update(cx, |buffer, _| buffer.subscribe());
let mut this = Self {
id,
message_anchors,
messages_metadata: saved_conversation.message_metadata,
next_message_id,
- ambient_context: AmbientContext::default(),
edit_suggestions: Vec::new(),
- slash_command_calls: Vec::new(),
+ pending_slash_commands: Vec::new(),
+ edits_since_last_slash_command_parse,
summary: Some(Summary {
text: saved_conversation.summary,
done: true,
@@ -1637,8 +1652,9 @@ impl Conversation {
pending_completions: Default::default(),
token_count: None,
pending_edit_suggestion_parse: None,
- pending_command_invocation_parse: None,
pending_token_count: Task::ready(None),
+ next_invocation_id: SlashCommandInvocationId::default(),
+ invocations: HashMap::default(),
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
@@ -1647,7 +1663,6 @@ impl Conversation {
telemetry,
language_registry,
slash_command_registry,
- lsp_adapter_delegate,
};
this.set_language(cx);
this.reparse_edit_suggestions(cx);
@@ -1668,60 +1683,6 @@ impl Conversation {
.detach_and_log_err(cx);
}
- fn toggle_recent_buffers(&mut self, cx: &mut ModelContext<Self>) {
- self.ambient_context.recent_buffers.enabled = !self.ambient_context.recent_buffers.enabled;
- match self.ambient_context.recent_buffers.update(cx) {
- ContextUpdated::Updating => {}
- ContextUpdated::Disabled => {
- self.count_remaining_tokens(cx);
- }
- }
- }
-
- fn toggle_current_project_context(
- &mut self,
- fs: Arc<dyn Fs>,
- project: WeakModel<Project>,
- cx: &mut ModelContext<Self>,
- ) {
- self.ambient_context.current_project.enabled =
- !self.ambient_context.current_project.enabled;
- match self.ambient_context.current_project.update(fs, project, cx) {
- ContextUpdated::Updating => {}
- ContextUpdated::Disabled => {
- self.count_remaining_tokens(cx);
- }
- }
- }
-
- fn set_recent_buffers(
- &mut self,
- buffers: impl IntoIterator<Item = Model<Buffer>>,
- cx: &mut ModelContext<Self>,
- ) {
- self.ambient_context.recent_buffers.buffers.clear();
- self.ambient_context
- .recent_buffers
- .buffers
- .extend(buffers.into_iter().map(|buffer| RecentBuffer {
- buffer: buffer.downgrade(),
- _subscription: cx.observe(&buffer, |this, _, cx| {
- match this.ambient_context.recent_buffers.update(cx) {
- ContextUpdated::Updating => {}
- ContextUpdated::Disabled => {
- this.count_remaining_tokens(cx);
- }
- }
- }),
- }));
- match self.ambient_context.recent_buffers.update(cx) {
- ContextUpdated::Updating => {}
- ContextUpdated::Disabled => {
- self.count_remaining_tokens(cx);
- }
- }
- }
-
fn handle_buffer_event(
&mut self,
_: Model<Buffer>,
@@ -1731,7 +1692,7 @@ impl Conversation {
if *event == language::Event::Edited {
self.count_remaining_tokens(cx);
self.reparse_edit_suggestions(cx);
- self.reparse_slash_command_calls(cx);
+ self.reparse_slash_commands(cx);
cx.emit(ConversationEvent::MessagesEdited);
}
}
@@ -1758,6 +1719,94 @@ impl Conversation {
});
}
+ fn reparse_slash_commands(&mut self, cx: &mut ModelContext<Self>) {
+ let buffer = self.buffer.read(cx);
+ let mut row_ranges = self
+ .edits_since_last_slash_command_parse
+ .consume()
+ .into_iter()
+ .map(|edit| {
+ let start_row = buffer.offset_to_point(edit.new.start).row;
+ let end_row = buffer.offset_to_point(edit.new.end).row + 1;
+ start_row..end_row
+ })
+ .peekable();
+
+ let mut removed = Vec::new();
+ let mut updated = Vec::new();
+ while let Some(mut row_range) = row_ranges.next() {
+ while let Some(next_row_range) = row_ranges.peek() {
+ if row_range.end >= next_row_range.start {
+ row_range.end = next_row_range.end;
+ row_ranges.next();
+ } else {
+ break;
+ }
+ }
+
+ let start = buffer.anchor_before(Point::new(row_range.start, 0));
+ let end = buffer.anchor_after(Point::new(
+ row_range.end - 1,
+ 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 mut new_commands = Vec::new();
+ let mut lines = buffer.text_for_range(start..end).lines();
+ let mut offset = lines.offset();
+ while let Some(line) = lines.next() {
+ if let Some(command_line) = SlashCommandLine::parse(line) {
+ let name = &line[command_line.name.clone()];
+ let argument = command_line.argument.as_ref().and_then(|argument| {
+ (!argument.is_empty()).then_some(&line[argument.clone()])
+ });
+ if let Some(command) = self.slash_command_registry.command(name) {
+ if !command.requires_argument() || argument.is_some() {
+ let start_ix = offset + command_line.name.start - 1;
+ let end_ix = offset
+ + command_line
+ .argument
+ .map_or(command_line.name.end, |argument| argument.end);
+ let source_range =
+ buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
+ let pending_command = PendingSlashCommand {
+ name: name.to_string(),
+ argument: argument.map(ToString::to_string),
+ tooltip_text: command.tooltip_text().into(),
+ source_range,
+ };
+ updated.push(pending_command.clone());
+ new_commands.push(pending_command);
+ }
+ }
+ }
+
+ offset = lines.offset();
+ }
+
+ let removed_commands = self
+ .pending_slash_commands
+ .splice(start_ix..end_ix, new_commands);
+ removed.extend(removed_commands.map(|command| command.source_range));
+ }
+
+ if !updated.is_empty() || !removed.is_empty() {
+ cx.emit(ConversationEvent::PendingSlashCommandsUpdated { removed, updated });
+ }
+ }
+
fn reparse_edit_suggestions(&mut self, cx: &mut ModelContext<Self>) {
self.pending_edit_suggestion_parse = Some(cx.spawn(|this, mut cx| async move {
cx.background_executor()
@@ -1817,222 +1866,72 @@ impl Conversation {
cx.notify();
}
- fn reparse_slash_command_calls(&mut self, cx: &mut ModelContext<Self>) {
- self.pending_command_invocation_parse = Some(cx.spawn(|this, mut cx| async move {
- cx.background_executor().timer(SLASH_COMMAND_DEBOUNCE).await;
-
- this.update(&mut cx, |this, cx| {
- let buffer = this.buffer.read(cx).snapshot();
-
- let mut changed = false;
- let mut new_calls = Vec::new();
- let mut old_calls = mem::take(&mut this.slash_command_calls)
- .into_iter()
- .peekable();
- let mut lines = buffer.as_rope().chunks().lines();
- let mut offset = 0;
- while let Some(line) = lines.next() {
- let line_end_offset = offset + line.len();
- if let Some(call) = SlashCommandLine::parse(line) {
- let mut unchanged_call = None;
- while let Some(old_call) = old_calls.peek() {
- match old_call.source_range.start.to_offset(&buffer).cmp(&offset) {
- Ordering::Greater => break,
- Ordering::Equal
- if this.slash_command_is_unchanged(
- old_call, &call, line, &buffer,
- ) =>
- {
- unchanged_call = old_calls.next();
- }
- _ => {
- changed = true;
- let old_call = old_calls.next().unwrap();
- this.slash_command_call_removed(old_call, cx);
- }
- }
- }
-
- let name = &line[call.name];
- if let Some(call) = unchanged_call {
- new_calls.push(call);
- } else if let Some((command, lsp_adapter_delegate)) = this
- .slash_command_registry
- .command(name)
- .zip(this.lsp_adapter_delegate.clone())
- {
- changed = true;
- let name = name.to_string();
- let source_range =
- buffer.anchor_after(offset)..buffer.anchor_before(line_end_offset);
-
- let argument = call.argument.map(|range| &line[range]);
- let invocation = command.run(argument, lsp_adapter_delegate, cx);
-
- new_calls.push(SlashCommandCall {
- name,
- argument: argument.map(|s| s.to_string()),
- source_range: source_range.clone(),
- output_range: None,
- should_rerun: false,
- _invalidate: cx.spawn(|this, mut cx| {
- let source_range = source_range.clone();
- let invalidated = invocation.invalidated;
- async move {
- if invalidated.await.is_ok() {
- _ = this.update(&mut cx, |this, cx| {
- let buffer = this.buffer.read(cx);
- let call_ix = this
- .slash_command_calls
- .binary_search_by(|probe| {
- probe
- .source_range
- .start
- .cmp(&source_range.start, buffer)
- });
- if let Ok(call_ix) = call_ix {
- this.slash_command_calls[call_ix]
- .should_rerun = true;
- this.reparse_slash_command_calls(cx);
- }
- });
- }
- }
- }),
- _command_cleanup: invocation.cleanup,
- });
-
- cx.spawn(|this, mut cx| async move {
- let output = invocation.output.await;
- this.update(&mut cx, |this, cx| {
- let output_range = this.buffer.update(cx, |buffer, cx| {
- let call_ix = this
- .slash_command_calls
- .binary_search_by(|probe| {
- probe
- .source_range
- .start
- .cmp(&source_range.start, buffer)
- })
- .ok()?;
-
- let mut output = output.log_err()?;
- output.truncate(output.trim_end().len());
-
- let source_end = source_range.end.to_offset(buffer);
- let output_start = source_end + '\n'.len_utf8();
- let output_end = output_start + output.len();
-
- if buffer
- .chars_at(source_end)
- .next()
- .map_or(false, |c| c != '\n')
- {
- output.push('\n');
- }
-
- buffer.edit(
- [
- (source_end..source_end, "\n".to_string()),
- (source_end..source_end, output),
- ],
- None,
- cx,
- );
-
- let output_start = buffer.anchor_after(output_start);
- let output_end = buffer.anchor_before(output_end);
- this.slash_command_calls[call_ix].output_range =
- Some(output_start..output_end);
- Some(source_range.end..output_end)
- });
- if let Some(output_range) = output_range {
- cx.emit(ConversationEvent::SlashCommandOutputAdded(
- output_range,
- ));
- cx.emit(ConversationEvent::SlashCommandsChanged);
- }
- })
- .ok();
- })
- .detach();
- }
- }
- offset = lines.offset();
- }
-
- for old_call in old_calls {
- changed = true;
- this.slash_command_call_removed(old_call, cx);
- }
-
- if changed {
- cx.emit(ConversationEvent::SlashCommandsChanged);
+ fn pending_command_for_position(
+ &self,
+ position: language::Anchor,
+ cx: &AppContext,
+ ) -> Option<&PendingSlashCommand> {
+ let buffer = self.buffer.read(cx);
+ let ix = 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
+ } else {
+ Ordering::Equal
}
-
- this.slash_command_calls = new_calls;
})
- .ok();
- }));
+ .ok()?;
+ self.pending_slash_commands.get(ix)
}
- fn slash_command_is_unchanged(
- &self,
- old_call: &SlashCommandCall,
- new_call: &SlashCommandLine,
- new_text: &str,
- buffer: &BufferSnapshot,
- ) -> bool {
- if old_call.name != new_text[new_call.name.clone()] {
- return false;
- }
+ fn insert_command_output(
+ &mut self,
+ invocation_id: SlashCommandInvocationId,
+ command_range: Range<language::Anchor>,
+ output: Task<Result<SlashCommandOutput>>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ let insert_output_task = cx.spawn(|this, mut cx| {
+ async move {
+ let output = output.await?;
- if old_call.argument.as_deref() != new_call.argument.clone().map(|range| &new_text[range]) {
- return false;
- }
+ let mut text = output.text;
+ LineEnding::normalize(&mut text);
+ if !text.ends_with('\n') {
+ text.push('\n');
+ }
- if old_call.should_rerun {
- return false;
- }
+ this.update(&mut cx, |this, cx| {
+ let output_range = 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 + text.len();
+ buffer.edit([(start..old_end, text)], None, cx);
+ if buffer.chars_at(new_end).next() != Some('\n') {
+ buffer.edit([(new_end..new_end, "\n")], None, cx);
+ }
+ buffer.anchor_after(start)..buffer.anchor_before(new_end)
+ });
+ cx.emit(ConversationEvent::SlashCommandFinished {
+ output_range,
+ render_placeholder: output.render_placeholder,
+ });
+ })?;
- if let Some(output_range) = &old_call.output_range {
- let source_range = old_call.source_range.to_point(buffer);
- let output_start = output_range.start.to_point(buffer);
- if source_range.start.column != 0 {
- return false;
- }
- if source_range.end.column != new_text.len() as u32 {
- return false;
- }
- if output_start != Point::new(source_range.end.row + 1, 0) {
- return false;
- }
- if let Some(next_char) = buffer.chars_at(output_range.end).next() {
- if next_char != '\n' {
- return false;
- }
+ anyhow::Ok(())
}
- }
- true
- }
+ .log_err()
+ });
- fn slash_command_call_removed(
- &self,
- old_call: SlashCommandCall,
- cx: &mut ModelContext<Conversation>,
- ) {
- if let Some(output_range) = old_call.output_range {
- self.buffer.update(cx, |buffer, cx| {
- buffer.edit(
- [(old_call.source_range.end..output_range.end, "")],
- None,
- cx,
- );
- });
- cx.emit(ConversationEvent::SlashCommandOutputRemoved(
- old_call.source_range.end..output_range.end,
- ))
- }
+ self.invocations.insert(
+ invocation_id,
+ SlashCommandInvocation {
+ _pending_output: insert_output_task,
+ },
+ );
}
fn remaining_tokens(&self) -> Option<isize> {
@@ -2207,18 +2106,11 @@ impl Conversation {
content: include_str!("./system_prompts/edits.md").to_string(),
};
- let recent_buffers_context = self.ambient_context.recent_buffers.to_message();
- let current_project_context = self.ambient_context.current_project.to_message();
-
- let messages = Some(edits_system_prompt)
- .into_iter()
- .chain(recent_buffers_context)
- .chain(current_project_context)
- .chain(
- self.messages(cx)
- .filter(|message| matches!(message.status, MessageStatus::Done))
- .map(|message| message.to_request_message(self.buffer.read(cx))),
- );
+ let messages = Some(edits_system_prompt).into_iter().chain(
+ self.messages(cx)
+ .filter(|message| matches!(message.status, MessageStatus::Done))
+ .map(|message| message.to_request_message(self.buffer.read(cx))),
+ );
LanguageModelRequest {
model: self.model.clone(),
@@ -2277,14 +2169,8 @@ impl Conversation {
};
self.message_anchors
.insert(next_message_ix, message.clone());
- self.messages_metadata.insert(
- message.id,
- MessageMetadata {
- role,
- status,
- ambient_context: self.ambient_context.snapshot(),
- },
- );
+ self.messages_metadata
+ .insert(message.id, MessageMetadata { role, status });
cx.emit(ConversationEvent::MessagesEdited);
Some(message)
} else {
@@ -2342,7 +2228,6 @@ impl Conversation {
MessageMetadata {
role,
status: MessageStatus::Done,
- ambient_context: message.ambient_context.clone(),
},
);
@@ -2387,7 +2272,6 @@ impl Conversation {
MessageMetadata {
role,
status: MessageStatus::Done,
- ambient_context: message.ambient_context,
},
);
(Some(selection), Some(suffix))
@@ -2493,17 +2377,6 @@ impl Conversation {
fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
let buffer = self.buffer.read(cx);
- let mut slash_command_calls = self
- .slash_command_calls
- .iter()
- .map(|call| {
- if let Some(output) = &call.output_range {
- call.source_range.start.to_offset(buffer)..output.start.to_offset(buffer)
- } else {
- call.source_range.to_offset(buffer)
- }
- })
- .peekable();
let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
iter::from_fn(move || {
if let Some((start_ix, message_anchor)) = message_anchors.next() {
@@ -2524,15 +2397,6 @@ impl Conversation {
.unwrap_or(language::Anchor::MAX)
.to_offset(buffer);
- let mut slash_command_ranges = Vec::new();
- while let Some(call_range) = slash_command_calls.peek() {
- if call_range.end <= message_end {
- slash_command_ranges.push(slash_command_calls.next().unwrap());
- } else {
- break;
- }
- }
-
return Some(Message {
index_range: start_ix..end_ix,
offset_range: message_start..message_end,
@@ -2540,8 +2404,6 @@ impl Conversation {
anchor: message_anchor.start,
role: metadata.role,
status: metadata.status.clone(),
- slash_command_ranges,
- ambient_context: metadata.ambient_context.clone(),
});
}
None
@@ -2699,14 +2561,12 @@ fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option<ParsedEditSugge
}
}
-struct SlashCommandCall {
- source_range: Range<language::Anchor>,
- output_range: Option<Range<language::Anchor>>,
+#[derive(Clone)]
+struct PendingSlashCommand {
name: String,
argument: Option<String>,
- should_rerun: bool,
- _invalidate: Task<()>,
- _command_cleanup: SlashCommandCleanup,
+ source_range: Range<language::Anchor>,
+ tooltip_text: SharedString,
}
struct PendingCompletion {
@@ -2724,14 +2584,16 @@ struct ScrollPosition {
cursor: Anchor,
}
-struct ConversationEditor {
+pub struct ConversationEditor {
conversation: Model<Conversation>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
+ slash_command_registry: Arc<SlashCommandRegistry>,
+ lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
editor: View<Editor>,
- flap_ids: HashMap<Range<language::Anchor>, FlapId>,
blocks: HashSet<BlockId>,
scroll_position: Option<ScrollPosition>,
+ pending_slash_command_flaps: HashMap<Range<language::Anchor>, FlapId>,
_subscriptions: Vec<Subscription>,
}
@@ -2754,21 +2616,27 @@ impl ConversationEditor {
language_registry,
slash_command_registry,
Some(telemetry),
- lsp_adapter_delegate,
cx,
)
});
- Self::for_conversation(conversation, fs, workspace, cx)
+
+ Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, cx)
}
fn for_conversation(
conversation: Model<Conversation>,
fs: Arc<dyn Fs>,
workspace: View<Workspace>,
+ lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut ViewContext<Self>,
) -> Self {
- let command_registry = conversation.read(cx).slash_command_registry.clone();
- let completion_provider = SlashCommandCompletionProvider::new(command_registry);
+ let slash_command_registry = conversation.read(cx).slash_command_registry.clone();
+
+ let completion_provider = SlashCommandCompletionProvider::new(
+ cx.view().downgrade(),
+ slash_command_registry.clone(),
+ workspace.downgrade(),
+ );
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
@@ -2786,20 +2654,20 @@ impl ConversationEditor {
cx.observe(&conversation, |_, _, cx| cx.notify()),
cx.subscribe(&conversation, Self::handle_conversation_event),
cx.subscribe(&editor, Self::handle_editor_event),
- cx.subscribe(&workspace, Self::handle_workspace_event),
];
let mut this = Self {
conversation,
editor,
+ slash_command_registry,
+ lsp_adapter_delegate,
blocks: Default::default(),
scroll_position: None,
- flap_ids: Default::default(),
fs,
workspace: workspace.downgrade(),
+ pending_slash_command_flaps: HashMap::default(),
_subscriptions,
};
- this.update_recent_editors(cx);
this.update_message_headers(cx);
this
}
@@ -2866,12 +2734,68 @@ impl ConversationEditor {
.collect()
}
+ pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
+ let selections = self.editor.read(cx).selections.disjoint_anchors();
+ let mut commands_by_range = HashMap::default();
+ let workspace = self.workspace.clone();
+ self.conversation.update(cx, |conversation, cx| {
+ for selection in selections.iter() {
+ if let Some(command) =
+ conversation.pending_command_for_position(selection.head().text_anchor, cx)
+ {
+ commands_by_range
+ .entry(command.source_range.clone())
+ .or_insert_with(|| command.clone());
+ }
+ }
+ });
+
+ if commands_by_range.is_empty() {
+ cx.propagate();
+ } else {
+ for command in commands_by_range.into_values() {
+ self.run_command(
+ command.source_range,
+ &command.name,
+ command.argument.as_deref(),
+ workspace.clone(),
+ cx,
+ );
+ }
+ cx.stop_propagation();
+ }
+ }
+
+ pub fn run_command(
+ &mut self,
+ command_range: Range<language::Anchor>,
+ name: &str,
+ argument: Option<&str>,
+ workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<SlashCommandInvocationId> {
+ let command = self.slash_command_registry.command(name)?;
+ let lsp_adapter_delegate = self.lsp_adapter_delegate.clone()?;
+ let argument = argument.map(ToString::to_string);
+ let id = self.conversation.update(cx, |conversation, _| {
+ conversation.next_invocation_id.post_inc()
+ });
+ let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
+ self.conversation.update(cx, |conversation, cx| {
+ conversation.insert_command_output(id, command_range, output, cx)
+ });
+
+ Some(id)
+ }
+
fn handle_conversation_event(
&mut self,
_: Model<Conversation>,
event: &ConversationEvent,
cx: &mut ViewContext<Self>,
) {
+ let conversation_editor = cx.view().downgrade();
+
match event {
ConversationEvent::MessagesEdited => {
self.update_message_headers(cx);
@@ -1,101 +0,0 @@
-use rope::Rope;
-use std::{cmp::Ordering, ops::Range};
-
-pub(crate) fn text_in_range_omitting_ranges(
- rope: &Rope,
- range: Range<usize>,
- omit_ranges: &[Range<usize>],
-) -> String {
- let mut content = String::with_capacity(range.len());
- let mut omit_ranges = omit_ranges
- .iter()
- .skip_while(|omit_range| omit_range.end <= range.start)
- .peekable();
- let mut offset = range.start;
- let mut chunks = rope.chunks_in_range(range.clone());
- while let Some(chunk) = chunks.next() {
- if let Some(omit_range) = omit_ranges.peek() {
- match offset.cmp(&omit_range.start) {
- Ordering::Less => {
- let max_len = omit_range.start - offset;
- if chunk.len() < max_len {
- content.push_str(chunk);
- offset += chunk.len();
- } else {
- content.push_str(&chunk[..max_len]);
- chunks.seek(omit_range.end.min(range.end));
- offset = omit_range.end;
- omit_ranges.next();
- }
- }
- Ordering::Equal | Ordering::Greater => {
- chunks.seek(omit_range.end.min(range.end));
- offset = omit_range.end;
- omit_ranges.next();
- }
- }
- } else {
- content.push_str(chunk);
- offset += chunk.len();
- }
- }
-
- content
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use rand::{rngs::StdRng, Rng as _};
- use util::RandomCharIter;
-
- #[gpui::test(iterations = 100)]
- fn test_text_in_range_omitting_ranges(mut rng: StdRng) {
- let text = RandomCharIter::new(&mut rng).take(1024).collect::<String>();
- let rope = Rope::from(text.as_str());
-
- let mut start = rng.gen_range(0..=text.len() / 2);
- let mut end = rng.gen_range(text.len() / 2..=text.len());
- while !text.is_char_boundary(start) {
- start -= 1;
- }
- while !text.is_char_boundary(end) {
- end += 1;
- }
- let range = start..end;
-
- let mut ix = 0;
- let mut omit_ranges = Vec::new();
- for _ in 0..rng.gen_range(0..10) {
- let mut start = rng.gen_range(ix..=text.len());
- while !text.is_char_boundary(start) {
- start += 1;
- }
- let mut end = rng.gen_range(start..=text.len());
- while !text.is_char_boundary(end) {
- end += 1;
- }
- omit_ranges.push(start..end);
- ix = end;
- if ix == text.len() {
- break;
- }
- }
-
- let mut expected_text = text[range.clone()].to_string();
- for omit_range in omit_ranges.iter().rev() {
- let start = omit_range
- .start
- .saturating_sub(range.start)
- .min(range.len());
- let end = omit_range.end.saturating_sub(range.start).min(range.len());
- expected_text.replace_range(start..end, "");
- }
-
- assert_eq!(
- text_in_range_omitting_ranges(&rope, range.clone(), &omit_ranges),
- expected_text,
- "text: {text:?}\nrange: {range:?}\nomit_ranges: {omit_ranges:?}"
- );
- }
-}
@@ -1,7 +1,9 @@
+use crate::assistant_panel::ConversationEditor;
use anyhow::Result;
+pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{AppContext, Model, Task, ViewContext};
+use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use rope::Point;
@@ -12,18 +14,18 @@ use std::{
Arc,
},
};
+use workspace::Workspace;
-pub use assistant_slash_command::{
- SlashCommand, SlashCommandCleanup, SlashCommandInvocation, SlashCommandRegistry,
-};
-
-pub mod current_file_command;
+pub mod active_command;
pub mod file_command;
+pub mod project_command;
pub mod prompt_command;
pub(crate) struct SlashCommandCompletionProvider {
+ editor: WeakView<ConversationEditor>,
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
+ workspace: WeakView<Workspace>,
}
pub(crate) struct SlashCommandLine {
@@ -34,18 +36,25 @@ pub(crate) struct SlashCommandLine {
}
impl SlashCommandCompletionProvider {
- pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
+ pub fn new(
+ editor: WeakView<ConversationEditor>,
+ commands: Arc<SlashCommandRegistry>,
+ workspace: WeakView<Workspace>,
+ ) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
+ editor,
commands,
+ workspace,
}
}
fn complete_command_name(
&self,
command_name: &str,
- range: Range<Anchor>,
- cx: &mut AppContext,
+ command_range: Range<Anchor>,
+ name_range: Range<Anchor>,
+ cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let candidates = self
.commands
@@ -60,6 +69,8 @@ impl SlashCommandCompletionProvider {
.collect::<Vec<_>>();
let commands = self.commands.clone();
let command_name = command_name.to_string();
+ let editor = self.editor.clone();
+ let workspace = self.workspace.clone();
let executor = cx.background_executor().clone();
executor.clone().spawn(async move {
let matches = match_strings(
@@ -77,17 +88,37 @@ impl SlashCommandCompletionProvider {
.filter_map(|mat| {
let command = commands.command(&mat.string)?;
let mut new_text = mat.string.clone();
- if command.requires_argument() {
+ let requires_argument = command.requires_argument();
+ if requires_argument {
new_text.push(' ');
}
Some(project::Completion {
- old_range: range.clone(),
+ old_range: name_range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
new_text,
- label: CodeLabel::plain(mat.string, None),
+ label: CodeLabel::plain(mat.string.clone(), None),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
+ confirm: (!requires_argument).then(|| {
+ let command_name = mat.string.clone();
+ let command_range = command_range.clone();
+ let editor = editor.clone();
+ let workspace = workspace.clone();
+ Arc::new(move |cx: &mut WindowContext| {
+ editor
+ .update(cx, |editor, cx| {
+ editor.run_command(
+ command_range.clone(),
+ &command_name,
+ None,
+ workspace.clone(),
+ cx,
+ );
+ })
+ .ok();
+ }) as Arc<_>
+ }),
})
})
.collect())
@@ -98,8 +129,9 @@ impl SlashCommandCompletionProvider {
&self,
command_name: &str,
argument: String,
- range: Range<Anchor>,
- cx: &mut AppContext,
+ command_range: Range<Anchor>,
+ argument_range: Range<Anchor>,
+ cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
@@ -108,17 +140,39 @@ impl SlashCommandCompletionProvider {
if let Some(command) = self.commands.command(command_name) {
let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
+ let command_name: Arc<str> = command_name.into();
+ let editor = self.editor.clone();
+ let workspace = self.workspace.clone();
cx.background_executor().spawn(async move {
Ok(completions
.await?
.into_iter()
.map(|arg| project::Completion {
- old_range: range.clone(),
+ old_range: argument_range.clone(),
label: CodeLabel::plain(arg.clone(), None),
new_text: arg.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
+ confirm: Some(Arc::new({
+ let command_name = command_name.clone();
+ let command_range = command_range.clone();
+ let editor = editor.clone();
+ let workspace = workspace.clone();
+ move |cx| {
+ editor
+ .update(cx, |editor, cx| {
+ editor.run_command(
+ command_range.clone(),
+ &command_name,
+ Some(&arg),
+ workspace.clone(),
+ cx,
+ );
+ })
+ .ok();
+ }
+ })),
})
.collect())
})
@@ -136,25 +190,44 @@ impl CompletionProvider for SlashCommandCompletionProvider {
buffer_position: Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
- let task = 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 name = &line[call.name.clone()];
- if let Some(argument) = call.argument {
- let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
- let argument = line[argument.clone()].to_string();
- Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
- } else {
- let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
- Some(self.complete_command_name(name, start..buffer_position, cx))
- }
- });
+ let Some((name, argument, command_range, 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)?;
- task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
+ let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
+ let command_range_end = Point::new(
+ position.row,
+ call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
+ );
+ let command_range = buffer.anchor_after(command_range_start)
+ ..buffer.anchor_after(command_range_end);
+
+ let name = line[call.name.clone()].to_string();
+
+ Some(if let Some(argument) = call.argument {
+ let start =
+ buffer.anchor_after(Point::new(position.row, argument.start as u32));
+ let argument = line[argument.clone()].to_string();
+ (name, Some(argument), command_range, start..buffer_position)
+ } else {
+ let start =
+ buffer.anchor_after(Point::new(position.row, call.name.start as u32));
+ (name, None, command_range, start..buffer_position)
+ })
+ })
+ else {
+ return Task::ready(Ok(Vec::new()));
+ };
+
+ if let Some(argument) = argument {
+ self.complete_command_argument(&name, argument, command_range, argument_range, cx)
+ } else {
+ self.complete_command_name(&name, command_range, argument_range, cx)
+ }
}
fn resolve_completions(
@@ -0,0 +1,117 @@
+use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
+use anyhow::{anyhow, Result};
+use collections::HashMap;
+use editor::Editor;
+use gpui::{AppContext, Entity, Task, WeakView};
+use language::LspAdapterDelegate;
+use std::{borrow::Cow, sync::Arc};
+use ui::{IntoElement, WindowContext};
+use workspace::Workspace;
+
+pub(crate) struct ActiveSlashCommand;
+
+impl SlashCommand for ActiveSlashCommand {
+ fn name(&self) -> String {
+ "active".into()
+ }
+
+ fn description(&self) -> String {
+ "insert active tab".into()
+ }
+
+ fn tooltip_text(&self) -> String {
+ "insert active tab".into()
+ }
+
+ fn complete_argument(
+ &self,
+ _query: String,
+ _cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
+ _cx: &mut AppContext,
+ ) -> Task<Result<Vec<String>>> {
+ Task::ready(Err(anyhow!("this command does not require argument")))
+ }
+
+ fn requires_argument(&self) -> bool {
+ false
+ }
+
+ fn run(
+ self: Arc<Self>,
+ _argument: Option<&str>,
+ workspace: WeakView<Workspace>,
+ _delegate: Arc<dyn LspAdapterDelegate>,
+ cx: &mut WindowContext,
+ ) -> Task<Result<SlashCommandOutput>> {
+ let output = workspace.update(cx, |workspace, cx| {
+ let mut timestamps_by_entity_id = HashMap::default();
+ 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);
+ }
+ }
+
+ let mut most_recent_buffer = None;
+ for editor in workspace.items_of_type::<Editor>(cx) {
+ let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
+ continue;
+ };
+
+ let timestamp = timestamps_by_entity_id
+ .get(&editor.entity_id())
+ .copied()
+ .unwrap_or_default();
+ if most_recent_buffer
+ .as_ref()
+ .map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
+ {
+ most_recent_buffer = Some((buffer, timestamp));
+ }
+ }
+
+ if let Some((buffer, _)) = most_recent_buffer {
+ let snapshot = buffer.read(cx).snapshot();
+ let path = snapshot.resolve_file_path(cx, true);
+ let text = cx.background_executor().spawn({
+ let path = path.clone();
+ async move {
+ let path = path
+ .as_ref()
+ .map(|path| path.to_string_lossy())
+ .unwrap_or_else(|| Cow::Borrowed("untitled"));
+
+ let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
+ output.push_str("```");
+ output.push_str(&path);
+ output.push('\n');
+ for chunk in snapshot.as_rope().chunks() {
+ output.push_str(chunk);
+ }
+ if !output.ends_with('\n') {
+ output.push('\n');
+ }
+ output.push_str("```");
+ output
+ }
+ });
+ cx.foreground_executor().spawn(async move {
+ Ok(SlashCommandOutput {
+ text: text.await,
+ render_placeholder: Arc::new(move |id, unfold, _| {
+ FilePlaceholder {
+ id,
+ path: path.clone(),
+ unfold,
+ }
+ .into_any_element()
+ }),
+ })
+ })
+ } else {
+ Task::ready(Err(anyhow!("no recent buffer found")))
+ }
+ });
+ output.unwrap_or_else(|error| Task::ready(Err(error)))
+ }
+}
@@ -1,142 +0,0 @@
-use std::sync::Arc;
-use std::{borrow::Cow, cell::Cell, rc::Rc};
-
-use anyhow::{anyhow, Result};
-use collections::HashMap;
-use editor::Editor;
-use futures::channel::oneshot;
-use gpui::{AppContext, Entity, Subscription, Task, WindowHandle};
-use language::LspAdapterDelegate;
-use workspace::{Event as WorkspaceEvent, Workspace};
-
-use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
-
-pub(crate) struct CurrentFileSlashCommand {
- workspace: WindowHandle<Workspace>,
-}
-
-impl CurrentFileSlashCommand {
- pub fn new(workspace: WindowHandle<Workspace>) -> Self {
- Self { workspace }
- }
-}
-
-impl SlashCommand for CurrentFileSlashCommand {
- fn name(&self) -> String {
- "current_file".into()
- }
-
- fn description(&self) -> String {
- "insert the current file".into()
- }
-
- fn complete_argument(
- &self,
- _query: String,
- _cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
- _cx: &mut AppContext,
- ) -> Task<Result<Vec<String>>> {
- Task::ready(Err(anyhow!("this command does not require argument")))
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn run(
- self: Arc<Self>,
- _argument: Option<&str>,
- _delegate: Arc<dyn LspAdapterDelegate>,
- cx: &mut AppContext,
- ) -> SlashCommandInvocation {
- let (invalidate_tx, invalidate_rx) = oneshot::channel();
- let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx)));
- let mut subscriptions: Vec<Subscription> = Vec::new();
- let output = self.workspace.update(cx, |workspace, cx| {
- let mut timestamps_by_entity_id = HashMap::default();
- 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);
- }
- }
-
- let mut most_recent_buffer = None;
- for editor in workspace.items_of_type::<Editor>(cx) {
- let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
- continue;
- };
-
- let timestamp = timestamps_by_entity_id
- .get(&editor.entity_id())
- .copied()
- .unwrap_or_default();
- if most_recent_buffer
- .as_ref()
- .map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
- {
- most_recent_buffer = Some((buffer, timestamp));
- }
- }
-
- subscriptions.push({
- let workspace_view = cx.view().clone();
- let invalidate_tx = invalidate_tx.clone();
- cx.window_context()
- .subscribe(&workspace_view, move |_workspace, event, _cx| match event {
- WorkspaceEvent::ActiveItemChanged
- | WorkspaceEvent::ItemAdded
- | WorkspaceEvent::ItemRemoved
- | WorkspaceEvent::PaneAdded(_)
- | WorkspaceEvent::PaneRemoved => {
- if let Some(invalidate_tx) = invalidate_tx.take() {
- _ = invalidate_tx.send(());
- }
- }
- _ => {}
- })
- });
-
- if let Some((buffer, _)) = most_recent_buffer {
- subscriptions.push({
- let invalidate_tx = invalidate_tx.clone();
- cx.window_context().observe(&buffer, move |_buffer, _cx| {
- if let Some(invalidate_tx) = invalidate_tx.take() {
- _ = invalidate_tx.send(());
- }
- })
- });
-
- let snapshot = buffer.read(cx).snapshot();
- let path = snapshot.resolve_file_path(cx, true);
- cx.background_executor().spawn(async move {
- let path = path
- .as_ref()
- .map(|path| path.to_string_lossy())
- .unwrap_or_else(|| Cow::Borrowed("untitled"));
-
- let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
- output.push_str("```");
- output.push_str(&path);
- output.push('\n');
- for chunk in snapshot.as_rope().chunks() {
- output.push_str(chunk);
- }
- if !output.ends_with('\n') {
- output.push('\n');
- }
- output.push_str("```");
- Ok(output)
- })
- } else {
- Task::ready(Err(anyhow!("no recent buffer found")))
- }
- });
-
- SlashCommandInvocation {
- output: output.unwrap_or_else(|error| Task::ready(Err(error))),
- invalidated: invalidate_rx,
- cleanup: SlashCommandCleanup::new(move || drop(subscriptions)),
- }
- }
-}
@@ -1,14 +1,15 @@
-use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
+use super::{SlashCommand, SlashCommandOutput};
use anyhow::Result;
-use futures::channel::oneshot;
use fuzzy::PathMatch;
-use gpui::{AppContext, Model, Task};
+use gpui::{AppContext, Model, RenderOnce, SharedString, Task, WeakView};
use language::LspAdapterDelegate;
use project::{PathMatchCandidateSet, Project};
use std::{
- path::Path,
+ path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc},
};
+use ui::{prelude::*, ButtonLike, ElevationIndex};
+use workspace::Workspace;
pub(crate) struct FileSlashCommand {
project: Model<Project>,
@@ -30,7 +31,6 @@ impl FileSlashCommand {
.read(cx)
.visible_worktrees(cx)
.collect::<Vec<_>>();
- let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
@@ -40,7 +40,7 @@ impl FileSlashCommand {
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
- include_root_name,
+ include_root_name: true,
directories_only: false,
}
})
@@ -68,7 +68,11 @@ impl SlashCommand for FileSlashCommand {
}
fn description(&self) -> String {
- "insert an entire file".into()
+ "insert a file".into()
+ }
+
+ fn tooltip_text(&self) -> String {
+ "insert file".into()
}
fn requires_argument(&self) -> bool {
@@ -100,36 +104,30 @@ impl SlashCommand for FileSlashCommand {
fn run(
self: Arc<Self>,
argument: Option<&str>,
+ _workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
- cx: &mut AppContext,
- ) -> SlashCommandInvocation {
+ cx: &mut WindowContext,
+ ) -> Task<Result<SlashCommandOutput>> {
let project = self.project.read(cx);
let Some(argument) = argument else {
- return SlashCommandInvocation {
- output: Task::ready(Err(anyhow::anyhow!("missing path"))),
- invalidated: oneshot::channel().1,
- cleanup: SlashCommandCleanup::default(),
- };
+ return Task::ready(Err(anyhow::anyhow!("missing path")));
};
- let path = Path::new(argument);
+ let path = PathBuf::from(argument);
let abs_path = project.worktrees().find_map(|worktree| {
let worktree = worktree.read(cx);
- worktree.entry_for_path(path)?;
- worktree.absolutize(path).ok()
+ let worktree_root_path = Path::new(worktree.root_name());
+ let relative_path = path.strip_prefix(worktree_root_path).ok()?;
+ worktree.absolutize(&relative_path).ok()
});
let Some(abs_path) = abs_path else {
- return SlashCommandInvocation {
- output: Task::ready(Err(anyhow::anyhow!("missing path"))),
- invalidated: oneshot::channel().1,
- cleanup: SlashCommandCleanup::default(),
- };
+ return Task::ready(Err(anyhow::anyhow!("missing path")));
};
let fs = project.fs().clone();
let argument = argument.to_string();
- let output = cx.background_executor().spawn(async move {
+ let text = cx.background_executor().spawn(async move {
let content = fs.load(&abs_path).await?;
let mut output = String::with_capacity(argument.len() + content.len() + 9);
output.push_str("```");
@@ -140,12 +138,46 @@ impl SlashCommand for FileSlashCommand {
output.push('\n');
}
output.push_str("```");
- Ok(output)
+ anyhow::Ok(output)
});
- SlashCommandInvocation {
- output,
- invalidated: oneshot::channel().1,
- cleanup: SlashCommandCleanup::default(),
- }
+ cx.foreground_executor().spawn(async move {
+ let text = text.await?;
+ Ok(SlashCommandOutput {
+ text,
+ render_placeholder: Arc::new(move |id, unfold, _cx| {
+ FilePlaceholder {
+ path: Some(path.clone()),
+ id,
+ unfold,
+ }
+ .into_any_element()
+ }),
+ })
+ })
+ }
+}
+
+#[derive(IntoElement)]
+pub struct FilePlaceholder {
+ pub path: Option<PathBuf>,
+ pub id: ElementId,
+ pub unfold: Arc<dyn Fn(&mut WindowContext)>,
+}
+
+impl RenderOnce for FilePlaceholder {
+ fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+ let unfold = self.unfold;
+ let title = if let Some(path) = self.path.as_ref() {
+ SharedString::from(path.to_string_lossy().to_string())
+ } else {
+ SharedString::from("untitled")
+ };
+
+ ButtonLike::new(self.id)
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ElevatedSurface)
+ .child(Icon::new(IconName::File))
+ .child(Label::new(title))
+ .on_click(move |_, cx| unfold(cx))
}
}
@@ -0,0 +1,151 @@
+use super::{SlashCommand, SlashCommandOutput};
+use anyhow::{anyhow, Context, Result};
+use fs::Fs;
+use gpui::{AppContext, Model, Task, WeakView};
+use language::LspAdapterDelegate;
+use project::{Project, ProjectPath};
+use std::{
+ fmt::Write,
+ path::Path,
+ sync::{atomic::AtomicBool, Arc},
+};
+use ui::{prelude::*, ButtonLike, ElevationIndex};
+use workspace::Workspace;
+
+pub(crate) struct ProjectSlashCommand;
+
+impl ProjectSlashCommand {
+ async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
+ let buffer = fs.load(path_to_cargo_toml).await?;
+ let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
+
+ let mut message = String::new();
+ writeln!(message, "You are in a Rust project.")?;
+
+ if let Some(workspace) = cargo_toml.workspace {
+ writeln!(
+ message,
+ "The project is a Cargo workspace with the following members:"
+ )?;
+ for member in workspace.members {
+ writeln!(message, "- {member}")?;
+ }
+
+ if !workspace.default_members.is_empty() {
+ writeln!(message, "The default members are:")?;
+ for member in workspace.default_members {
+ writeln!(message, "- {member}")?;
+ }
+ }
+
+ if !workspace.dependencies.is_empty() {
+ writeln!(
+ message,
+ "The following workspace dependencies are installed:"
+ )?;
+ for dependency in workspace.dependencies.keys() {
+ writeln!(message, "- {dependency}")?;
+ }
+ }
+ } else if let Some(package) = cargo_toml.package {
+ writeln!(
+ message,
+ "The project name is \"{name}\".",
+ name = package.name
+ )?;
+
+ let description = package
+ .description
+ .as_ref()
+ .and_then(|description| description.get().ok().cloned());
+ if let Some(description) = description.as_ref() {
+ writeln!(message, "It describes itself as \"{description}\".")?;
+ }
+
+ if !cargo_toml.dependencies.is_empty() {
+ writeln!(message, "The following dependencies are installed:")?;
+ for dependency in cargo_toml.dependencies.keys() {
+ writeln!(message, "- {dependency}")?;
+ }
+ }
+ }
+
+ Ok(message)
+ }
+
+ fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
+ let worktree = project.read(cx).worktrees().next()?;
+ let worktree = worktree.read(cx);
+ let entry = worktree.entry_for_path("Cargo.toml")?;
+ let path = ProjectPath {
+ worktree_id: worktree.id(),
+ path: entry.path.clone(),
+ };
+ Some(Arc::from(
+ project.read(cx).absolute_path(&path, cx)?.as_path(),
+ ))
+ }
+}
+
+impl SlashCommand for ProjectSlashCommand {
+ fn name(&self) -> String {
+ "project".into()
+ }
+
+ fn description(&self) -> String {
+ "insert current project context".into()
+ }
+
+ fn tooltip_text(&self) -> String {
+ "insert current project context".into()
+ }
+
+ fn complete_argument(
+ &self,
+ _query: String,
+ _cancel: Arc<AtomicBool>,
+ _cx: &mut AppContext,
+ ) -> Task<Result<Vec<String>>> {
+ Task::ready(Err(anyhow!("this command does not require argument")))
+ }
+
+ fn requires_argument(&self) -> bool {
+ false
+ }
+
+ fn run(
+ self: Arc<Self>,
+ _argument: Option<&str>,
+ workspace: WeakView<Workspace>,
+ _delegate: Arc<dyn LspAdapterDelegate>,
+ cx: &mut WindowContext,
+ ) -> Task<Result<SlashCommandOutput>> {
+ let output = workspace.update(cx, |workspace, cx| {
+ let project = workspace.project().clone();
+ let fs = workspace.project().read(cx).fs().clone();
+ let path = Self::path_to_cargo_toml(project, cx);
+ let output = cx.background_executor().spawn(async move {
+ let path = path.with_context(|| "Cargo.toml not found")?;
+ Self::build_message(fs, &path).await
+ });
+
+ cx.foreground_executor().spawn(async move {
+ let text = output.await?;
+
+ Ok(SlashCommandOutput {
+ text,
+ render_placeholder: Arc::new(move |id, unfold, _cx| {
+ ButtonLike::new(id)
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ElevatedSurface)
+ .child(Icon::new(IconName::FileTree))
+ .child(Label::new("Project"))
+ .on_click(move |_, cx| unfold(cx))
+ .into_any_element()
+ }),
+ })
+ })
+ });
+ output.unwrap_or_else(|error| Task::ready(Err(error)))
+ }
+}
@@ -1,11 +1,12 @@
-use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
+use super::{SlashCommand, SlashCommandOutput};
use crate::prompts::PromptLibrary;
use anyhow::{anyhow, Context, Result};
-use futures::channel::oneshot;
use fuzzy::StringMatchCandidate;
-use gpui::{AppContext, Task};
+use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::{atomic::AtomicBool, Arc};
+use ui::{prelude::*, ButtonLike, ElevationIndex};
+use workspace::Workspace;
pub(crate) struct PromptSlashCommand {
library: Arc<PromptLibrary>,
@@ -26,6 +27,10 @@ impl SlashCommand for PromptSlashCommand {
"insert a prompt from the library".into()
}
+ fn tooltip_text(&self) -> String {
+ "insert prompt".into()
+ }
+
fn requires_argument(&self) -> bool {
true
}
@@ -64,32 +69,43 @@ impl SlashCommand for PromptSlashCommand {
fn run(
self: Arc<Self>,
title: Option<&str>,
+ _workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
- cx: &mut AppContext,
- ) -> SlashCommandInvocation {
+ cx: &mut WindowContext,
+ ) -> Task<Result<SlashCommandOutput>> {
let Some(title) = title else {
- return SlashCommandInvocation {
- output: Task::ready(Err(anyhow!("missing prompt name"))),
- invalidated: oneshot::channel().1,
- cleanup: SlashCommandCleanup::default(),
- };
+ return Task::ready(Err(anyhow!("missing prompt name")));
};
let library = self.library.clone();
- let title = title.to_string();
- let output = cx.background_executor().spawn(async move {
- let prompt = library
- .prompts()
- .into_iter()
- .find(|prompt| &prompt.1.title().to_string() == &title)
- .with_context(|| format!("no prompt found with title {:?}", title))?
- .1;
- Ok(prompt.body())
+ let title = SharedString::from(title.to_string());
+ let prompt = cx.background_executor().spawn({
+ let title = title.clone();
+ async move {
+ let prompt = library
+ .prompts()
+ .into_iter()
+ .map(|prompt| (prompt.1.title(), prompt))
+ .find(|(t, _)| t == &title)
+ .with_context(|| format!("no prompt found with title {:?}", title))?
+ .1;
+ anyhow::Ok(prompt.1.body())
+ }
});
- SlashCommandInvocation {
- output,
- invalidated: oneshot::channel().1,
- cleanup: SlashCommandCleanup::default(),
- }
+ cx.foreground_executor().spawn(async move {
+ let prompt = prompt.await?;
+ Ok(SlashCommandOutput {
+ text: prompt,
+ render_placeholder: Arc::new(move |id, unfold, _cx| {
+ ButtonLike::new(id)
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ElevatedSurface)
+ .child(Icon::new(IconName::Library))
+ .child(Label::new(title.clone()))
+ .on_click(move |_, cx| unfold(cx))
+ .into_any_element()
+ }),
+ })
+ })
}
}
@@ -15,7 +15,7 @@ path = "src/assistant_slash_command.rs"
anyhow.workspace = true
collections.workspace = true
derive_more.workspace = true
-futures.workspace = true
gpui.workspace = true
language.workspace = true
parking_lot.workspace = true
+workspace.workspace = true
@@ -1,14 +1,11 @@
mod slash_command_registry;
-use std::sync::atomic::AtomicBool;
-use std::sync::Arc;
-
use anyhow::Result;
-use futures::channel::oneshot;
-use gpui::{AppContext, Task};
+use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
use language::LspAdapterDelegate;
-
pub use slash_command_registry::*;
+use std::sync::{atomic::AtomicBool, Arc};
+use workspace::Workspace;
pub fn init(cx: &mut AppContext) {
SlashCommandRegistry::default_global(cx);
@@ -17,6 +14,7 @@ pub fn init(cx: &mut AppContext) {
pub trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String;
fn description(&self) -> String;
+ fn tooltip_text(&self) -> String;
fn complete_argument(
&self,
query: String,
@@ -27,35 +25,24 @@ pub trait SlashCommand: 'static + Send + Sync {
fn run(
self: Arc<Self>,
argument: Option<&str>,
+ workspace: WeakView<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: Arc<dyn LspAdapterDelegate>,
- cx: &mut AppContext,
- ) -> SlashCommandInvocation;
+ cx: &mut WindowContext,
+ ) -> Task<Result<SlashCommandOutput>>;
}
-pub struct SlashCommandInvocation {
- pub output: Task<Result<String>>,
- pub invalidated: oneshot::Receiver<()>,
- pub cleanup: SlashCommandCleanup,
-}
-
-#[derive(Default)]
-pub struct SlashCommandCleanup(Option<Box<dyn FnOnce()>>);
-
-impl SlashCommandCleanup {
- pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
- Self(Some(Box::new(cleanup)))
- }
-}
+pub type RenderFoldPlaceholder = Arc<
+ dyn Send
+ + Sync
+ + Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
+>;
-impl Drop for SlashCommandCleanup {
- fn drop(&mut self) {
- if let Some(cleanup) = self.0.take() {
- cleanup();
- }
- }
+pub struct SlashCommandOutput {
+ pub text: String,
+ pub render_placeholder: RenderFoldPlaceholder,
}
@@ -305,6 +305,7 @@ impl MessageEditor {
documentation: None,
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
+ confirm: None,
}
})
.collect()
@@ -36,7 +36,13 @@ impl FlapSnapshot {
while let Some(item) = cursor.item() {
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
Ordering::Less => cursor.next(snapshot),
- Ordering::Equal => return Some(&item.flap),
+ Ordering::Equal => {
+ if item.flap.range.start.is_valid(snapshot) {
+ return Some(&item.flap);
+ } else {
+ cursor.next(snapshot);
+ }
+ }
Ordering::Greater => break,
}
}
@@ -20,6 +20,8 @@ pub struct FoldPlaceholder {
pub render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of an ellipsis.
pub constrain_width: bool,
+ /// If true, merges the fold with an adjacent one.
+ pub merge_adjacent: bool,
}
impl FoldPlaceholder {
@@ -30,6 +32,7 @@ impl FoldPlaceholder {
Self {
render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
constrain_width: true,
+ merge_adjacent: true,
}
}
}
@@ -374,8 +377,11 @@ impl FoldMap {
assert!(fold_range.start.0 >= sum.input.len);
- while folds.peek().map_or(false, |(_, next_fold_range)| {
- next_fold_range.start <= fold_range.end
+ while folds.peek().map_or(false, |(next_fold, next_fold_range)| {
+ next_fold_range.start < fold_range.end
+ || (next_fold_range.start == fold_range.end
+ && fold.placeholder.merge_adjacent
+ && next_fold.placeholder.merge_adjacent)
}) {
let (_, next_fold_range) = folds.next().unwrap();
if next_fold_range.end > fold_range.end {
@@ -1628,6 +1628,7 @@ impl Editor {
})
.into_any()
}),
+ merge_adjacent: true,
};
let display_map = cx.new_model(|cx| {
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
@@ -3905,6 +3906,7 @@ impl Editor {
let snippet;
let text;
+
if completion.is_snippet() {
snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
text = snippet.as_ref().unwrap().text.clone();
@@ -3998,6 +4000,10 @@ impl Editor {
this.refresh_inline_completion(true, cx);
});
+ if let Some(confirm) = completion.confirm.as_ref() {
+ (confirm)(cx);
+ }
+
let provider = self.completion_provider.as_ref()?;
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
@@ -3908,7 +3908,7 @@ enum LineFragment {
Text(ShapedLine),
Element {
element: Option<AnyElement>,
- width: Pixels,
+ size: Size<Pixels>,
len: usize,
},
}
@@ -3917,9 +3917,9 @@ impl fmt::Debug for LineFragment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
- LineFragment::Element { width, len, .. } => f
+ LineFragment::Element { size, len, .. } => f
.debug_struct("Element")
- .field("width", width)
+ .field("size", size)
.field("len", len)
.finish(),
}
@@ -3999,7 +3999,7 @@ impl LineWithInvisibles {
len += highlighted_chunk.text.len();
fragments.push(LineFragment::Element {
element: Some(element),
- width: size.width,
+ size,
len: highlighted_chunk.text.len(),
});
} else {
@@ -4112,13 +4112,18 @@ impl LineWithInvisibles {
LineFragment::Text(line) => {
fragment_origin.x += line.width;
}
- LineFragment::Element { element, width, .. } => {
+ LineFragment::Element { element, size, .. } => {
let mut element = element
.take()
.expect("you can't prepaint LineWithInvisibles twice");
- element.prepaint_at(fragment_origin, cx);
+
+ // Center the element vertically within the line.
+ let mut element_origin = fragment_origin;
+ element_origin.y += (line_height - size.height) / 2.;
+ element.prepaint_at(element_origin, cx);
line_elements.push(element);
- fragment_origin.x += *width;
+
+ fragment_origin.x += size.width;
}
}
}
@@ -4146,8 +4151,8 @@ impl LineWithInvisibles {
line.paint(fragment_origin, line_height, cx).log_err();
fragment_origin.x += line.width;
}
- LineFragment::Element { width, .. } => {
- fragment_origin.x += *width;
+ LineFragment::Element { size, .. } => {
+ fragment_origin.x += size.width;
}
}
}
@@ -4225,12 +4230,12 @@ impl LineWithInvisibles {
fragment_start_x += shaped_line.width;
fragment_start_index = fragment_end_index;
}
- LineFragment::Element { len, width, .. } => {
+ LineFragment::Element { len, size, .. } => {
let fragment_end_index = fragment_start_index + len;
if index < fragment_end_index {
return fragment_start_x;
}
- fragment_start_x += *width;
+ fragment_start_x += size.width;
fragment_start_index = fragment_end_index;
}
}
@@ -4255,8 +4260,8 @@ impl LineWithInvisibles {
fragment_start_x = fragment_end_x;
fragment_start_index += shaped_line.len;
}
- LineFragment::Element { len, width, .. } => {
- let fragment_end_x = fragment_start_x + *width;
+ LineFragment::Element { len, size, .. } => {
+ let fragment_end_x = fragment_start_x + size.width;
if x < fragment_end_x {
return Some(fragment_start_index);
}
@@ -46,6 +46,7 @@ wasmtime.workspace = true
wasmtime-wasi.workspace = true
wasmparser.workspace = true
wit-component.workspace = true
+workspace.workspace = true
task.workspace = true
serde_json_lenient.workspace = true
@@ -58,3 +59,4 @@ fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }
@@ -133,6 +133,7 @@ impl LanguageServerManifestEntry {
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct SlashCommandManifestEntry {
pub description: String,
+ pub tooltip_text: String,
pub requires_argument: bool,
}
@@ -1,15 +1,12 @@
-use std::sync::atomic::AtomicBool;
-use std::sync::Arc;
-
+use crate::wasm_host::{WasmExtension, WasmHost};
use anyhow::{anyhow, Result};
-use assistant_slash_command::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
-use futures::channel::oneshot;
+use assistant_slash_command::{SlashCommand, SlashCommandOutput};
use futures::FutureExt;
-use gpui::{AppContext, Task};
+use gpui::{AppContext, IntoElement, Task, WeakView, WindowContext};
use language::LspAdapterDelegate;
+use std::sync::{atomic::AtomicBool, Arc};
use wasmtime_wasi::WasiView;
-
-use crate::wasm_host::{WasmExtension, WasmHost};
+use workspace::Workspace;
pub struct ExtensionSlashCommand {
pub(crate) extension: WasmExtension,
@@ -27,6 +24,10 @@ impl SlashCommand for ExtensionSlashCommand {
self.command.description.clone()
}
+ fn tooltip_text(&self) -> String {
+ self.command.tooltip_text.clone()
+ }
+
fn requires_argument(&self) -> bool {
self.command.requires_argument
}
@@ -43,11 +44,11 @@ impl SlashCommand for ExtensionSlashCommand {
fn run(
self: Arc<Self>,
argument: Option<&str>,
+ _workspace: WeakView<Workspace>,
delegate: Arc<dyn LspAdapterDelegate>,
- cx: &mut AppContext,
- ) -> SlashCommandInvocation {
+ cx: &mut WindowContext,
+ ) -> Task<Result<SlashCommandOutput>> {
let argument = argument.map(|arg| arg.to_string());
-
let output = cx.background_executor().spawn(async move {
let output = self
.extension
@@ -72,14 +73,16 @@ impl SlashCommand for ExtensionSlashCommand {
}
})
.await?;
-
output.ok_or_else(|| anyhow!("no output from command: {}", self.command.name))
});
-
- SlashCommandInvocation {
- output,
- invalidated: oneshot::channel().1,
- cleanup: SlashCommandCleanup::default(),
- }
+ cx.foreground_executor().spawn(async move {
+ let output = output.await?;
+ Ok(SlashCommandOutput {
+ text: output,
+ render_placeholder: Arc::new(|_, _, _| {
+ "TODO: Extension command output".into_any_element()
+ }),
+ })
+ })
}
}
@@ -1183,6 +1183,7 @@ impl ExtensionStore {
command: crate::wit::SlashCommand {
name: slash_command_name.to_string(),
description: slash_command.description.to_string(),
+ tooltip_text: slash_command.tooltip_text.to_string(),
requires_argument: slash_command.requires_argument,
},
extension: wasm_extension.clone(),
@@ -5,6 +5,8 @@ interface slash-command {
name: string,
/// The description of the slash command.
description: string,
+ /// The tooltip text to display for the run button.
+ tooltip-text: string,
/// Whether this slash command requires an argument.
requires-argument: bool,
}
@@ -291,6 +291,8 @@ impl Interactivity {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Capture {
(listener)(action, cx)
+ } else {
+ cx.propagate();
}
}),
));
@@ -36,7 +36,7 @@ use git::{blame::Blame, repository::GitRepository};
use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
- EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel,
+ EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel, WindowContext,
};
use itertools::Itertools;
use language::{
@@ -407,7 +407,7 @@ pub struct InlayHint {
}
/// A completion provided by a language server
-#[derive(Clone, Debug)]
+#[derive(Clone)]
pub struct Completion {
/// The range of the buffer that will be replaced.
pub old_range: Range<Anchor>,
@@ -421,6 +421,21 @@ pub struct Completion {
pub documentation: Option<Documentation>,
/// The raw completion provided by the language server.
pub lsp_completion: lsp::CompletionItem,
+ /// An optional callback to invoke when this completion is confirmed.
+ pub confirm: Option<Arc<dyn Send + Sync + Fn(&mut WindowContext)>>,
+}
+
+impl std::fmt::Debug for Completion {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Completion")
+ .field("old_range", &self.old_range)
+ .field("new_text", &self.new_text)
+ .field("label", &self.label)
+ .field("server_id", &self.server_id)
+ .field("documentation", &self.documentation)
+ .field("lsp_completion", &self.lsp_completion)
+ .finish()
+ }
}
/// A completion provided by a language server
@@ -2029,6 +2044,30 @@ impl Project {
})
}
+ pub fn open_buffer_for_full_path(
+ &mut self,
+ path: &Path,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Model<Buffer>>> {
+ if let Some(worktree_name) = path.components().next() {
+ let worktree = self.worktrees().find(|worktree| {
+ OsStr::new(worktree.read(cx).root_name()) == worktree_name.as_os_str()
+ });
+ if let Some(worktree) = worktree {
+ let worktree = worktree.read(cx);
+ let worktree_root_path = Path::new(worktree.root_name());
+ if let Ok(path) = path.strip_prefix(worktree_root_path) {
+ let project_path = ProjectPath {
+ worktree_id: worktree.id(),
+ path: path.into(),
+ };
+ return self.open_buffer(project_path, cx);
+ }
+ }
+ }
+ Task::ready(Err(anyhow!("buffer not found for {:?}", path)))
+ }
+
pub fn open_local_buffer(
&mut self,
abs_path: impl AsRef<Path>,
@@ -9212,6 +9251,7 @@ impl Project {
runs: Default::default(),
filter_range: Default::default(),
},
+ confirm: None,
},
false,
cx,
@@ -10883,6 +10923,7 @@ async fn populate_labels_for_completions(
server_id: completion.server_id,
documentation,
lsp_completion,
+ confirm: None,
})
}
}
@@ -184,6 +184,7 @@ pub enum IconName {
Tab,
Terminal,
Trash,
+ TriangleRight,
Update,
WholeWord,
XCircle,
@@ -303,6 +304,7 @@ impl IconName {
IconName::Tab => "icons/tab.svg",
IconName::Terminal => "icons/terminal.svg",
IconName::Trash => "icons/trash.svg",
+ IconName::TriangleRight => "icons/triangle_right.svg",
IconName::Update => "icons/update.svg",
IconName::WholeWord => "icons/word_search.svg",
IconName::XCircle => "icons/error.svg",
@@ -17,3 +17,4 @@ commit = "8432ffe32ccd360534837256747beb5b1c82fca1"
[slash_commands.gleam-project]
description = "Returns information about the current Gleam project."
requires_argument = false
+tooltip_text = "Insert Gleam project data"