Extract ContextEditor to assistant_context_editor (#23433)

This PR extracts the `ContextEditor` to the `assistant_context_editor`
crate.

As part of this, we have decoupled the `ContextEditor` from the
`AssistantPanel`.

There is now an `AssistantPanelDelegate` that the `ContextEditor` uses
when it needs to interface with the Assistant panel.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-01-21 18:08:34 -05:00 committed by GitHub
parent 9a7f1d1de4
commit 417760ade7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 263 additions and 166 deletions

10
Cargo.lock generated
View file

@ -389,7 +389,6 @@ dependencies = [
"feature_flags", "feature_flags",
"fs", "fs",
"futures 0.3.31", "futures 0.3.31",
"fuzzy",
"gpui", "gpui",
"indexed_docs", "indexed_docs",
"indoc", "indoc",
@ -502,6 +501,7 @@ name = "assistant_context_editor"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assistant_settings",
"assistant_slash_command", "assistant_slash_command",
"assistant_slash_commands", "assistant_slash_commands",
"assistant_tool", "assistant_tool",
@ -516,18 +516,24 @@ dependencies = [
"futures 0.3.31", "futures 0.3.31",
"fuzzy", "fuzzy",
"gpui", "gpui",
"indexed_docs",
"language", "language",
"language_model", "language_model",
"language_model_selector",
"language_models", "language_models",
"languages",
"log", "log",
"multi_buffer",
"open_ai", "open_ai",
"parking_lot", "parking_lot",
"paths", "paths",
"picker",
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_library", "prompt_library",
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"rope",
"rpc", "rpc",
"serde", "serde",
"serde_json", "serde_json",
@ -537,6 +543,8 @@ dependencies = [
"strum", "strum",
"telemetry_events", "telemetry_events",
"text", "text",
"theme",
"tree-sitter-md",
"ui", "ui",
"unindent", "unindent",
"util", "util",

View file

@ -37,7 +37,6 @@ editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
indexed_docs.workspace = true indexed_docs.workspace = true
indoc.workspace = true indoc.workspace = true

View file

@ -1,15 +1,11 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))] #![cfg_attr(target_os = "windows", allow(unused, dead_code))]
pub mod assistant_panel; pub mod assistant_panel;
mod context_editor;
mod context_history; mod context_history;
mod inline_assistant; mod inline_assistant;
mod slash_command;
pub(crate) mod slash_command_picker;
pub mod slash_command_settings; pub mod slash_command_settings;
mod terminal_inline_assistant; mod terminal_inline_assistant;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
@ -19,7 +15,6 @@ use client::Client;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt; use feature_flags::FeatureFlagAppExt;
use fs::Fs; use fs::Fs;
use gpui::impl_internal_actions;
use gpui::{actions, AppContext, Global, UpdateGlobal}; use gpui::{actions, AppContext, Global, UpdateGlobal};
use language_model::{ use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
@ -37,33 +32,16 @@ use crate::slash_command_settings::SlashCommandSettings;
actions!( actions!(
assistant, assistant,
[ [
Assist,
Edit,
Split,
CopyCode,
CycleMessageRole,
QuoteSelection,
InsertIntoEditor,
ToggleFocus, ToggleFocus,
InsertActivePrompt, InsertActivePrompt,
DeployHistory, DeployHistory,
DeployPromptLibrary, DeployPromptLibrary,
ConfirmCommand,
NewContext, NewContext,
ToggleModelSelector,
CycleNextInlineAssist, CycleNextInlineAssist,
CyclePreviousInlineAssist CyclePreviousInlineAssist
] ]
); );
#[derive(PartialEq, Clone)]
pub enum InsertDraggedFiles {
ProjectPaths(Vec<PathBuf>),
ExternalFiles(Vec<PathBuf>),
}
impl_internal_actions!(assistant, [InsertDraggedFiles]);
const DEFAULT_CONTEXT_LINES: usize = 50; const DEFAULT_CONTEXT_LINES: usize = 50;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -334,24 +312,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
} }
} }
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),
1000..=9999 => {
let thousands = count / 1000;
let hundreds = (count % 1000 + 50) / 100;
if hundreds == 0 {
format!("{}k", thousands)
} else if hundreds == 10 {
format!("{}k", thousands + 1)
} else {
format!("{}.{}k", thousands, hundreds)
}
}
_ => format!("{}k", (count + 500) / 1000),
}
}
#[cfg(test)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {

View file

@ -1,14 +1,14 @@
use crate::context_editor::{
ContextEditor, ContextEditorToolbarItem, ContextEditorToolbarItemEvent, DEFAULT_TAB_TITLE,
};
use crate::context_history::ContextHistory; use crate::context_history::ContextHistory;
use crate::{ use crate::{
slash_command::SlashCommandCompletionProvider,
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, DeployPromptLibrary, terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, DeployPromptLibrary,
InlineAssistant, InsertDraggedFiles, NewContext, ToggleFocus, ToggleModelSelector, InlineAssistant, NewContext, ToggleFocus,
};
use anyhow::{anyhow, Result};
use assistant_context_editor::{
AssistantPanelDelegate, Context, ContextEditor, ContextEditorToolbarItem,
ContextEditorToolbarItemEvent, ContextId, ContextStore, ContextStoreEvent, InsertDraggedFiles,
SlashCommandCompletionProvider, ToggleModelSelector, DEFAULT_TAB_TITLE,
}; };
use anyhow::Result;
use assistant_context_editor::{Context, ContextId, ContextStore, ContextStoreEvent};
use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet; use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet; use assistant_tool::ToolWorkingSet;
@ -46,6 +46,8 @@ use workspace::{
use zed_actions::InlineAssist; use zed_actions::InlineAssist;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
<dyn AssistantPanelDelegate>::set_global(Arc::new(ConcreteAssistantPanelDelegate), cx);
workspace::FollowableViewRegistry::register::<ContextEditor>(cx); workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
cx.observe_new_views( cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| { |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@ -438,7 +440,7 @@ impl AssistantPanel {
if let Some(context_editor) = self.active_context_editor(cx) { if let Some(context_editor) = self.active_context_editor(cx) {
let new_summary = model_summary_editor.read(cx).text(cx); let new_summary = model_summary_editor.read(cx).text(cx);
context_editor.update(cx, |context_editor, cx| { context_editor.update(cx, |context_editor, cx| {
context_editor.context.update(cx, |context, cx| { context_editor.context().update(cx, |context, cx| {
if context.summary().is_none() if context.summary().is_none()
&& (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty()) && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
{ {
@ -475,7 +477,7 @@ impl AssistantPanel {
) { ) {
if let Some(context_editor) = self.active_context_editor(cx) { if let Some(context_editor) = self.active_context_editor(cx) {
context_editor.update(cx, |context_editor, cx| { context_editor.update(cx, |context_editor, cx| {
context_editor.context.update(cx, |context, cx| { context_editor.context().update(cx, |context, cx| {
context.summarize(true, cx); context.summarize(true, cx);
}) })
}) })
@ -501,7 +503,6 @@ impl AssistantPanel {
.log_err() .log_err()
.flatten(); .flatten();
let assistant_panel = cx.view().downgrade();
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
let mut editor = ContextEditor::for_context( let mut editor = ContextEditor::for_context(
context, context,
@ -509,7 +510,6 @@ impl AssistantPanel {
self.workspace.clone(), self.workspace.clone(),
self.project.clone(), self.project.clone(),
lsp_adapter_delegate, lsp_adapter_delegate,
assistant_panel,
cx, cx,
); );
editor.insert_default_prompt(cx); editor.insert_default_prompt(cx);
@ -523,7 +523,7 @@ impl AssistantPanel {
if let Some(editor) = self.active_context_editor(cx) { if let Some(editor) = self.active_context_editor(cx) {
editor.update(cx, |active_context, cx| { editor.update(cx, |active_context, cx| {
active_context active_context
.context .context()
.update(cx, |context, cx| context.completion_provider_changed(cx)) .update(cx, |context, cx| context.completion_provider_changed(cx))
}) })
} }
@ -716,7 +716,7 @@ impl AssistantPanel {
.read(cx) .read(cx)
.active_context_editor(cx) .active_context_editor(cx)
.and_then(|editor| { .and_then(|editor| {
let editor = &editor.read(cx).editor; let editor = &editor.read(cx).editor().clone();
if editor.read(cx).is_focused(cx) { if editor.read(cx).is_focused(cx) {
Some(editor.clone()) Some(editor.clone())
} else { } else {
@ -778,7 +778,6 @@ impl AssistantPanel {
let fs = this.fs.clone(); let fs = this.fs.clone();
let project = this.project.clone(); let project = this.project.clone();
let weak_assistant_panel = cx.view().downgrade();
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
ContextEditor::for_context( ContextEditor::for_context(
@ -787,7 +786,6 @@ impl AssistantPanel {
workspace, workspace,
project, project,
lsp_adapter_delegate, lsp_adapter_delegate,
weak_assistant_panel,
cx, cx,
) )
}); });
@ -808,7 +806,6 @@ impl AssistantPanel {
.log_err() .log_err()
.flatten(); .flatten();
let assistant_panel = cx.view().downgrade();
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
let mut editor = ContextEditor::for_context( let mut editor = ContextEditor::for_context(
context, context,
@ -816,7 +813,6 @@ impl AssistantPanel {
self.workspace.clone(), self.workspace.clone(),
self.project.clone(), self.project.clone(),
lsp_adapter_delegate, lsp_adapter_delegate,
assistant_panel,
cx, cx,
); );
editor.insert_default_prompt(cx); editor.insert_default_prompt(cx);
@ -1013,7 +1009,7 @@ impl AssistantPanel {
} }
pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> { pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
Some(self.active_context_editor(cx)?.read(cx).context.clone()) Some(self.active_context_editor(cx)?.read(cx).context().clone())
} }
pub fn open_saved_context( pub fn open_saved_context(
@ -1023,7 +1019,7 @@ impl AssistantPanel {
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let existing_context = self.pane.read(cx).items().find_map(|item| { let existing_context = self.pane.read(cx).items().find_map(|item| {
item.downcast::<ContextEditor>() item.downcast::<ContextEditor>()
.filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path)) .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path))
}); });
if let Some(existing_context) = existing_context { if let Some(existing_context) = existing_context {
return cx.spawn(|this, mut cx| async move { return cx.spawn(|this, mut cx| async move {
@ -1042,7 +1038,6 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let context = context.await?; let context = context.await?;
let assistant_panel = this.clone();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
ContextEditor::for_context( ContextEditor::for_context(
@ -1051,7 +1046,6 @@ impl AssistantPanel {
workspace, workspace,
project, project,
lsp_adapter_delegate, lsp_adapter_delegate,
assistant_panel,
cx, cx,
) )
}); });
@ -1069,7 +1063,7 @@ impl AssistantPanel {
) -> Task<Result<View<ContextEditor>>> { ) -> Task<Result<View<ContextEditor>>> {
let existing_context = self.pane.read(cx).items().find_map(|item| { let existing_context = self.pane.read(cx).items().find_map(|item| {
item.downcast::<ContextEditor>() item.downcast::<ContextEditor>()
.filter(|editor| *editor.read(cx).context.read(cx).id() == id) .filter(|editor| *editor.read(cx).context().read(cx).id() == id)
}); });
if let Some(existing_context) = existing_context { if let Some(existing_context) = existing_context {
return cx.spawn(|this, mut cx| async move { return cx.spawn(|this, mut cx| async move {
@ -1091,7 +1085,6 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let context = context.await?; let context = context.await?;
let assistant_panel = this.clone();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
ContextEditor::for_context( ContextEditor::for_context(
@ -1100,7 +1093,6 @@ impl AssistantPanel {
workspace, workspace,
this.project.clone(), this.project.clone(),
lsp_adapter_delegate, lsp_adapter_delegate,
assistant_panel,
cx, cx,
) )
}); });
@ -1304,6 +1296,61 @@ impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
} }
} }
struct ConcreteAssistantPanelDelegate;
impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn active_context_editor(
&self,
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<View<ContextEditor>> {
let panel = workspace.panel::<AssistantPanel>(cx)?;
panel.read(cx).active_context_editor(cx)
}
fn open_remote_context(
&self,
workspace: &mut Workspace,
context_id: ContextId,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<View<ContextEditor>>> {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return Task::ready(Err(anyhow!("no Assistant panel found")));
};
panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx))
}
fn quote_selection(
&self,
workspace: &mut Workspace,
creases: Vec<(String, String)>,
cx: &mut ViewContext<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
// Activate the panel
if !panel.focus_handle(cx).contains_focused(cx) {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
}
panel.update(cx, |_, cx| {
// Wait to create a new context until the workspace is no longer
// being updated.
cx.defer(move |panel, cx| {
if let Some(context) = panel
.active_context_editor(cx)
.or_else(|| panel.new_context(cx))
{
context.update(cx, |context, cx| context.quote_creases(creases, cx));
};
});
});
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum WorkflowAssistStatus { pub enum WorkflowAssistStatus {
Pending, Pending,

View file

@ -1,6 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use assistant_context_editor::{ContextStore, RemoteContextMetadata, SavedContextMetadata}; use assistant_context_editor::{
ContextStore, RemoteContextMetadata, SavedContextMetadata, DEFAULT_TAB_TITLE,
};
use gpui::{ use gpui::{
AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, View, WeakView, AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, View, WeakView,
}; };
@ -10,7 +12,6 @@ use ui::utils::{format_distance_from_now, DateTimeType};
use ui::{prelude::*, Avatar, ListItem, ListItemSpacing}; use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
use workspace::Item; use workspace::Item;
use crate::context_editor::DEFAULT_TAB_TITLE;
use crate::AssistantPanel; use crate::AssistantPanel;
#[derive(Clone)] #[derive(Clone)]

View file

@ -1,9 +1,8 @@
use crate::{ use crate::{
humanize_token_count, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, CyclePreviousInlineAssist,
CyclePreviousInlineAssist,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use assistant_context_editor::RequestType; use assistant_context_editor::{humanize_token_count, RequestType};
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use client::{telemetry::Telemetry, ErrorExt}; use client::{telemetry::Telemetry, ErrorExt};
use collections::{hash_map, HashMap, HashSet, VecDeque}; use collections::{hash_map, HashMap, HashSet, VecDeque};

View file

@ -1,6 +1,6 @@
use crate::{humanize_token_count, AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES}; use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use assistant_context_editor::RequestType; use assistant_context_editor::{humanize_token_count, RequestType};
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};

View file

@ -13,6 +13,7 @@ path = "src/assistant_context_editor.rs"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true assistant_slash_commands.workspace = true
assistant_tool.workspace = true assistant_tool.workspace = true
@ -27,32 +28,41 @@ fs.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true language_models.workspace = true
log.workspace = true log.workspace = true
multi_buffer.workspace = true
open_ai.workspace = true open_ai.workspace = true
parking_lot.workspace = true
paths.workspace = true paths.workspace = true
picker.workspace = true
project.workspace = true project.workspace = true
prompt_library.workspace = true prompt_library.workspace = true
regex.workspace = true regex.workspace = true
rope.workspace = true
rpc.workspace = true rpc.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
strum.workspace = true strum.workspace = true
telemetry_events.workspace = true telemetry_events.workspace = true
text.workspace = true text.workspace = true
theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
workspace.workspace = true
[dev-dependencies] [dev-dependencies]
language_model = { workspace = true, features = ["test-support"] } language_model = { workspace = true, features = ["test-support"] }
parking_lot.workspace = true languages = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true pretty_assertions.workspace = true
rand.workspace = true rand.workspace = true
settings.workspace = true tree-sitter-md.workspace = true
unindent.workspace = true unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] }

View file

@ -1,6 +1,9 @@
mod context; mod context;
mod context_editor;
mod context_store; mod context_store;
mod patch; mod patch;
mod slash_command;
mod slash_command_picker;
use std::sync::Arc; use std::sync::Arc;
@ -8,8 +11,10 @@ use client::Client;
use gpui::AppContext; use gpui::AppContext;
pub use crate::context::*; pub use crate::context::*;
pub use crate::context_editor::*;
pub use crate::context_store::*; pub use crate::context_store::*;
pub use crate::patch::*; pub use crate::patch::*;
pub use crate::slash_command::*;
pub fn init(client: Arc<Client>, _cx: &mut AppContext) { pub fn init(client: Arc<Client>, _cx: &mut AppContext) {
context_store::init(&client.into()); context_store::init(&client.into());

View file

@ -1,9 +1,4 @@
use anyhow::Result; use anyhow::Result;
use assistant_context_editor::{
AssistantPatch, AssistantPatchStatus, CacheStatus, Content, Context, ContextEvent, ContextId,
InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
};
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet}; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
use assistant_slash_commands::{ use assistant_slash_commands::{
@ -27,12 +22,12 @@ use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs; use fs::Fs;
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
div, img, percentage, point, prelude::*, pulsating_between, size, Animation, AnimationExt, actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between,
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem, size, Animation, AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext,
CursorStyle, Empty, Entity, EventEmitter, FocusHandle, FocusableView, FontWeight, ClipboardEntry, ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle,
InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage, FocusableView, FontWeight, Global, InteractiveElement, IntoElement, Model, ParentElement,
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled,
View, WeakModel, WeakView, Subscription, Task, Transformation, View, WeakModel, WeakView,
}; };
use indexed_docs::IndexedDocsStore; use indexed_docs::IndexedDocsStore;
use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset}; use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset};
@ -61,10 +56,34 @@ use workspace::{
Workspace, Workspace,
}; };
actions!(
assistant,
[
Assist,
ConfirmCommand,
CopyCode,
CycleMessageRole,
Edit,
InsertIntoEditor,
QuoteSelection,
Split,
ToggleModelSelector,
]
);
#[derive(PartialEq, Clone)]
pub enum InsertDraggedFiles {
ProjectPaths(Vec<PathBuf>),
ExternalFiles(Vec<PathBuf>),
}
impl_internal_actions!(assistant, [InsertDraggedFiles]);
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
use crate::{ use crate::{
humanize_token_count, slash_command::SlashCommandCompletionProvider, slash_command_picker, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, Context, ContextEvent, ContextId,
Assist, AssistantPanel, ConfirmCommand, CopyCode, CycleMessageRole, Edit, InsertDraggedFiles, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
InsertIntoEditor, QuoteSelection, Split, ToggleModelSelector, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
}; };
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
@ -94,15 +113,54 @@ enum AssistError {
Message(SharedString), Message(SharedString),
} }
pub trait AssistantPanelDelegate {
fn active_context_editor(
&self,
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<View<ContextEditor>>;
fn open_remote_context(
&self,
workspace: &mut Workspace,
context_id: ContextId,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<View<ContextEditor>>>;
fn quote_selection(
&self,
workspace: &mut Workspace,
creases: Vec<(String, String)>,
cx: &mut ViewContext<Workspace>,
);
}
impl dyn AssistantPanelDelegate {
/// Returns the global [`AssistantPanelDelegate`], if it exists.
pub fn try_global(cx: &AppContext) -> Option<Arc<Self>> {
cx.try_global::<GlobalAssistantPanelDelegate>()
.map(|global| global.0.clone())
}
/// Sets the global [`AssistantPanelDelegate`].
pub fn set_global(delegate: Arc<Self>, cx: &mut AppContext) {
cx.set_global(GlobalAssistantPanelDelegate(delegate));
}
}
struct GlobalAssistantPanelDelegate(Arc<dyn AssistantPanelDelegate>);
impl Global for GlobalAssistantPanelDelegate {}
pub struct ContextEditor { pub struct ContextEditor {
pub(crate) context: Model<Context>, context: Model<Context>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
slash_commands: Arc<SlashCommandWorkingSet>, slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>, tools: Arc<ToolWorkingSet>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
project: Model<Project>, project: Model<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>, lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
pub(crate) editor: View<Editor>, editor: View<Editor>,
blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>, blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
image_blocks: HashSet<CustomBlockId>, image_blocks: HashSet<CustomBlockId>,
scroll_position: Option<ScrollPosition>, scroll_position: Option<ScrollPosition>,
@ -113,7 +171,6 @@ pub struct ContextEditor {
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
patches: HashMap<Range<language::Anchor>, PatchViewState>, patches: HashMap<Range<language::Anchor>, PatchViewState>,
active_patch: Option<Range<language::Anchor>>, active_patch: Option<Range<language::Anchor>>,
assistant_panel: WeakView<AssistantPanel>,
last_error: Option<AssistError>, last_error: Option<AssistError>,
show_accept_terms: bool, show_accept_terms: bool,
pub(crate) slash_menu_handle: pub(crate) slash_menu_handle:
@ -130,13 +187,12 @@ pub const DEFAULT_TAB_TITLE: &str = "New Chat";
const MAX_TAB_TITLE_LEN: usize = 16; const MAX_TAB_TITLE_LEN: usize = 16;
impl ContextEditor { impl ContextEditor {
pub(crate) fn for_context( pub fn for_context(
context: Model<Context>, context: Model<Context>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
project: Model<Project>, project: Model<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>, lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
assistant_panel: WeakView<AssistantPanel>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let completion_provider = SlashCommandCompletionProvider::new( let completion_provider = SlashCommandCompletionProvider::new(
@ -190,7 +246,6 @@ impl ContextEditor {
_subscriptions, _subscriptions,
patches: HashMap::default(), patches: HashMap::default(),
active_patch: None, active_patch: None,
assistant_panel,
last_error: None, last_error: None,
show_accept_terms: false, show_accept_terms: false,
slash_menu_handle: Default::default(), slash_menu_handle: Default::default(),
@ -203,6 +258,14 @@ impl ContextEditor {
this this
} }
pub fn context(&self) -> &Model<Context> {
&self.context
}
pub fn editor(&self) -> &View<Editor> {
&self.editor
}
pub fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) { pub fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
let command_name = DefaultSlashCommand.name(); let command_name = DefaultSlashCommand.name();
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
@ -1523,10 +1586,12 @@ impl ContextEditor {
_: &InsertIntoEditor, _: &InsertIntoEditor,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else { let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return; return;
}; };
let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else { let Some(context_editor_view) =
assistant_panel_delegate.active_context_editor(workspace, cx)
else {
return; return;
}; };
let Some(active_editor_view) = workspace let Some(active_editor_view) = workspace
@ -1546,8 +1611,9 @@ impl ContextEditor {
pub fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) { pub fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) {
let result = maybe!({ let result = maybe!({
let panel = workspace.panel::<AssistantPanel>(cx)?; let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
let context_editor_view = panel.read(cx).active_context_editor(cx)?; let context_editor_view =
assistant_panel_delegate.active_context_editor(workspace, cx)?;
Self::get_selection_or_code_block(&context_editor_view, cx) Self::get_selection_or_code_block(&context_editor_view, cx)
}); });
let Some((text, is_code_block)) = result else { let Some((text, is_code_block)) = result else {
@ -1579,10 +1645,12 @@ impl ContextEditor {
action: &InsertDraggedFiles, action: &InsertDraggedFiles,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else { let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return; return;
}; };
let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else { let Some(context_editor_view) =
assistant_panel_delegate.active_context_editor(workspace, cx)
else {
return; return;
}; };
@ -1653,7 +1721,7 @@ impl ContextEditor {
_: &QuoteSelection, _: &QuoteSelection,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else { let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return; return;
}; };
@ -1664,61 +1732,46 @@ impl ContextEditor {
if creases.is_empty() { if creases.is_empty() {
return; return;
} }
// Activate the panel
if !panel.focus_handle(cx).contains_focused(cx) {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
}
panel.update(cx, |_, cx| { assistant_panel_delegate.quote_selection(workspace, creases, cx);
// Wait to create a new context until the workspace is no longer }
// being updated.
cx.defer(move |panel, cx| {
if let Some(context) = panel
.active_context_editor(cx)
.or_else(|| panel.new_context(cx))
{
context.update(cx, |context, cx| {
context.editor.update(cx, |editor, cx| {
editor.insert("\n", cx);
for (text, crease_title) in creases {
let point = editor.selections.newest::<Point>(cx).head();
let start_row = MultiBufferRow(point.row);
editor.insert(&text, cx); pub fn quote_creases(&mut self, creases: Vec<(String, String)>, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
editor.insert("\n", cx);
for (text, crease_title) in creases {
let point = editor.selections.newest::<Point>(cx).head();
let start_row = MultiBufferRow(point.row);
let snapshot = editor.buffer().read(cx).snapshot(cx); editor.insert(&text, cx);
let anchor_before = snapshot.anchor_after(point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
editor.insert("\n", cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
let fold_placeholder = quote_selection_fold_placeholder( editor.insert("\n", cx);
crease_title,
cx.view().downgrade(), let fold_placeholder =
); quote_selection_fold_placeholder(crease_title, cx.view().downgrade());
let crease = Crease::inline( let crease = Crease::inline(
anchor_before..anchor_after, anchor_before..anchor_after,
fold_placeholder, fold_placeholder,
render_quote_selection_output_toggle, render_quote_selection_output_toggle,
|_, _, _| Empty.into_any(), |_, _, _| Empty.into_any(),
); );
editor.insert_creases(vec![crease], cx); editor.insert_creases(vec![crease], cx);
editor.fold_at( editor.fold_at(
&FoldAt { &FoldAt {
buffer_row: start_row, buffer_row: start_row,
}, },
cx, cx,
); );
} }
}) })
});
};
});
});
} }
fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) { fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
@ -2154,10 +2207,10 @@ impl ContextEditor {
} }
fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> { fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
use feature_flags::FeatureFlagAppExt; // This was previously gated behind the `zed-pro` feature flag. Since we
let nudge = self.assistant_panel.upgrade().map(|assistant_panel| { // aren't planning to ship that right now, we're just hard-coding this
assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>() // value to not show the nudge.
}); let nudge = Some(false);
if nudge.map_or(false, |value| value) { if nudge.map_or(false, |value| value) {
Some( Some(
@ -3039,18 +3092,15 @@ impl FollowableItem for ContextEditor {
let context_id = ContextId::from_proto(state.context_id); let context_id = ContextId::from_proto(state.context_id);
let editor_state = state.editor?; let editor_state = state.editor?;
let (project, panel) = workspace.update(cx, |workspace, cx| { let project = workspace.read(cx).project().clone();
Some(( let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
workspace.project().clone(),
workspace.panel::<AssistantPanel>(cx)?,
))
})?;
let context_editor = let context_editor_task = workspace.update(cx, |workspace, cx| {
panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx)); assistant_panel_delegate.open_remote_context(workspace, context_id, cx)
});
Some(cx.spawn(|mut cx| async move { Some(cx.spawn(|mut cx| async move {
let context_editor = context_editor.await?; let context_editor = context_editor_task.await?;
context_editor context_editor
.update(&mut cx, |context_editor, cx| { .update(&mut cx, |context_editor, cx| {
context_editor.remote_id = Some(id); context_editor.remote_id = Some(id);
@ -3466,6 +3516,24 @@ fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
None None
} }
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),
1000..=9999 => {
let thousands = count / 1000;
let hundreds = (count % 1000 + 50) / 100;
if hundreds == 0 {
format!("{}k", thousands)
} else if hundreds == 10 {
format!("{}k", thousands + 1)
} else {
format!("{}.{}k", thousands, hundreds)
}
}
_ => format!("{}k", (count + 500) / 1000),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -20,7 +20,7 @@ use std::{
}; };
use workspace::Workspace; use workspace::Workspace;
pub(crate) struct SlashCommandCompletionProvider { pub struct SlashCommandCompletionProvider {
cancel_flag: Mutex<Arc<AtomicBool>>, cancel_flag: Mutex<Arc<AtomicBool>>,
slash_commands: Arc<SlashCommandWorkingSet>, slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>, editor: Option<WeakView<ContextEditor>>,