crates/assistant/src/assistant.rs 🔗
@@ -2,6 +2,8 @@
pub mod assistant_panel;
mod context;
+mod context_editor;
+mod context_history;
pub mod context_store;
mod inline_assistant;
mod patch;
Marshall Bowers created
This PR extracts the `ContextEditor` and `ContextHistory`
implementations into their own modules so that it's clearer which parts
depend on other constructs in the `assistant` crate.
Release Notes:
- N/A
crates/assistant/src/assistant.rs | 2
crates/assistant/src/assistant_panel.rs | 3759 +--------------------
crates/assistant/src/context_editor.rs | 3547 ++++++++++++++++++++
crates/assistant/src/context_history.rs | 249 +
crates/assistant/src/slash_command.rs | 2
crates/assistant/src/slash_command_picker.rs | 2
6 files changed, 3,986 insertions(+), 3,575 deletions(-)
@@ -2,6 +2,8 @@
pub mod assistant_panel;
mod context;
+mod context_editor;
+mod context_history;
pub mod context_store;
mod inline_assistant;
mod patch;
@@ -1,93 +1,48 @@
+use crate::context_editor::{
+ ContextEditor, ContextEditorToolbarItem, ContextEditorToolbarItemEvent, DEFAULT_TAB_TITLE,
+};
+use crate::context_history::ContextHistory;
use crate::{
- humanize_token_count, slash_command::SlashCommandCompletionProvider, slash_command_picker,
- terminal_inline_assistant::TerminalInlineAssistant, Assist, AssistantPatch,
- AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context, ContextEvent, ContextId,
- ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory,
- DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles, InsertIntoEditor,
- InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
- MessageStatus, NewContext, ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection,
- RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
- ToggleModelSelector,
+ slash_command::SlashCommandCompletionProvider,
+ terminal_inline_assistant::TerminalInlineAssistant, Context, ContextId, ContextStore,
+ ContextStoreEvent, DeployHistory, DeployPromptLibrary, InlineAssistant, InsertDraggedFiles,
+ NewContext, ToggleFocus, ToggleModelSelector,
};
use anyhow::Result;
use assistant_settings::{AssistantDockPosition, AssistantSettings};
-use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
-use assistant_slash_commands::{
- selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs,
- FileSlashCommand,
-};
+use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
-use client::{proto, zed_urls, Client, Status};
-use collections::{hash_map, BTreeSet, HashMap, HashSet};
-use editor::{
- actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
- display_map::{
- BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
- CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
- },
- scroll::{Autoscroll, AutoscrollStrategy},
- Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
- ToOffset as _, ToPoint,
-};
-use editor::{display_map::CreaseId, FoldPlaceholder};
+use client::{proto, Client, Status};
+use collections::HashMap;
+use editor::{Editor, EditorEvent};
use fs::Fs;
-use futures::FutureExt;
use gpui::{
- canvas, div, img, percentage, point, prelude::*, pulsating_between, size, Action, Animation,
- AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry,
- ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle,
- FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
- Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription,
- Task, Transformation, UpdateGlobal, View, WeakModel, WeakView,
-};
-use indexed_docs::IndexedDocsStore;
-use language::{
- language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset,
+ canvas, div, prelude::*, Action, AnyView, AppContext, AsyncWindowContext, EventEmitter,
+ ExternalPaths, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model,
+ ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
+ Task, UpdateGlobal, View, WeakView,
};
-use language_model::{LanguageModelImage, LanguageModelToolUse};
+use language::{LanguageRegistry, LspAdapterDelegate};
use language_model::{
- LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
- ZED_CLOUD_PROVIDER_ID,
+ LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
-use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
-use multi_buffer::MultiBufferRow;
-use picker::{Picker, PickerDelegate};
+use language_model_selector::LanguageModelSelector;
use project::lsp_store::LocalLspAdapterDelegate;
-use project::{Project, Worktree};
+use project::Project;
use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
-use rope::Point;
use search::{buffer_search::DivRegistrar, BufferSearchBar};
-use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings};
use smol::stream::StreamExt;
-use std::{
- any::TypeId,
- borrow::Cow,
- cmp,
- ops::{ControlFlow, Range},
- path::PathBuf,
- sync::Arc,
- time::Duration,
-};
+use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
-use text::SelectionGoal;
-use ui::{
- prelude::*,
- utils::{format_distance_from_now, DateTimeType},
- Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
- ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
-};
+use ui::{prelude::*, ContextMenu, ElevationIndex, PopoverMenu, PopoverMenuHandle, Tooltip};
use util::{maybe, ResultExt};
+use workspace::DraggedTab;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
- item::{self, FollowableItem, Item, ItemHandle},
- notifications::NotificationId,
- pane::{self, SaveIntent},
- searchable::{SearchEvent, SearchableItem},
- DraggedSelection, Pane, Save, ShowConfiguration, Toast, ToggleZoom, ToolbarItemEvent,
- ToolbarItemLocation, ToolbarItemView, Workspace,
+ item::Item,
+ pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
};
-use workspace::{searchable::SearchableItemHandle, DraggedTab};
use zed_actions::InlineAssist;
pub fn init(cx: &mut AppContext) {
@@ -143,24 +98,7 @@ pub struct AssistantPanel {
configuration_subscription: Option<Subscription>,
client_status: Option<client::Status>,
watch_client_status: Option<Task<()>>,
- show_zed_ai_notice: bool,
-}
-
-#[derive(Clone)]
-enum ContextMetadata {
- Remote(RemoteContextMetadata),
- Saved(SavedContextMetadata),
-}
-
-struct SavedContextPickerDelegate {
- store: Model<ContextStore>,
- project: Model<Project>,
- matches: Vec<ContextMetadata>,
- selected_index: usize,
-}
-
-enum SavedContextPickerEvent {
- Confirmed(ContextMetadata),
+ pub(crate) show_zed_ai_notice: bool,
}
enum InlineAssistTarget {
@@ -168,143 +106,6 @@ enum InlineAssistTarget {
Terminal(View<TerminalView>),
}
-impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
-
-impl SavedContextPickerDelegate {
- fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
- Self {
- project,
- store,
- matches: Vec::new(),
- selected_index: 0,
- }
- }
-}
-
-impl PickerDelegate for SavedContextPickerDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
- self.selected_index = ix;
- }
-
- fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
- "Search...".into()
- }
-
- fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
- let search = self.store.read(cx).search(query, cx);
- cx.spawn(|this, mut cx| async move {
- let matches = search.await;
- this.update(&mut cx, |this, cx| {
- let host_contexts = this.delegate.store.read(cx).host_contexts();
- this.delegate.matches = host_contexts
- .iter()
- .cloned()
- .map(ContextMetadata::Remote)
- .chain(matches.into_iter().map(ContextMetadata::Saved))
- .collect();
- this.delegate.selected_index = 0;
- cx.notify();
- })
- .ok();
- })
- }
-
- fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
- if let Some(metadata) = self.matches.get(self.selected_index) {
- cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
- }
- }
-
- fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let context = self.matches.get(ix)?;
- let item = match context {
- ContextMetadata::Remote(context) => {
- let host_user = self.project.read(cx).host().and_then(|collaborator| {
- self.project
- .read(cx)
- .user_store()
- .read(cx)
- .get_cached_user(collaborator.user_id)
- });
- div()
- .flex()
- .w_full()
- .justify_between()
- .gap_2()
- .child(
- h_flex().flex_1().overflow_x_hidden().child(
- Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
- .size(LabelSize::Small),
- ),
- )
- .child(
- h_flex()
- .gap_2()
- .children(if let Some(host_user) = host_user {
- vec![
- Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
- Label::new(format!("Shared by @{}", host_user.github_login))
- .color(Color::Muted)
- .size(LabelSize::Small)
- .into_any_element(),
- ]
- } else {
- vec![Label::new("Shared by host")
- .color(Color::Muted)
- .size(LabelSize::Small)
- .into_any_element()]
- }),
- )
- }
- ContextMetadata::Saved(context) => div()
- .flex()
- .w_full()
- .justify_between()
- .gap_2()
- .child(
- h_flex()
- .flex_1()
- .child(Label::new(context.title.clone()).size(LabelSize::Small))
- .overflow_x_hidden(),
- )
- .child(
- Label::new(format_distance_from_now(
- DateTimeType::Local(context.mtime),
- false,
- true,
- true,
- ))
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- };
- Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .toggle_state(selected)
- .child(item),
- )
- }
-}
-
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
@@ -959,7 +760,7 @@ impl AssistantPanel {
}
}
- fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
+ pub fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
let project = self.project.read(cx);
if project.is_via_collab() {
let task = self
@@ -1204,7 +1005,7 @@ impl AssistantPanel {
self.model_selector_menu_handle.toggle(cx);
}
- fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
+ pub(crate) fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
self.pane
.read(cx)
.active_item()?
@@ -1215,7 +1016,7 @@ impl AssistantPanel {
Some(self.active_context_editor(cx)?.read(cx).context.clone())
}
- fn open_saved_context(
+ pub fn open_saved_context(
&mut self,
path: PathBuf,
cx: &mut ViewContext<Self>,
@@ -1261,7 +1062,7 @@ impl AssistantPanel {
})
}
- fn open_remote_context(
+ pub fn open_remote_context(
&mut self,
id: ContextId,
cx: &mut ViewContext<Self>,
@@ -1503,3377 +1304,189 @@ impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
}
}
-pub enum ContextEditorEvent {
- Edited,
- TabContentChanged,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq)]
-struct ScrollPosition {
- offset_before_cursor: gpui::Point<f32>,
- cursor: Anchor,
-}
-
-struct PatchViewState {
- crease_id: CreaseId,
- editor: Option<PatchEditorState>,
- update_task: Option<Task<()>>,
-}
-
-struct PatchEditorState {
- editor: WeakView<ProposedChangesEditor>,
- opened_patch: AssistantPatch,
-}
-
-type MessageHeader = MessageMetadata;
-
-#[derive(Clone)]
-enum AssistError {
- FileRequired,
- PaymentRequired,
- MaxMonthlySpendReached,
- Message(SharedString),
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum WorkflowAssistStatus {
+ Pending,
+ Confirmed,
+ Done,
+ Idle,
}
-pub struct ContextEditor {
- context: Model<Context>,
- fs: Arc<dyn Fs>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- tools: Arc<ToolWorkingSet>,
- workspace: WeakView<Workspace>,
- project: Model<Project>,
- lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
- editor: View<Editor>,
- blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
- image_blocks: HashSet<CustomBlockId>,
- scroll_position: Option<ScrollPosition>,
- remote_id: Option<workspace::ViewId>,
- pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
- invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
- pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
- _subscriptions: Vec<Subscription>,
- patches: HashMap<Range<language::Anchor>, PatchViewState>,
- active_patch: Option<Range<language::Anchor>>,
- assistant_panel: WeakView<AssistantPanel>,
- last_error: Option<AssistError>,
- show_accept_terms: bool,
- pub(crate) slash_menu_handle:
- PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
- // dragged_file_worktrees is used to keep references to worktrees that were added
- // when the user drag/dropped an external file onto the context editor. Since
- // the worktree is not part of the project panel, it would be dropped as soon as
- // the file is opened. In order to keep the worktree alive for the duration of the
- // context editor, we keep a reference here.
- dragged_file_worktrees: Vec<Model<Worktree>>,
+pub struct ConfigurationView {
+ focus_handle: FocusHandle,
+ configuration_views: HashMap<LanguageModelProviderId, AnyView>,
+ _registry_subscription: Subscription,
}
-const DEFAULT_TAB_TITLE: &str = "New Chat";
-const MAX_TAB_TITLE_LEN: usize = 16;
+impl ConfigurationView {
+ fn new(cx: &mut ViewContext<Self>) -> Self {
+ let focus_handle = cx.focus_handle();
-impl ContextEditor {
- fn for_context(
- context: Model<Context>,
- fs: Arc<dyn Fs>,
- workspace: WeakView<Workspace>,
- project: Model<Project>,
- lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
- assistant_panel: WeakView<AssistantPanel>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let completion_provider = SlashCommandCompletionProvider::new(
- context.read(cx).slash_commands.clone(),
- Some(cx.view().downgrade()),
- Some(workspace.clone()),
+ let registry_subscription = cx.subscribe(
+ &LanguageModelRegistry::global(cx),
+ |this, _, event: &language_model::Event, cx| match event {
+ language_model::Event::AddedProvider(provider_id) => {
+ let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
+ if let Some(provider) = provider {
+ this.add_configuration_view(&provider, cx);
+ }
+ }
+ language_model::Event::RemovedProvider(provider_id) => {
+ this.remove_configuration_view(provider_id);
+ }
+ _ => {}
+ },
);
- let editor = cx.new_view(|cx| {
- let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
- editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
- editor.set_show_line_numbers(false, cx);
- editor.set_show_scrollbars(false, cx);
- editor.set_show_git_diff_gutter(false, cx);
- editor.set_show_code_actions(false, cx);
- editor.set_show_runnables(false, cx);
- editor.set_show_wrap_guides(false, cx);
- editor.set_show_indent_guides(false, cx);
- editor.set_completion_provider(Some(Box::new(completion_provider)));
- editor.set_collaboration_hub(Box::new(project.clone()));
- editor
- });
-
- let _subscriptions = vec![
- cx.observe(&context, |_, _, cx| cx.notify()),
- cx.subscribe(&context, Self::handle_context_event),
- cx.subscribe(&editor, Self::handle_editor_event),
- cx.subscribe(&editor, Self::handle_editor_search_event),
- ];
-
- let sections = context.read(cx).slash_command_output_sections().to_vec();
- let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
- let slash_commands = context.read(cx).slash_commands.clone();
- let tools = context.read(cx).tools.clone();
let mut this = Self {
- context,
- slash_commands,
- tools,
- editor,
- lsp_adapter_delegate,
- blocks: Default::default(),
- image_blocks: Default::default(),
- scroll_position: None,
- remote_id: None,
- fs,
- workspace,
- project,
- pending_slash_command_creases: HashMap::default(),
- invoked_slash_command_creases: HashMap::default(),
- pending_tool_use_creases: HashMap::default(),
- _subscriptions,
- patches: HashMap::default(),
- active_patch: None,
- assistant_panel,
- last_error: None,
- show_accept_terms: false,
- slash_menu_handle: Default::default(),
- dragged_file_worktrees: Vec::new(),
+ focus_handle,
+ configuration_views: HashMap::default(),
+ _registry_subscription: registry_subscription,
};
- this.update_message_headers(cx);
- this.update_image_blocks(cx);
- this.insert_slash_command_output_sections(sections, false, cx);
- this.patches_updated(&Vec::new(), &patch_ranges, cx);
+ this.build_configuration_views(cx);
this
}
- fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
- let command_name = DefaultSlashCommand.name();
- self.editor.update(cx, |editor, cx| {
- editor.insert(&format!("/{command_name}\n\n"), cx)
- });
- let command = self.context.update(cx, |context, cx| {
- context.reparse(cx);
- context.parsed_slash_commands()[0].clone()
- });
- self.run_command(
- command.source_range,
- &command.name,
- &command.arguments,
- false,
- self.workspace.clone(),
- cx,
- );
+ fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
+ let providers = LanguageModelRegistry::read_global(cx).providers();
+ for provider in providers {
+ self.add_configuration_view(&provider, cx);
+ }
}
- fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
- self.send_to_model(RequestType::Chat, cx);
+ fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
+ self.configuration_views.remove(provider_id);
}
- fn edit(&mut self, _: &Edit, cx: &mut ViewContext<Self>) {
- self.send_to_model(RequestType::SuggestEdits, cx);
+ fn add_configuration_view(
+ &mut self,
+ provider: &Arc<dyn LanguageModelProvider>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let configuration_view = provider.configuration_view(cx);
+ self.configuration_views
+ .insert(provider.id(), configuration_view);
}
- fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some((_range, patch)) = self.active_patch() {
- if let Some(editor) = patch
- .editor
- .as_ref()
- .and_then(|state| state.editor.upgrade())
- {
- cx.focus_view(&editor);
- return true;
+ fn render_provider_view(
+ &mut self,
+ provider: &Arc<dyn LanguageModelProvider>,
+ cx: &mut ViewContext<Self>,
+ ) -> Div {
+ let provider_id = provider.id().0.clone();
+ let provider_name = provider.name().0.clone();
+ let configuration_view = self.configuration_views.get(&provider.id()).cloned();
+
+ let open_new_context = cx.listener({
+ let provider = provider.clone();
+ move |_, _, cx| {
+ cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
+ provider.clone(),
+ ))
}
- }
+ });
- false
+ v_flex()
+ .gap_2()
+ .child(
+ h_flex()
+ .justify_between()
+ .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
+ .when(provider.is_authenticated(cx), move |this| {
+ this.child(
+ h_flex().justify_end().child(
+ Button::new(
+ SharedString::from(format!("new-context-{provider_id}")),
+ "Open New Chat",
+ )
+ .icon_position(IconPosition::Start)
+ .icon(IconName::Plus)
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ModalSurface)
+ .on_click(open_new_context),
+ ),
+ )
+ }),
+ )
+ .child(
+ div()
+ .p(DynamicSpacing::Base08.rems(cx))
+ .bg(cx.theme().colors().surface_background)
+ .border_1()
+ .border_color(cx.theme().colors().border_variant)
+ .rounded_md()
+ .when(configuration_view.is_none(), |this| {
+ this.child(div().child(Label::new(format!(
+ "No configuration view for {}",
+ provider_name
+ ))))
+ })
+ .when_some(configuration_view, |this, configuration_view| {
+ this.child(configuration_view)
+ }),
+ )
}
+}
- fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext<Self>) {
- let provider = LanguageModelRegistry::read_global(cx).active_provider();
- if provider
- .as_ref()
- .map_or(false, |provider| provider.must_accept_terms(cx))
- {
- self.show_accept_terms = true;
- cx.notify();
- return;
- }
-
- if self.focus_active_patch(cx) {
- return;
- }
-
- self.last_error = None;
+impl Render for ConfigurationView {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let providers = LanguageModelRegistry::read_global(cx).providers();
+ let provider_views = providers
+ .into_iter()
+ .map(|provider| self.render_provider_view(&provider, cx))
+ .collect::<Vec<_>>();
- if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
- self.last_error = Some(AssistError::FileRequired);
- cx.notify();
- } else if let Some(user_message) = self
- .context
- .update(cx, |context, cx| context.assist(request_type, cx))
- {
- let new_selection = {
- let cursor = user_message
- .start
- .to_offset(self.context.read(cx).buffer().read(cx));
- cursor..cursor
- };
- self.editor.update(cx, |editor, cx| {
- editor.change_selections(
- Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
- cx,
- |selections| selections.select_ranges([new_selection]),
- );
- });
- // Avoid scrolling to the new cursor position so the assistant's output is stable.
- cx.defer(|this, _| this.scroll_position = None);
- }
+ let mut element = v_flex()
+ .id("assistant-configuration-view")
+ .track_focus(&self.focus_handle(cx))
+ .bg(cx.theme().colors().editor_background)
+ .size_full()
+ .overflow_y_scroll()
+ .child(
+ v_flex()
+ .p(DynamicSpacing::Base16.rems(cx))
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .gap_1()
+ .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
+ .child(
+ Label::new(
+ "At least one LLM provider must be configured to use the Assistant.",
+ )
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ v_flex()
+ .p(DynamicSpacing::Base16.rems(cx))
+ .mt_1()
+ .gap_6()
+ .flex_1()
+ .children(provider_views),
+ )
+ .into_any();
- cx.notify();
+ // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
+ // because we couldn't the element to take up the size of the parent.
+ canvas(
+ move |bounds, cx| {
+ element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
+ element
+ },
+ |_, mut element, cx| {
+ element.paint(cx);
+ },
+ )
+ .flex_1()
+ .w_full()
}
+}
- fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
- self.last_error = None;
+pub enum ConfigurationViewEvent {
+ NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
+}
- if self
- .context
- .update(cx, |context, cx| context.cancel_last_assist(cx))
- {
- return;
- }
-
- cx.propagate();
- }
-
- fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
- let cursors = self.cursors(cx);
- self.context.update(cx, |context, cx| {
- let messages = context
- .messages_for_offsets(cursors, cx)
- .into_iter()
- .map(|message| message.id)
- .collect();
- context.cycle_message_roles(messages, cx)
- });
- }
-
- fn cursors(&self, cx: &mut WindowContext) -> Vec<usize> {
- let selections = self
- .editor
- .update(cx, |editor, cx| editor.selections.all::<usize>(cx));
- selections
- .into_iter()
- .map(|selection| selection.head())
- .collect()
- }
-
- pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
- if let Some(command) = self.slash_commands.command(name, cx) {
- self.editor.update(cx, |editor, cx| {
- editor.transact(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let newest_cursor = editor.selections.newest::<Point>(cx).head();
- if newest_cursor.column > 0
- || snapshot
- .chars_at(newest_cursor)
- .next()
- .map_or(false, |ch| ch != '\n')
- {
- editor.move_to_end_of_line(
- &MoveToEndOfLine {
- stop_at_soft_wraps: false,
- },
- cx,
- );
- editor.newline(&Newline, cx);
- }
-
- editor.insert(&format!("/{name}"), cx);
- if command.accepts_arguments() {
- editor.insert(" ", cx);
- editor.show_completions(&ShowCompletions::default(), cx);
- }
- });
- });
- if !command.requires_argument() {
- self.confirm_command(&ConfirmCommand, cx);
- }
- }
- }
-
- pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
- if self.editor.read(cx).has_active_completions_menu() {
- return;
- }
-
- let selections = self.editor.read(cx).selections.disjoint_anchors();
- let mut commands_by_range = HashMap::default();
- let workspace = self.workspace.clone();
- self.context.update(cx, |context, cx| {
- context.reparse(cx);
- for selection in selections.iter() {
- if let Some(command) =
- context.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.arguments,
- true,
- workspace.clone(),
- cx,
- );
- }
- cx.stop_propagation();
- }
- }
-
- #[allow(clippy::too_many_arguments)]
- pub fn run_command(
- &mut self,
- command_range: Range<language::Anchor>,
- name: &str,
- arguments: &[String],
- ensure_trailing_newline: bool,
- workspace: WeakView<Workspace>,
- cx: &mut ViewContext<Self>,
- ) {
- if let Some(command) = self.slash_commands.command(name, cx) {
- let context = self.context.read(cx);
- let sections = context
- .slash_command_output_sections()
- .into_iter()
- .filter(|section| section.is_valid(context.buffer().read(cx)))
- .cloned()
- .collect::<Vec<_>>();
- let snapshot = context.buffer().read(cx).snapshot();
- let output = command.run(
- arguments,
- §ions,
- snapshot,
- workspace,
- self.lsp_adapter_delegate.clone(),
- cx,
- );
- self.context.update(cx, |context, cx| {
- context.insert_command_output(
- command_range,
- name,
- output,
- ensure_trailing_newline,
- cx,
- )
- });
- }
- }
-
- fn handle_context_event(
- &mut self,
- _: Model<Context>,
- event: &ContextEvent,
- cx: &mut ViewContext<Self>,
- ) {
- let context_editor = cx.view().downgrade();
-
- match event {
- ContextEvent::MessagesEdited => {
- self.update_message_headers(cx);
- self.update_image_blocks(cx);
- self.context.update(cx, |context, cx| {
- context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
- });
- }
- ContextEvent::SummaryChanged => {
- cx.emit(EditorEvent::TitleChanged);
- self.context.update(cx, |context, cx| {
- context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
- });
- }
- ContextEvent::StreamedCompletion => {
- self.editor.update(cx, |editor, cx| {
- if let Some(scroll_position) = self.scroll_position {
- let snapshot = editor.snapshot(cx);
- let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
- let scroll_top =
- cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
- editor.set_scroll_position(
- point(scroll_position.offset_before_cursor.x, scroll_top),
- cx,
- );
- }
-
- let new_tool_uses = self
- .context
- .read(cx)
- .pending_tool_uses()
- .into_iter()
- .filter(|tool_use| {
- !self
- .pending_tool_use_creases
- .contains_key(&tool_use.source_range)
- })
- .cloned()
- .collect::<Vec<_>>();
-
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
- let excerpt_id = *excerpt_id;
-
- let mut buffer_rows_to_fold = BTreeSet::new();
-
- let creases = new_tool_uses
- .iter()
- .map(|tool_use| {
- let placeholder = FoldPlaceholder {
- render: render_fold_icon_button(
- cx.view().downgrade(),
- IconName::PocketKnife,
- tool_use.name.clone().into(),
- ),
- ..Default::default()
- };
- let render_trailer =
- move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
-
- let start = buffer
- .anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
- .unwrap();
- let end = buffer
- .anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
- .unwrap();
-
- let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
- buffer_rows_to_fold.insert(buffer_row);
-
- self.context.update(cx, |context, cx| {
- context.insert_content(
- Content::ToolUse {
- range: tool_use.source_range.clone(),
- tool_use: LanguageModelToolUse {
- id: tool_use.id.clone(),
- name: tool_use.name.clone(),
- input: tool_use.input.clone(),
- },
- },
- cx,
- );
- });
-
- Crease::inline(
- start..end,
- placeholder,
- fold_toggle("tool-use"),
- render_trailer,
- )
- })
- .collect::<Vec<_>>();
-
- let crease_ids = editor.insert_creases(creases, cx);
-
- for buffer_row in buffer_rows_to_fold.into_iter().rev() {
- editor.fold_at(&FoldAt { buffer_row }, cx);
- }
-
- self.pending_tool_use_creases.extend(
- new_tool_uses
- .iter()
- .map(|tool_use| tool_use.source_range.clone())
- .zip(crease_ids),
- );
- });
- }
- ContextEvent::PatchesUpdated { removed, updated } => {
- self.patches_updated(removed, updated, cx);
- }
- ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
-
- editor.remove_creases(
- removed
- .iter()
- .filter_map(|range| self.pending_slash_command_creases.remove(range)),
- cx,
- );
-
- let crease_ids = editor.insert_creases(
- updated.iter().map(|command| {
- let workspace = self.workspace.clone();
- let confirm_command = Arc::new({
- let context_editor = context_editor.clone();
- let command = command.clone();
- move |cx: &mut WindowContext| {
- context_editor
- .update(cx, |context_editor, cx| {
- context_editor.run_command(
- command.source_range.clone(),
- &command.name,
- &command.arguments,
- false,
- workspace.clone(),
- cx,
- );
- })
- .ok();
- }
- });
- let placeholder = FoldPlaceholder {
- render: Arc::new(move |_, _, _| Empty.into_any()),
- ..Default::default()
- };
- let render_toggle = {
- let confirm_command = confirm_command.clone();
- let command = command.clone();
- move |row, _, _, _cx: &mut WindowContext| {
- render_pending_slash_command_gutter_decoration(
- row,
- &command.status,
- confirm_command.clone(),
- )
- }
- };
- let render_trailer = {
- let command = command.clone();
- move |row, _unfold, cx: &mut WindowContext| {
- // TODO: In the future we should investigate how we can expose
- // this as a hook on the `SlashCommand` trait so that we don't
- // need to special-case it here.
- if command.name == DocsSlashCommand::NAME {
- return render_docs_slash_command_trailer(
- row,
- command.clone(),
- cx,
- );
- }
-
- Empty.into_any()
- }
- };
-
- let start = buffer
- .anchor_in_excerpt(excerpt_id, command.source_range.start)
- .unwrap();
- let end = buffer
- .anchor_in_excerpt(excerpt_id, command.source_range.end)
- .unwrap();
- Crease::inline(start..end, placeholder, render_toggle, render_trailer)
- }),
- cx,
- );
-
- self.pending_slash_command_creases.extend(
- updated
- .iter()
- .map(|command| command.source_range.clone())
- .zip(crease_ids),
- );
- })
- }
- ContextEvent::InvokedSlashCommandChanged { command_id } => {
- self.update_invoked_slash_command(*command_id, cx);
- }
- ContextEvent::SlashCommandOutputSectionAdded { section } => {
- self.insert_slash_command_output_sections([section.clone()], false, cx);
- }
- ContextEvent::UsePendingTools => {
- let pending_tool_uses = self
- .context
- .read(cx)
- .pending_tool_uses()
- .into_iter()
- .filter(|tool_use| tool_use.status.is_idle())
- .cloned()
- .collect::<Vec<_>>();
-
- for tool_use in pending_tool_uses {
- if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
- let task = tool.run(tool_use.input, self.workspace.clone(), cx);
-
- self.context.update(cx, |context, cx| {
- context.insert_tool_output(tool_use.id.clone(), task, cx);
- });
- }
- }
- }
- ContextEvent::ToolFinished {
- tool_use_id,
- output_range,
- } => {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
- let excerpt_id = *excerpt_id;
-
- let placeholder = FoldPlaceholder {
- render: render_fold_icon_button(
- cx.view().downgrade(),
- IconName::PocketKnife,
- format!("Tool Result: {tool_use_id}").into(),
- ),
- ..Default::default()
- };
- let render_trailer =
- move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
-
- let start = buffer
- .anchor_in_excerpt(excerpt_id, output_range.start)
- .unwrap();
- let end = buffer
- .anchor_in_excerpt(excerpt_id, output_range.end)
- .unwrap();
-
- let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
-
- let crease = Crease::inline(
- start..end,
- placeholder,
- fold_toggle("tool-use"),
- render_trailer,
- );
-
- editor.insert_creases([crease], cx);
- editor.fold_at(&FoldAt { buffer_row }, cx);
- });
- }
- ContextEvent::Operation(_) => {}
- ContextEvent::ShowAssistError(error_message) => {
- self.last_error = Some(AssistError::Message(error_message.clone()));
- }
- ContextEvent::ShowPaymentRequiredError => {
- self.last_error = Some(AssistError::PaymentRequired);
- }
- ContextEvent::ShowMaxMonthlySpendReachedError => {
- self.last_error = Some(AssistError::MaxMonthlySpendReached);
- }
- }
- }
-
- fn update_invoked_slash_command(
- &mut self,
- command_id: InvokedSlashCommandId,
- cx: &mut ViewContext<Self>,
- ) {
- if let Some(invoked_slash_command) =
- self.context.read(cx).invoked_slash_command(&command_id)
- {
- if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
- let run_commands_in_ranges = invoked_slash_command
- .run_commands_in_ranges
- .iter()
- .cloned()
- .collect::<Vec<_>>();
- for range in run_commands_in_ranges {
- let commands = self.context.update(cx, |context, cx| {
- context.reparse(cx);
- context
- .pending_commands_for_range(range.clone(), cx)
- .to_vec()
- });
-
- for command in commands {
- self.run_command(
- command.source_range,
- &command.name,
- &command.arguments,
- false,
- self.workspace.clone(),
- cx,
- );
- }
- }
- }
- }
-
- self.editor.update(cx, |editor, cx| {
- if let Some(invoked_slash_command) =
- self.context.read(cx).invoked_slash_command(&command_id)
- {
- if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (&excerpt_id, _buffer_id, _buffer_snapshot) =
- buffer.as_singleton().unwrap();
-
- let start = buffer
- .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
- .unwrap();
- let end = buffer
- .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
- .unwrap();
- editor.remove_folds_with_type(
- &[start..end],
- TypeId::of::<PendingSlashCommand>(),
- false,
- cx,
- );
-
- editor.remove_creases(
- HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
- cx,
- );
- } else if let hash_map::Entry::Vacant(entry) =
- self.invoked_slash_command_creases.entry(command_id)
- {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (&excerpt_id, _buffer_id, _buffer_snapshot) =
- buffer.as_singleton().unwrap();
- let context = self.context.downgrade();
- let crease_start = buffer
- .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
- .unwrap();
- let crease_end = buffer
- .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
- .unwrap();
- let crease = Crease::inline(
- crease_start..crease_end,
- invoked_slash_command_fold_placeholder(command_id, context),
- fold_toggle("invoked-slash-command"),
- |_row, _folded, _cx| Empty.into_any(),
- );
- let crease_ids = editor.insert_creases([crease.clone()], cx);
- editor.fold_creases(vec![crease], false, cx);
- entry.insert(crease_ids[0]);
- } else {
- cx.notify()
- }
- } else {
- editor.remove_creases(
- HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
- cx,
- );
- cx.notify();
- };
- });
- }
-
- fn patches_updated(
- &mut self,
- removed: &Vec<Range<text::Anchor>>,
- updated: &Vec<Range<text::Anchor>>,
- cx: &mut ViewContext<ContextEditor>,
- ) {
- let this = cx.view().downgrade();
- let mut editors_to_close = Vec::new();
-
- self.editor.update(cx, |editor, cx| {
- let snapshot = editor.snapshot(cx);
- let multibuffer = &snapshot.buffer_snapshot;
- let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
-
- let mut removed_crease_ids = Vec::new();
- let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
- for range in removed {
- if let Some(state) = self.patches.remove(range) {
- let patch_start = multibuffer
- .anchor_in_excerpt(excerpt_id, range.start)
- .unwrap();
- let patch_end = multibuffer
- .anchor_in_excerpt(excerpt_id, range.end)
- .unwrap();
-
- editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
- ranges_to_unfold.push(patch_start..patch_end);
- removed_crease_ids.push(state.crease_id);
- }
- }
- editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
- editor.remove_creases(removed_crease_ids, cx);
-
- for range in updated {
- let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
- continue;
- };
-
- let path_count = patch.path_count();
- let patch_start = multibuffer
- .anchor_in_excerpt(excerpt_id, patch.range.start)
- .unwrap();
- let patch_end = multibuffer
- .anchor_in_excerpt(excerpt_id, patch.range.end)
- .unwrap();
- let render_block: RenderBlock = Arc::new({
- let this = this.clone();
- let patch_range = range.clone();
- move |cx: &mut BlockContext<'_, '_>| {
- let max_width = cx.max_width;
- let gutter_width = cx.gutter_dimensions.full_width();
- let block_id = cx.block_id;
- let selected = cx.selected;
- this.update(&mut **cx, |this, cx| {
- this.render_patch_block(
- patch_range.clone(),
- max_width,
- gutter_width,
- block_id,
- selected,
- cx,
- )
- })
- .ok()
- .flatten()
- .unwrap_or_else(|| Empty.into_any())
- }
- });
-
- let height = path_count as u32 + 1;
- let crease = Crease::block(
- patch_start..patch_end,
- height,
- BlockStyle::Flex,
- render_block.clone(),
- );
-
- let should_refold;
- if let Some(state) = self.patches.get_mut(&range) {
- if let Some(editor_state) = &state.editor {
- if editor_state.opened_patch != patch {
- state.update_task = Some({
- let this = this.clone();
- cx.spawn(|_, cx| async move {
- Self::update_patch_editor(this.clone(), patch, cx)
- .await
- .log_err();
- })
- });
- }
- }
-
- should_refold =
- snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
- } else {
- let crease_id = editor.insert_creases([crease.clone()], cx)[0];
- self.patches.insert(
- range.clone(),
- PatchViewState {
- crease_id,
- editor: None,
- update_task: None,
- },
- );
-
- should_refold = true;
- }
-
- if should_refold {
- editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
- editor.fold_creases(vec![crease], false, cx);
- }
- }
- });
-
- for editor in editors_to_close {
- self.close_patch_editor(editor, cx);
- }
-
- self.update_active_patch(cx);
- }
-
- fn insert_slash_command_output_sections(
- &mut self,
- sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
- expand_result: bool,
- cx: &mut ViewContext<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let excerpt_id = *buffer.as_singleton().unwrap().0;
- let mut buffer_rows_to_fold = BTreeSet::new();
- let mut creases = Vec::new();
- for section in sections {
- let start = buffer
- .anchor_in_excerpt(excerpt_id, section.range.start)
- .unwrap();
- let end = buffer
- .anchor_in_excerpt(excerpt_id, section.range.end)
- .unwrap();
- let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
- buffer_rows_to_fold.insert(buffer_row);
- creases.push(
- Crease::inline(
- start..end,
- FoldPlaceholder {
- render: render_fold_icon_button(
- cx.view().downgrade(),
- section.icon,
- section.label.clone(),
- ),
- merge_adjacent: false,
- ..Default::default()
- },
- render_slash_command_output_toggle,
- |_, _, _| Empty.into_any_element(),
- )
- .with_metadata(CreaseMetadata {
- icon: section.icon,
- label: section.label,
- }),
- );
- }
-
- editor.insert_creases(creases, cx);
-
- if expand_result {
- buffer_rows_to_fold.clear();
- }
- for buffer_row in buffer_rows_to_fold.into_iter().rev() {
- editor.fold_at(&FoldAt { buffer_row }, cx);
- }
- });
- }
-
- fn handle_editor_event(
- &mut self,
- _: View<Editor>,
- event: &EditorEvent,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
- let cursor_scroll_position = self.cursor_scroll_position(cx);
- if *autoscroll {
- self.scroll_position = cursor_scroll_position;
- } else if self.scroll_position != cursor_scroll_position {
- self.scroll_position = None;
- }
- }
- EditorEvent::SelectionsChanged { .. } => {
- self.scroll_position = self.cursor_scroll_position(cx);
- self.update_active_patch(cx);
- }
- _ => {}
- }
- cx.emit(event.clone());
- }
-
- fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
- let patch = self.active_patch.as_ref()?;
- Some((patch.clone(), self.patches.get(&patch)?))
- }
-
- fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
- let newest_cursor = self.editor.update(cx, |editor, cx| {
- editor.selections.newest::<Point>(cx).head()
- });
- let context = self.context.read(cx);
-
- let new_patch = context.patch_containing(newest_cursor, cx).cloned();
-
- if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
- return;
- }
-
- if let Some(old_patch_range) = self.active_patch.take() {
- if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
- if let Some(state) = patch_state.editor.take() {
- if let Some(editor) = state.editor.upgrade() {
- self.close_patch_editor(editor, cx);
- }
- }
- }
- }
-
- if let Some(new_patch) = new_patch {
- self.active_patch = Some(new_patch.range.clone());
-
- if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
- let mut editor = None;
- if let Some(state) = &patch_state.editor {
- if let Some(opened_editor) = state.editor.upgrade() {
- editor = Some(opened_editor);
- }
- }
-
- if let Some(editor) = editor {
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.activate_item(&editor, true, false, cx);
- })
- .ok();
- } else {
- patch_state.update_task = Some(cx.spawn(move |this, cx| async move {
- Self::open_patch_editor(this, new_patch, cx).await.log_err();
- }));
- }
- }
- }
- }
-
- fn close_patch_editor(
- &mut self,
- editor: View<ProposedChangesEditor>,
- cx: &mut ViewContext<ContextEditor>,
- ) {
- self.workspace
- .update(cx, |workspace, cx| {
- if let Some(pane) = workspace.pane_for(&editor) {
- pane.update(cx, |pane, cx| {
- let item_id = editor.entity_id();
- if !editor.read(cx).focus_handle(cx).is_focused(cx) {
- pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
- .detach_and_log_err(cx);
- }
- });
- }
- })
- .ok();
- }
-
- async fn open_patch_editor(
- this: WeakView<Self>,
- patch: AssistantPatch,
- mut cx: AsyncWindowContext,
- ) -> Result<()> {
- let project = this.update(&mut cx, |this, _| this.project.clone())?;
- let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
-
- let editor = cx.new_view(|cx| {
- let editor = ProposedChangesEditor::new(
- patch.title.clone(),
- resolved_patch
- .edit_groups
- .iter()
- .map(|(buffer, groups)| ProposedChangeLocation {
- buffer: buffer.clone(),
- ranges: groups
- .iter()
- .map(|group| group.context_range.clone())
- .collect(),
- })
- .collect(),
- Some(project.clone()),
- cx,
- );
- resolved_patch.apply(&editor, cx);
- editor
- })?;
-
- this.update(&mut cx, |this, cx| {
- if let Some(patch_state) = this.patches.get_mut(&patch.range) {
- patch_state.editor = Some(PatchEditorState {
- editor: editor.downgrade(),
- opened_patch: patch,
- });
- patch_state.update_task.take();
- }
-
- this.workspace
- .update(cx, |workspace, cx| {
- workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
- })
- .log_err();
- })?;
-
- Ok(())
- }
-
- async fn update_patch_editor(
- this: WeakView<Self>,
- patch: AssistantPatch,
- mut cx: AsyncWindowContext,
- ) -> Result<()> {
- let project = this.update(&mut cx, |this, _| this.project.clone())?;
- let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
- this.update(&mut cx, |this, cx| {
- let patch_state = this.patches.get_mut(&patch.range)?;
-
- let locations = resolved_patch
- .edit_groups
- .iter()
- .map(|(buffer, groups)| ProposedChangeLocation {
- buffer: buffer.clone(),
- ranges: groups
- .iter()
- .map(|group| group.context_range.clone())
- .collect(),
- })
- .collect();
-
- if let Some(state) = &mut patch_state.editor {
- if let Some(editor) = state.editor.upgrade() {
- editor.update(cx, |editor, cx| {
- editor.set_title(patch.title.clone(), cx);
- editor.reset_locations(locations, cx);
- resolved_patch.apply(editor, cx);
- });
-
- state.opened_patch = patch;
- } else {
- patch_state.editor.take();
- }
- }
- patch_state.update_task.take();
-
- Some(())
- })?;
- Ok(())
- }
-
- fn handle_editor_search_event(
- &mut self,
- _: View<Editor>,
- event: &SearchEvent,
- cx: &mut ViewContext<Self>,
- ) {
- cx.emit(event.clone());
- }
-
- fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
- self.editor.update(cx, |editor, cx| {
- let snapshot = editor.snapshot(cx);
- let cursor = editor.selections.newest_anchor().head();
- let cursor_row = cursor
- .to_display_point(&snapshot.display_snapshot)
- .row()
- .as_f32();
- let scroll_position = editor
- .scroll_manager
- .anchor()
- .scroll_position(&snapshot.display_snapshot);
-
- let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
- if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
- Some(ScrollPosition {
- cursor,
- offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
- })
- } else {
- None
- }
- })
- }
-
- fn esc_kbd(cx: &WindowContext) -> Div {
- let colors = cx.theme().colors().clone();
-
- h_flex()
- .items_center()
- .gap_1()
- .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
- .text_size(TextSize::XSmall.rems(cx))
- .text_color(colors.text_muted)
- .child("Press")
- .child(
- h_flex()
- .rounded_md()
- .px_1()
- .mr_0p5()
- .border_1()
- .border_color(theme::color_alpha(colors.border_variant, 0.6))
- .bg(theme::color_alpha(colors.element_background, 0.6))
- .child("esc"),
- )
- .child("to cancel")
- }
-
- fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
-
- let excerpt_id = *buffer.as_singleton().unwrap().0;
- let mut old_blocks = std::mem::take(&mut self.blocks);
- let mut blocks_to_remove: HashMap<_, _> = old_blocks
- .iter()
- .map(|(message_id, (_, block_id))| (*message_id, *block_id))
- .collect();
- let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
-
- let render_block = |message: MessageMetadata| -> RenderBlock {
- Arc::new({
- let context = self.context.clone();
-
- move |cx| {
- let message_id = MessageId(message.timestamp);
- let llm_loading = message.role == Role::Assistant
- && message.status == MessageStatus::Pending;
-
- let (label, spinner, note) = match message.role {
- Role::User => (
- Label::new("You").color(Color::Default).into_any_element(),
- None,
- None,
- ),
- Role::Assistant => {
- let base_label = Label::new("Assistant").color(Color::Info);
- let mut spinner = None;
- let mut note = None;
- let animated_label = if llm_loading {
- base_label
- .with_animation(
- "pulsating-label",
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- |label, delta| label.alpha(delta),
- )
- .into_any_element()
- } else {
- base_label.into_any_element()
- };
- if llm_loading {
- spinner = Some(
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::XSmall)
- .color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(
- percentage(delta),
- ))
- },
- )
- .into_any_element(),
- );
- note = Some(Self::esc_kbd(cx).into_any_element());
- }
- (animated_label, spinner, note)
- }
- Role::System => (
- Label::new("System")
- .color(Color::Warning)
- .into_any_element(),
- None,
- None,
- ),
- };
-
- let sender = h_flex()
- .items_center()
- .gap_2p5()
- .child(
- ButtonLike::new("role")
- .style(ButtonStyle::Filled)
- .child(
- h_flex()
- .items_center()
- .gap_1p5()
- .child(label)
- .children(spinner),
- )
- .tooltip(|cx| {
- Tooltip::with_meta(
- "Toggle message role",
- None,
- "Available roles: You (User), Assistant, System",
- cx,
- )
- })
- .on_click({
- let context = context.clone();
- move |_, cx| {
- context.update(cx, |context, cx| {
- context.cycle_message_roles(
- HashSet::from_iter(Some(message_id)),
- cx,
- )
- })
- }
- }),
- )
- .children(note);
-
- h_flex()
- .id(("message_header", message_id.as_u64()))
- .pl(cx.gutter_dimensions.full_width())
- .h_11()
- .w_full()
- .relative()
- .gap_1p5()
- .child(sender)
- .children(match &message.cache {
- Some(cache) if cache.is_final_anchor => match cache.status {
- CacheStatus::Cached => Some(
- div()
- .id("cached")
- .child(
- Icon::new(IconName::DatabaseZap)
- .size(IconSize::XSmall)
- .color(Color::Hint),
- )
- .tooltip(|cx| {
- Tooltip::with_meta(
- "Context Cached",
- None,
- "Large messages cached to optimize performance",
- cx,
- )
- })
- .into_any_element(),
- ),
- CacheStatus::Pending => Some(
- div()
- .child(
- Icon::new(IconName::Ellipsis)
- .size(IconSize::XSmall)
- .color(Color::Hint),
- )
- .into_any_element(),
- ),
- },
- _ => None,
- })
- .children(match &message.status {
- MessageStatus::Error(error) => Some(
- Button::new("show-error", "Error")
- .color(Color::Error)
- .selected_label_color(Color::Error)
- .selected_icon_color(Color::Error)
- .icon(IconName::XCircle)
- .icon_color(Color::Error)
- .icon_size(IconSize::XSmall)
- .icon_position(IconPosition::Start)
- .tooltip(move |cx| Tooltip::text("View Details", cx))
- .on_click({
- let context = context.clone();
- let error = error.clone();
- move |_, cx| {
- context.update(cx, |_, cx| {
- cx.emit(ContextEvent::ShowAssistError(
- error.clone(),
- ));
- });
- }
- })
- .into_any_element(),
- ),
- MessageStatus::Canceled => Some(
- h_flex()
- .gap_1()
- .items_center()
- .child(
- Icon::new(IconName::XCircle)
- .color(Color::Disabled)
- .size(IconSize::XSmall),
- )
- .child(
- Label::new("Canceled")
- .size(LabelSize::Small)
- .color(Color::Disabled),
- )
- .into_any_element(),
- ),
- _ => None,
- })
- .into_any_element()
- }
- })
- };
- let create_block_properties = |message: &Message| BlockProperties {
- height: 2,
- style: BlockStyle::Sticky,
- placement: BlockPlacement::Above(
- buffer
- .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
- .unwrap(),
- ),
- priority: usize::MAX,
- render: render_block(MessageMetadata::from(message)),
- };
- let mut new_blocks = vec![];
- let mut block_index_to_message = vec![];
- for message in self.context.read(cx).messages(cx) {
- if let Some(_) = blocks_to_remove.remove(&message.id) {
- // This is an old message that we might modify.
- let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
- debug_assert!(
- false,
- "old_blocks should contain a message_id we've just removed."
- );
- continue;
- };
- // Should we modify it?
- let message_meta = MessageMetadata::from(&message);
- if meta != &message_meta {
- blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
- *meta = message_meta;
- }
- } else {
- // This is a new message.
- new_blocks.push(create_block_properties(&message));
- block_index_to_message.push((message.id, MessageMetadata::from(&message)));
- }
- }
- editor.replace_blocks(blocks_to_replace, None, cx);
- editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
-
- let ids = editor.insert_blocks(new_blocks, None, cx);
- old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
- |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
- ));
- self.blocks = old_blocks;
- });
- }
-
- /// Returns either the selected text, or the content of the Markdown code
- /// block surrounding the cursor.
- fn get_selection_or_code_block(
- context_editor_view: &View<ContextEditor>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<(String, bool)> {
- const CODE_FENCE_DELIMITER: &'static str = "```";
-
- let context_editor = context_editor_view.read(cx).editor.clone();
- context_editor.update(cx, |context_editor, cx| {
- if context_editor.selections.newest::<Point>(cx).is_empty() {
- let snapshot = context_editor.buffer().read(cx).snapshot(cx);
- let (_, _, snapshot) = snapshot.as_singleton()?;
-
- let head = context_editor.selections.newest::<Point>(cx).head();
- let offset = snapshot.point_to_offset(head);
-
- let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
- let mut text = snapshot
- .text_for_range(surrounding_code_block_range)
- .collect::<String>();
-
- // If there is no newline trailing the closing three-backticks, then
- // tree-sitter-md extends the range of the content node to include
- // the backticks.
- if text.ends_with(CODE_FENCE_DELIMITER) {
- text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
- }
-
- (!text.is_empty()).then_some((text, true))
- } else {
- let anchor = context_editor.selections.newest_anchor();
- let text = context_editor
- .buffer()
- .read(cx)
- .read(cx)
- .text_for_range(anchor.range())
- .collect::<String>();
-
- (!text.is_empty()).then_some((text, false))
- }
- })
- }
-
- fn insert_selection(
- workspace: &mut Workspace,
- _: &InsertIntoEditor,
- cx: &mut ViewContext<Workspace>,
- ) {
- let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
- return;
- };
- let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
- return;
- };
- let Some(active_editor_view) = workspace
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(cx))
- else {
- return;
- };
-
- if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
- active_editor_view.update(cx, |editor, cx| {
- editor.insert(&text, cx);
- editor.focus(cx);
- })
- }
- }
-
- fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) {
- let result = maybe!({
- let panel = workspace.panel::<AssistantPanel>(cx)?;
- let context_editor_view = panel.read(cx).active_context_editor(cx)?;
- Self::get_selection_or_code_block(&context_editor_view, cx)
- });
- let Some((text, is_code_block)) = result else {
- return;
- };
-
- cx.write_to_clipboard(ClipboardItem::new_string(text));
-
- struct CopyToClipboardToast;
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<CopyToClipboardToast>(),
- format!(
- "{} copied to clipboard.",
- if is_code_block {
- "Code block"
- } else {
- "Selection"
- }
- ),
- )
- .autohide(),
- cx,
- );
- }
-
- fn insert_dragged_files(
- workspace: &mut Workspace,
- action: &InsertDraggedFiles,
- cx: &mut ViewContext<Workspace>,
- ) {
- let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
- return;
- };
- let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
- return;
- };
-
- let project = workspace.project().clone();
-
- let paths = match action {
- InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
- InsertDraggedFiles::ExternalFiles(paths) => {
- let tasks = paths
- .clone()
- .into_iter()
- .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
- .collect::<Vec<_>>();
-
- cx.spawn(move |_, cx| async move {
- let mut paths = vec![];
- let mut worktrees = vec![];
-
- let opened_paths = futures::future::join_all(tasks).await;
- for (worktree, project_path) in opened_paths.into_iter().flatten() {
- let Ok(worktree_root_name) =
- worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string())
- else {
- continue;
- };
-
- let mut full_path = PathBuf::from(worktree_root_name.clone());
- full_path.push(&project_path.path);
- paths.push(full_path);
- worktrees.push(worktree);
- }
-
- (paths, worktrees)
- })
- }
- };
-
- cx.spawn(|_, mut cx| async move {
- let (paths, dragged_file_worktrees) = paths.await;
- let cmd_name = FileSlashCommand.name();
-
- context_editor_view
- .update(&mut cx, |context_editor, cx| {
- let file_argument = paths
- .into_iter()
- .map(|path| path.to_string_lossy().to_string())
- .collect::<Vec<_>>()
- .join(" ");
-
- context_editor.editor.update(cx, |editor, cx| {
- editor.insert("\n", cx);
- editor.insert(&format!("/{} {}", cmd_name, file_argument), cx);
- });
-
- context_editor.confirm_command(&ConfirmCommand, cx);
-
- context_editor
- .dragged_file_worktrees
- .extend(dragged_file_worktrees);
- })
- .log_err();
- })
- .detach();
- }
-
- fn quote_selection(
- workspace: &mut Workspace,
- _: &QuoteSelection,
- cx: &mut ViewContext<Workspace>,
- ) {
- let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
- return;
- };
-
- let Some(creases) = selections_creases(workspace, cx) else {
- return;
- };
-
- if creases.is_empty() {
- 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.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);
-
- 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);
-
- editor.insert("\n", cx);
-
- let fold_placeholder = quote_selection_fold_placeholder(
- crease_title,
- cx.view().downgrade(),
- );
- let crease = Crease::inline(
- anchor_before..anchor_after,
- fold_placeholder,
- render_quote_selection_output_toggle,
- |_, _, _| Empty.into_any(),
- );
- editor.insert_creases(vec![crease], cx);
- editor.fold_at(
- &FoldAt {
- buffer_row: start_row,
- },
- cx,
- );
- }
- })
- });
- };
- });
- });
- }
-
- fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
- if self.editor.read(cx).selections.count() == 1 {
- let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
- cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
- copied_text,
- metadata,
- ));
- cx.stop_propagation();
- return;
- }
-
- cx.propagate();
- }
-
- fn cut(&mut self, _: &editor::actions::Cut, cx: &mut ViewContext<Self>) {
- if self.editor.read(cx).selections.count() == 1 {
- let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
-
- self.editor.update(cx, |editor, cx| {
- editor.transact(cx, |this, cx| {
- this.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.select(selections);
- });
- this.insert("", cx);
- cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
- copied_text,
- metadata,
- ));
- });
- });
-
- cx.stop_propagation();
- return;
- }
-
- cx.propagate();
- }
-
- fn get_clipboard_contents(
- &mut self,
- cx: &mut ViewContext<Self>,
- ) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
- let (snapshot, selection, creases) = self.editor.update(cx, |editor, cx| {
- let mut selection = editor.selections.newest::<Point>(cx);
- let snapshot = editor.buffer().read(cx).snapshot(cx);
-
- let is_entire_line = selection.is_empty() || editor.selections.line_mode;
- if is_entire_line {
- selection.start = Point::new(selection.start.row, 0);
- selection.end =
- cmp::min(snapshot.max_point(), Point::new(selection.start.row + 1, 0));
- selection.goal = SelectionGoal::None;
- }
-
- let selection_start = snapshot.point_to_offset(selection.start);
-
- (
- snapshot.clone(),
- selection.clone(),
- editor.display_map.update(cx, |display_map, cx| {
- display_map
- .snapshot(cx)
- .crease_snapshot
- .creases_in_range(
- MultiBufferRow(selection.start.row)
- ..MultiBufferRow(selection.end.row + 1),
- &snapshot,
- )
- .filter_map(|crease| {
- if let Crease::Inline {
- range, metadata, ..
- } = &crease
- {
- let metadata = metadata.as_ref()?;
- let start = range
- .start
- .to_offset(&snapshot)
- .saturating_sub(selection_start);
- let end = range
- .end
- .to_offset(&snapshot)
- .saturating_sub(selection_start);
-
- let range_relative_to_selection = start..end;
- if !range_relative_to_selection.is_empty() {
- return Some(SelectedCreaseMetadata {
- range_relative_to_selection,
- crease: metadata.clone(),
- });
- }
- }
- None
- })
- .collect::<Vec<_>>()
- }),
- )
- });
-
- let selection = selection.map(|point| snapshot.point_to_offset(point));
- let context = self.context.read(cx);
-
- let mut text = String::new();
- for message in context.messages(cx) {
- if message.offset_range.start >= selection.range().end {
- break;
- } else if message.offset_range.end >= selection.range().start {
- let range = cmp::max(message.offset_range.start, selection.range().start)
- ..cmp::min(message.offset_range.end, selection.range().end);
- if !range.is_empty() {
- for chunk in context.buffer().read(cx).text_for_range(range) {
- text.push_str(chunk);
- }
- if message.offset_range.end < selection.range().end {
- text.push('\n');
- }
- }
- }
- }
-
- (text, CopyMetadata { creases }, vec![selection])
- }
-
- fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
- cx.stop_propagation();
-
- let images = if let Some(item) = cx.read_from_clipboard() {
- item.into_entries()
- .filter_map(|entry| {
- if let ClipboardEntry::Image(image) = entry {
- Some(image)
- } else {
- None
- }
- })
- .collect()
- } else {
- Vec::new()
- };
-
- let metadata = if let Some(item) = cx.read_from_clipboard() {
- item.entries().first().and_then(|entry| {
- if let ClipboardEntry::String(text) = entry {
- text.metadata_json::<CopyMetadata>()
- } else {
- None
- }
- })
- } else {
- None
- };
-
- if images.is_empty() {
- self.editor.update(cx, |editor, cx| {
- let paste_position = editor.selections.newest::<usize>(cx).head();
- editor.paste(action, cx);
-
- if let Some(metadata) = metadata {
- let buffer = editor.buffer().read(cx).snapshot(cx);
-
- let mut buffer_rows_to_fold = BTreeSet::new();
- let weak_editor = cx.view().downgrade();
- editor.insert_creases(
- metadata.creases.into_iter().map(|metadata| {
- let start = buffer.anchor_after(
- paste_position + metadata.range_relative_to_selection.start,
- );
- let end = buffer.anchor_before(
- paste_position + metadata.range_relative_to_selection.end,
- );
-
- let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
- buffer_rows_to_fold.insert(buffer_row);
- Crease::inline(
- start..end,
- FoldPlaceholder {
- render: render_fold_icon_button(
- weak_editor.clone(),
- metadata.crease.icon,
- metadata.crease.label.clone(),
- ),
- ..Default::default()
- },
- render_slash_command_output_toggle,
- |_, _, _| Empty.into_any(),
- )
- .with_metadata(metadata.crease.clone())
- }),
- cx,
- );
- for buffer_row in buffer_rows_to_fold.into_iter().rev() {
- editor.fold_at(&FoldAt { buffer_row }, cx);
- }
- }
- });
- } else {
- let mut image_positions = Vec::new();
- self.editor.update(cx, |editor, cx| {
- editor.transact(cx, |editor, cx| {
- let edits = editor
- .selections
- .all::<usize>(cx)
- .into_iter()
- .map(|selection| (selection.start..selection.end, "\n"));
- editor.edit(edits, cx);
-
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- for selection in editor.selections.all::<usize>(cx) {
- image_positions.push(snapshot.anchor_before(selection.end));
- }
- });
- });
-
- self.context.update(cx, |context, cx| {
- for image in images {
- let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
- else {
- continue;
- };
- let image_id = image.id();
- let image_task = LanguageModelImage::from_image(image, cx).shared();
-
- for image_position in image_positions.iter() {
- context.insert_content(
- Content::Image {
- anchor: image_position.text_anchor,
- image_id,
- image: image_task.clone(),
- render_image: render_image.clone(),
- },
- cx,
- );
- }
- }
- });
- }
- }
-
- fn update_image_blocks(&mut self, cx: &mut ViewContext<Self>) {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let excerpt_id = *buffer.as_singleton().unwrap().0;
- let old_blocks = std::mem::take(&mut self.image_blocks);
- let new_blocks = self
- .context
- .read(cx)
- .contents(cx)
- .filter_map(|content| {
- if let Content::Image {
- anchor,
- render_image,
- ..
- } = content
- {
- Some((anchor, render_image))
- } else {
- None
- }
- })
- .filter_map(|(anchor, render_image)| {
- const MAX_HEIGHT_IN_LINES: u32 = 8;
- let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
- let image = render_image.clone();
- anchor.is_valid(&buffer).then(|| BlockProperties {
- placement: BlockPlacement::Above(anchor),
- height: MAX_HEIGHT_IN_LINES,
- style: BlockStyle::Sticky,
- render: Arc::new(move |cx| {
- let image_size = size_for_image(
- &image,
- size(
- cx.max_width - cx.gutter_dimensions.full_width(),
- MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
- ),
- );
- h_flex()
- .pl(cx.gutter_dimensions.full_width())
- .child(
- img(image.clone())
- .object_fit(gpui::ObjectFit::ScaleDown)
- .w(image_size.width)
- .h(image_size.height),
- )
- .into_any_element()
- }),
- priority: 0,
- })
- })
- .collect::<Vec<_>>();
-
- editor.remove_blocks(old_blocks, None, cx);
- let ids = editor.insert_blocks(new_blocks, None, cx);
- self.image_blocks = HashSet::from_iter(ids);
- });
- }
-
- fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
- self.context.update(cx, |context, cx| {
- let selections = self.editor.read(cx).selections.disjoint_anchors();
- for selection in selections.as_ref() {
- let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
- let range = selection
- .map(|endpoint| endpoint.to_offset(&buffer))
- .range();
- context.split_message(range, cx);
- }
- });
- }
-
- fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
- self.context.update(cx, |context, cx| {
- context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
- });
- }
-
- fn title(&self, cx: &AppContext) -> Cow<str> {
- self.context
- .read(cx)
- .summary()
- .map(|summary| summary.text.clone())
- .map(Cow::Owned)
- .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
- }
-
- fn render_patch_block(
- &mut self,
- range: Range<text::Anchor>,
- max_width: Pixels,
- gutter_width: Pixels,
- id: BlockId,
- selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement> {
- let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
- let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
- let excerpt_id = *excerpt_id;
- let anchor = snapshot
- .buffer_snapshot
- .anchor_in_excerpt(excerpt_id, range.start)
- .unwrap();
-
- let theme = cx.theme().clone();
- let patch = self.context.read(cx).patch_for_range(&range, cx)?;
- let paths = patch
- .paths()
- .map(|p| SharedString::from(p.to_string()))
- .collect::<BTreeSet<_>>();
-
- Some(
- v_flex()
- .id(id)
- .bg(theme.colors().editor_background)
- .ml(gutter_width)
- .pb_1()
- .w(max_width - gutter_width)
- .rounded_md()
- .border_1()
- .border_color(theme.colors().border_variant)
- .overflow_hidden()
- .hover(|style| style.border_color(theme.colors().text_accent))
- .when(selected, |this| {
- this.border_color(theme.colors().text_accent)
- })
- .cursor(CursorStyle::PointingHand)
- .on_click(cx.listener(move |this, _, cx| {
- this.editor.update(cx, |editor, cx| {
- editor.change_selections(None, cx, |selections| {
- selections.select_ranges(vec![anchor..anchor]);
- });
- });
- this.focus_active_patch(cx);
- }))
- .child(
- div()
- .px_2()
- .py_1()
- .overflow_hidden()
- .text_ellipsis()
- .border_b_1()
- .border_color(theme.colors().border_variant)
- .bg(theme.colors().element_background)
- .child(
- Label::new(patch.title.clone())
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .children(paths.into_iter().map(|path| {
- h_flex()
- .px_2()
- .pt_1()
- .gap_1p5()
- .child(Icon::new(IconName::File).size(IconSize::Small))
- .child(Label::new(path).size(LabelSize::Small))
- }))
- .when(patch.status == AssistantPatchStatus::Pending, |div| {
- div.child(
- h_flex()
- .pt_1()
- .px_2()
- .gap_1()
- .child(
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::XSmall)
- .color(Color::Muted)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(percentage(
- delta,
- )))
- },
- ),
- )
- .child(
- Label::new("Generating…")
- .color(Color::Muted)
- .size(LabelSize::Small)
- .with_animation(
- "pulsating-label",
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- |label, delta| label.alpha(delta),
- ),
- ),
- )
- })
- .into_any(),
- )
- }
-
- fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
- use feature_flags::FeatureFlagAppExt;
- let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
- assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
- });
-
- if nudge.map_or(false, |value| value) {
- Some(
- h_flex()
- .p_3()
- .border_b_1()
- .border_color(cx.theme().colors().border_variant)
- .bg(cx.theme().colors().editor_background)
- .justify_between()
- .child(
- h_flex()
- .gap_3()
- .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
- .child(Label::new("Zed AI is here! Get started by signing in →")),
- )
- .child(
- Button::new("sign-in", "Sign in")
- .size(ButtonSize::Compact)
- .style(ButtonStyle::Filled)
- .on_click(cx.listener(|this, _event, cx| {
- let client = this
- .workspace
- .update(cx, |workspace, _| workspace.client().clone())
- .log_err();
-
- if let Some(client) = client {
- cx.spawn(|this, mut cx| async move {
- client.authenticate_and_connect(true, &mut cx).await?;
- this.update(&mut cx, |_, cx| cx.notify())
- })
- .detach_and_log_err(cx)
- }
- })),
- )
- .into_any_element(),
- )
- } else if let Some(configuration_error) = configuration_error(cx) {
- let label = match configuration_error {
- ConfigurationError::NoProvider => "No LLM provider selected.",
- ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
- };
- Some(
- h_flex()
- .px_3()
- .py_2()
- .border_b_1()
- .border_color(cx.theme().colors().border_variant)
- .bg(cx.theme().colors().editor_background)
- .justify_between()
- .child(
- h_flex()
- .gap_3()
- .child(
- Icon::new(IconName::Warning)
- .size(IconSize::Small)
- .color(Color::Warning),
- )
- .child(Label::new(label)),
- )
- .child(
- Button::new("open-configuration", "Configure Providers")
- .size(ButtonSize::Compact)
- .icon(Some(IconName::SlidersVertical))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .style(ButtonStyle::Filled)
- .on_click({
- let focus_handle = self.focus_handle(cx).clone();
- move |_event, cx| {
- focus_handle.dispatch_action(&ShowConfiguration, cx);
- }
- }),
- )
- .into_any_element(),
- )
- } else {
- None
- }
- }
-
- fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let focus_handle = self.focus_handle(cx).clone();
-
- let (style, tooltip) = match token_state(&self.context, cx) {
- Some(TokenState::NoTokensLeft { .. }) => (
- ButtonStyle::Tinted(TintColor::Error),
- Some(Tooltip::text("Token limit reached", cx)),
- ),
- Some(TokenState::HasMoreTokens {
- over_warn_threshold,
- ..
- }) => {
- let (style, tooltip) = if over_warn_threshold {
- (
- ButtonStyle::Tinted(TintColor::Warning),
- Some(Tooltip::text("Token limit is close to exhaustion", cx)),
- )
- } else {
- (ButtonStyle::Filled, None)
- };
- (style, tooltip)
- }
- None => (ButtonStyle::Filled, None),
- };
-
- let provider = LanguageModelRegistry::read_global(cx).active_provider();
-
- let has_configuration_error = configuration_error(cx).is_some();
- let needs_to_accept_terms = self.show_accept_terms
- && provider
- .as_ref()
- .map_or(false, |provider| provider.must_accept_terms(cx));
- let disabled = has_configuration_error || needs_to_accept_terms;
-
- ButtonLike::new("send_button")
- .disabled(disabled)
- .style(style)
- .when_some(tooltip, |button, tooltip| {
- button.tooltip(move |_| tooltip.clone())
- })
- .layer(ElevationIndex::ModalSurface)
- .child(Label::new(
- if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
- "Chat"
- } else {
- "Send"
- },
- ))
- .children(
- KeyBinding::for_action_in(&Assist, &focus_handle, cx)
- .map(|binding| binding.into_any_element()),
- )
- .on_click(move |_event, cx| {
- focus_handle.dispatch_action(&Assist, cx);
- })
- }
-
- fn render_edit_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let focus_handle = self.focus_handle(cx).clone();
-
- let (style, tooltip) = match token_state(&self.context, cx) {
- Some(TokenState::NoTokensLeft { .. }) => (
- ButtonStyle::Tinted(TintColor::Error),
- Some(Tooltip::text("Token limit reached", cx)),
- ),
- Some(TokenState::HasMoreTokens {
- over_warn_threshold,
- ..
- }) => {
- let (style, tooltip) = if over_warn_threshold {
- (
- ButtonStyle::Tinted(TintColor::Warning),
- Some(Tooltip::text("Token limit is close to exhaustion", cx)),
- )
- } else {
- (ButtonStyle::Filled, None)
- };
- (style, tooltip)
- }
- None => (ButtonStyle::Filled, None),
- };
-
- let provider = LanguageModelRegistry::read_global(cx).active_provider();
-
- let has_configuration_error = configuration_error(cx).is_some();
- let needs_to_accept_terms = self.show_accept_terms
- && provider
- .as_ref()
- .map_or(false, |provider| provider.must_accept_terms(cx));
- let disabled = has_configuration_error || needs_to_accept_terms;
-
- ButtonLike::new("edit_button")
- .disabled(disabled)
- .style(style)
- .when_some(tooltip, |button, tooltip| {
- button.tooltip(move |_| tooltip.clone())
- })
- .layer(ElevationIndex::ModalSurface)
- .child(Label::new("Suggest Edits"))
- .children(
- KeyBinding::for_action_in(&Edit, &focus_handle, cx)
- .map(|binding| binding.into_any_element()),
- )
- .on_click(move |_event, cx| {
- focus_handle.dispatch_action(&Edit, cx);
- })
- }
-
- fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- slash_command_picker::SlashCommandSelector::new(
- self.slash_commands.clone(),
- cx.view().downgrade(),
- Button::new("trigger", "Add Context")
- .icon(IconName::Plus)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
- )
- }
-
- fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
- let last_error = self.last_error.as_ref()?;
-
- Some(
- div()
- .absolute()
- .right_3()
- .bottom_12()
- .max_w_96()
- .py_2()
- .px_3()
- .elevation_2(cx)
- .occlude()
- .child(match last_error {
- AssistError::FileRequired => self.render_file_required_error(cx),
- AssistError::PaymentRequired => self.render_payment_required_error(cx),
- AssistError::MaxMonthlySpendReached => {
- self.render_max_monthly_spend_reached_error(cx)
- }
- AssistError::Message(error_message) => {
- self.render_assist_error(error_message, cx)
- }
- })
- .into_any(),
- )
- }
-
- fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::Warning).color(Color::Warning))
- .child(
- Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
- ),
- )
- .child(
- div()
- .id("error-message")
- .max_h_24()
- .overflow_y_scroll()
- .child(Label::new(
- "To include files, type /file or /tab in your prompt.",
- )),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- |this, _, cx| {
- this.last_error = None;
- cx.notify();
- },
- ))),
- )
- .into_any()
- }
-
- fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
- const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
-
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_24()
- .overflow_y_scroll()
- .child(Label::new(ERROR_MESSAGE)),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
- |this, _, cx| {
- this.last_error = None;
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- },
- )))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- |this, _, cx| {
- this.last_error = None;
- cx.notify();
- },
- ))),
- )
- .into_any()
- }
-
- fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
- const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
-
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_24()
- .overflow_y_scroll()
- .child(Label::new(ERROR_MESSAGE)),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .child(
- Button::new("subscribe", "Update Monthly Spend Limit").on_click(
- cx.listener(|this, _, cx| {
- this.last_error = None;
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- }),
- ),
- )
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- |this, _, cx| {
- this.last_error = None;
- cx.notify();
- },
- ))),
- )
- .into_any()
- }
-
- fn render_assist_error(
- &self,
- error_message: &SharedString,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement {
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(
- Label::new("Error interacting with language model")
- .weight(FontWeight::MEDIUM),
- ),
- )
- .child(
- div()
- .id("error-message")
- .max_h_32()
- .overflow_y_scroll()
- .child(Label::new(error_message.clone())),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- |this, _, cx| {
- this.last_error = None;
- cx.notify();
- },
- ))),
- )
- .into_any()
- }
-}
-
-/// Returns the contents of the *outermost* fenced code block that contains the given offset.
-fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
- const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
- const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
-
- let layer = snapshot.syntax_layers().next()?;
-
- let root_node = layer.node();
- let mut cursor = root_node.walk();
-
- // Go to the first child for the given offset
- while cursor.goto_first_child_for_byte(offset).is_some() {
- // If we're at the end of the node, go to the next one.
- // Example: if you have a fenced-code-block, and you're on the start of the line
- // right after the closing ```, you want to skip the fenced-code-block and
- // go to the next sibling.
- if cursor.node().end_byte() == offset {
- cursor.goto_next_sibling();
- }
-
- if cursor.node().start_byte() > offset {
- break;
- }
-
- // We found the fenced code block.
- if cursor.node().kind() == CODE_BLOCK_NODE {
- // Now we need to find the child node that contains the code.
- cursor.goto_first_child();
- loop {
- if cursor.node().kind() == CODE_BLOCK_CONTENT {
- return Some(cursor.node().byte_range());
- }
- if !cursor.goto_next_sibling() {
- break;
- }
- }
- }
- }
-
- None
-}
-
-fn render_fold_icon_button(
- editor: WeakView<Editor>,
- icon: IconName,
- label: SharedString,
-) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement> {
- Arc::new(move |fold_id, fold_range, _cx| {
- let editor = editor.clone();
- ButtonLike::new(fold_id)
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ElevatedSurface)
- .child(Icon::new(icon))
- .child(Label::new(label.clone()).single_line())
- .on_click(move |_, cx| {
- editor
- .update(cx, |editor, cx| {
- let buffer_start = fold_range
- .start
- .to_point(&editor.buffer().read(cx).read(cx));
- let buffer_row = MultiBufferRow(buffer_start.row);
- editor.unfold_at(&UnfoldAt { buffer_row }, cx);
- })
- .ok();
- })
- .into_any_element()
- })
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct CopyMetadata {
- creases: Vec<SelectedCreaseMetadata>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct SelectedCreaseMetadata {
- range_relative_to_selection: Range<usize>,
- crease: CreaseMetadata,
-}
-
-impl EventEmitter<EditorEvent> for ContextEditor {}
-impl EventEmitter<SearchEvent> for ContextEditor {}
-
-impl Render for ContextEditor {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let provider = LanguageModelRegistry::read_global(cx).active_provider();
- let accept_terms = if self.show_accept_terms {
- provider
- .as_ref()
- .and_then(|provider| provider.render_accept_terms(cx))
- } else {
- None
- };
-
- v_flex()
- .key_context("ContextEditor")
- .capture_action(cx.listener(ContextEditor::cancel))
- .capture_action(cx.listener(ContextEditor::save))
- .capture_action(cx.listener(ContextEditor::copy))
- .capture_action(cx.listener(ContextEditor::cut))
- .capture_action(cx.listener(ContextEditor::paste))
- .capture_action(cx.listener(ContextEditor::cycle_message_role))
- .capture_action(cx.listener(ContextEditor::confirm_command))
- .on_action(cx.listener(ContextEditor::edit))
- .on_action(cx.listener(ContextEditor::assist))
- .on_action(cx.listener(ContextEditor::split))
- .size_full()
- .children(self.render_notice(cx))
- .child(
- div()
- .flex_grow()
- .bg(cx.theme().colors().editor_background)
- .child(self.editor.clone()),
- )
- .when_some(accept_terms, |this, element| {
- this.child(
- div()
- .absolute()
- .right_3()
- .bottom_12()
- .max_w_96()
- .py_2()
- .px_3()
- .elevation_2(cx)
- .bg(cx.theme().colors().surface_background)
- .occlude()
- .child(element),
- )
- })
- .children(self.render_last_error(cx))
- .child(
- h_flex().w_full().relative().child(
- h_flex()
- .p_2()
- .w_full()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .bg(cx.theme().colors().editor_background)
- .child(h_flex().gap_1().child(self.render_inject_context_menu(cx)))
- .child(
- h_flex()
- .w_full()
- .justify_end()
- .when(
- AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
- |buttons| {
- buttons
- .items_center()
- .gap_1p5()
- .child(self.render_edit_button(cx))
- .child(
- Label::new("or")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- },
- )
- .child(self.render_send_button(cx)),
- ),
- ),
- )
- }
-}
-
-impl FocusableView for ContextEditor {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.editor.focus_handle(cx)
- }
-}
-
-impl Item for ContextEditor {
- type Event = editor::EditorEvent;
-
- fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
- Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
- }
-
- fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
- match event {
- EditorEvent::Edited { .. } => {
- f(item::ItemEvent::Edit);
- }
- EditorEvent::TitleChanged => {
- f(item::ItemEvent::UpdateTab);
- }
- _ => {}
- }
- }
-
- fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
- Some(self.title(cx).to_string().into())
- }
-
- fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(handle.clone()))
- }
-
- fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
- self.editor.update(cx, |editor, cx| {
- Item::set_nav_history(editor, nav_history, cx)
- })
- }
-
- fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
- self.editor
- .update(cx, |editor, cx| Item::navigate(editor, data, cx))
- }
-
- fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
- self.editor.update(cx, Item::deactivated)
- }
-
- fn act_as_type<'a>(
- &'a self,
- type_id: TypeId,
- self_handle: &'a View<Self>,
- _: &'a AppContext,
- ) -> Option<AnyView> {
- if type_id == TypeId::of::<Self>() {
- Some(self_handle.to_any())
- } else if type_id == TypeId::of::<Editor>() {
- Some(self.editor.to_any())
- } else {
- None
- }
- }
-
- fn include_in_nav_history() -> bool {
- false
- }
-}
-
-impl SearchableItem for ContextEditor {
- type Match = <Editor as SearchableItem>::Match;
-
- fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
- self.editor.update(cx, |editor, cx| {
- editor.clear_matches(cx);
- });
- }
-
- fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| editor.update_matches(matches, cx));
- }
-
- fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
- self.editor
- .update(cx, |editor, cx| editor.query_suggestion(cx))
- }
-
- fn activate_match(
- &mut self,
- index: usize,
- matches: &[Self::Match],
- cx: &mut ViewContext<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- editor.activate_match(index, matches, cx);
- });
- }
-
- fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| editor.select_matches(matches, cx));
- }
-
- fn replace(
- &mut self,
- identifier: &Self::Match,
- query: &project::search::SearchQuery,
- cx: &mut ViewContext<Self>,
- ) {
- self.editor
- .update(cx, |editor, cx| editor.replace(identifier, query, cx));
- }
-
- fn find_matches(
- &mut self,
- query: Arc<project::search::SearchQuery>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Self::Match>> {
- self.editor
- .update(cx, |editor, cx| editor.find_matches(query, cx))
- }
-
- fn active_match_index(
- &mut self,
- matches: &[Self::Match],
- cx: &mut ViewContext<Self>,
- ) -> Option<usize> {
- self.editor
- .update(cx, |editor, cx| editor.active_match_index(matches, cx))
- }
-}
-
-impl FollowableItem for ContextEditor {
- fn remote_id(&self) -> Option<workspace::ViewId> {
- self.remote_id
- }
-
- fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
- let context = self.context.read(cx);
- Some(proto::view::Variant::ContextEditor(
- proto::view::ContextEditor {
- context_id: context.id().to_proto(),
- editor: if let Some(proto::view::Variant::Editor(proto)) =
- self.editor.read(cx).to_state_proto(cx)
- {
- Some(proto)
- } else {
- None
- },
- },
- ))
- }
-
- fn from_state_proto(
- workspace: View<Workspace>,
- id: workspace::ViewId,
- state: &mut Option<proto::view::Variant>,
- cx: &mut WindowContext,
- ) -> Option<Task<Result<View<Self>>>> {
- let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
- return None;
- };
- let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
- unreachable!()
- };
-
- let context_id = ContextId::from_proto(state.context_id);
- let editor_state = state.editor?;
-
- let (project, panel) = workspace.update(cx, |workspace, cx| {
- Some((
- workspace.project().clone(),
- workspace.panel::<AssistantPanel>(cx)?,
- ))
- })?;
-
- let context_editor =
- panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
-
- Some(cx.spawn(|mut cx| async move {
- let context_editor = context_editor.await?;
- context_editor
- .update(&mut cx, |context_editor, cx| {
- context_editor.remote_id = Some(id);
- context_editor.editor.update(cx, |editor, cx| {
- editor.apply_update_proto(
- &project,
- proto::update_view::Variant::Editor(proto::update_view::Editor {
- selections: editor_state.selections,
- pending_selection: editor_state.pending_selection,
- scroll_top_anchor: editor_state.scroll_top_anchor,
- scroll_x: editor_state.scroll_y,
- scroll_y: editor_state.scroll_y,
- ..Default::default()
- }),
- cx,
- )
- })
- })?
- .await?;
- Ok(context_editor)
- }))
- }
-
- fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
- Editor::to_follow_event(event)
- }
-
- fn add_event_to_update_proto(
- &self,
- event: &Self::Event,
- update: &mut Option<proto::update_view::Variant>,
- cx: &WindowContext,
- ) -> bool {
- self.editor
- .read(cx)
- .add_event_to_update_proto(event, update, cx)
- }
-
- fn apply_update_proto(
- &mut self,
- project: &Model<Project>,
- message: proto::update_view::Variant,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- self.editor.update(cx, |editor, cx| {
- editor.apply_update_proto(project, message, cx)
- })
- }
-
- fn is_project_item(&self, _cx: &WindowContext) -> bool {
- true
- }
-
- fn set_leader_peer_id(
- &mut self,
- leader_peer_id: Option<proto::PeerId>,
- cx: &mut ViewContext<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- editor.set_leader_peer_id(leader_peer_id, cx)
- })
- }
-
- fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
- if existing.context.read(cx).id() == self.context.read(cx).id() {
- Some(item::Dedup::KeepExisting)
- } else {
- None
- }
- }
-}
-
-pub struct ContextEditorToolbarItem {
- active_context_editor: Option<WeakView<ContextEditor>>,
- model_summary_editor: View<Editor>,
- language_model_selector: View<LanguageModelSelector>,
- language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
-}
-
-impl ContextEditorToolbarItem {
- pub fn new(
- workspace: &Workspace,
- model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
- model_summary_editor: View<Editor>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- Self {
- active_context_editor: None,
- model_summary_editor,
- language_model_selector: cx.new_view(|cx| {
- let fs = workspace.app_state().fs.clone();
- LanguageModelSelector::new(
- move |model, cx| {
- update_settings_file::<AssistantSettings>(
- fs.clone(),
- cx,
- move |settings, _| settings.set_model(model.clone()),
- );
- },
- cx,
- )
- }),
- language_model_selector_menu_handle: model_selector_menu_handle,
- }
- }
-
- fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
- let context = &self
- .active_context_editor
- .as_ref()?
- .upgrade()?
- .read(cx)
- .context;
- let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
- TokenState::NoTokensLeft {
- max_token_count,
- token_count,
- } => (Color::Error, token_count, max_token_count),
- TokenState::HasMoreTokens {
- max_token_count,
- token_count,
- over_warn_threshold,
- } => {
- let color = if over_warn_threshold {
- Color::Warning
- } else {
- Color::Muted
- };
- (color, token_count, max_token_count)
- }
- };
- Some(
- h_flex()
- .gap_0p5()
- .child(
- Label::new(humanize_token_count(token_count))
- .size(LabelSize::Small)
- .color(token_count_color),
- )
- .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
- .child(
- Label::new(humanize_token_count(max_token_count))
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- }
-}
-
-impl Render for ContextEditorToolbarItem {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let left_side = h_flex()
- .group("chat-title-group")
- .gap_1()
- .items_center()
- .flex_grow()
- .child(
- div()
- .w_full()
- .when(self.active_context_editor.is_some(), |left_side| {
- left_side.child(self.model_summary_editor.clone())
- }),
- )
- .child(
- div().visible_on_hover("chat-title-group").child(
- IconButton::new("regenerate-context", IconName::RefreshTitle)
- .shape(ui::IconButtonShape::Square)
- .tooltip(|cx| Tooltip::text("Regenerate Title", cx))
- .on_click(cx.listener(move |_, _, cx| {
- cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
- })),
- ),
- );
- let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
- let active_model = LanguageModelRegistry::read_global(cx).active_model();
- let right_side = h_flex()
- .gap_2()
- // TODO display this in a nicer way, once we have a design for it.
- // .children({
- // let project = self
- // .workspace
- // .upgrade()
- // .map(|workspace| workspace.read(cx).project().downgrade());
- //
- // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
- // project.and_then(|project| db.remaining_summaries(&project, cx))
- // });
- // scan_items_remaining
- // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
- // })
- .child(
- LanguageModelSelectorPopoverMenu::new(
- self.language_model_selector.clone(),
- ButtonLike::new("active-model")
- .style(ButtonStyle::Subtle)
- .child(
- h_flex()
- .w_full()
- .gap_0p5()
- .child(
- div()
- .overflow_x_hidden()
- .flex_grow()
- .whitespace_nowrap()
- .child(match (active_provider, active_model) {
- (Some(provider), Some(model)) => h_flex()
- .gap_1()
- .child(
- Icon::new(
- model
- .icon()
- .unwrap_or_else(|| provider.icon()),
- )
- .color(Color::Muted)
- .size(IconSize::XSmall),
- )
- .child(
- Label::new(model.name().0)
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .into_any_element(),
- _ => Label::new("No model selected")
- .size(LabelSize::Small)
- .color(Color::Muted)
- .into_any_element(),
- }),
- )
- .child(
- Icon::new(IconName::ChevronDown)
- .color(Color::Muted)
- .size(IconSize::XSmall),
- ),
- )
- .tooltip(move |cx| {
- Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
- }),
- )
- .with_handle(self.language_model_selector_menu_handle.clone()),
- )
- .children(self.render_remaining_tokens(cx));
-
- h_flex()
- .px_0p5()
- .size_full()
- .gap_2()
- .justify_between()
- .child(left_side)
- .child(right_side)
- }
-}
-
-impl ToolbarItemView for ContextEditorToolbarItem {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> ToolbarItemLocation {
- self.active_context_editor = active_pane_item
- .and_then(|item| item.act_as::<ContextEditor>(cx))
- .map(|editor| editor.downgrade());
- cx.notify();
- if self.active_context_editor.is_none() {
- ToolbarItemLocation::Hidden
- } else {
- ToolbarItemLocation::PrimaryRight
- }
- }
-
- fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
- cx.notify();
- }
-}
-
-impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
-
-enum ContextEditorToolbarItemEvent {
- RegenerateSummary,
-}
-impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
-
-pub struct ContextHistory {
- picker: View<Picker<SavedContextPickerDelegate>>,
- _subscriptions: Vec<Subscription>,
- assistant_panel: WeakView<AssistantPanel>,
-}
-
-impl ContextHistory {
- fn new(
- project: Model<Project>,
- context_store: Model<ContextStore>,
- assistant_panel: WeakView<AssistantPanel>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let picker = cx.new_view(|cx| {
- Picker::uniform_list(
- SavedContextPickerDelegate::new(project, context_store.clone()),
- cx,
- )
- .modal(false)
- .max_height(None)
- });
-
- let _subscriptions = vec![
- cx.observe(&context_store, |this, _, cx| {
- this.picker.update(cx, |picker, cx| picker.refresh(cx));
- }),
- cx.subscribe(&picker, Self::handle_picker_event),
- ];
-
- Self {
- picker,
- _subscriptions,
- assistant_panel,
- }
- }
-
- fn handle_picker_event(
- &mut self,
- _: View<Picker<SavedContextPickerDelegate>>,
- event: &SavedContextPickerEvent,
- cx: &mut ViewContext<Self>,
- ) {
- let SavedContextPickerEvent::Confirmed(context) = event;
- self.assistant_panel
- .update(cx, |assistant_panel, cx| match context {
- ContextMetadata::Remote(metadata) => {
- assistant_panel
- .open_remote_context(metadata.id.clone(), cx)
- .detach_and_log_err(cx);
- }
- ContextMetadata::Saved(metadata) => {
- assistant_panel
- .open_saved_context(metadata.path.clone(), cx)
- .detach_and_log_err(cx);
- }
- })
- .ok();
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum WorkflowAssistStatus {
- Pending,
- Confirmed,
- Done,
- Idle,
-}
-
-impl Render for ContextHistory {
- fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
- div().size_full().child(self.picker.clone())
- }
-}
-
-impl FocusableView for ContextHistory {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl EventEmitter<()> for ContextHistory {}
-
-impl Item for ContextHistory {
- type Event = ();
-
- fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
- Some("History".into())
- }
-}
-
-pub struct ConfigurationView {
- focus_handle: FocusHandle,
- configuration_views: HashMap<LanguageModelProviderId, AnyView>,
- _registry_subscription: Subscription,
-}
-
-impl ConfigurationView {
- fn new(cx: &mut ViewContext<Self>) -> Self {
- let focus_handle = cx.focus_handle();
-
- let registry_subscription = cx.subscribe(
- &LanguageModelRegistry::global(cx),
- |this, _, event: &language_model::Event, cx| match event {
- language_model::Event::AddedProvider(provider_id) => {
- let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
- if let Some(provider) = provider {
- this.add_configuration_view(&provider, cx);
- }
- }
- language_model::Event::RemovedProvider(provider_id) => {
- this.remove_configuration_view(provider_id);
- }
- _ => {}
- },
- );
-
- let mut this = Self {
- focus_handle,
- configuration_views: HashMap::default(),
- _registry_subscription: registry_subscription,
- };
- this.build_configuration_views(cx);
- this
- }
-
- fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
- let providers = LanguageModelRegistry::read_global(cx).providers();
- for provider in providers {
- self.add_configuration_view(&provider, cx);
- }
- }
-
- fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
- self.configuration_views.remove(provider_id);
- }
-
- fn add_configuration_view(
- &mut self,
- provider: &Arc<dyn LanguageModelProvider>,
- cx: &mut ViewContext<Self>,
- ) {
- let configuration_view = provider.configuration_view(cx);
- self.configuration_views
- .insert(provider.id(), configuration_view);
- }
-
- fn render_provider_view(
- &mut self,
- provider: &Arc<dyn LanguageModelProvider>,
- cx: &mut ViewContext<Self>,
- ) -> Div {
- let provider_id = provider.id().0.clone();
- let provider_name = provider.name().0.clone();
- let configuration_view = self.configuration_views.get(&provider.id()).cloned();
-
- let open_new_context = cx.listener({
- let provider = provider.clone();
- move |_, _, cx| {
- cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
- provider.clone(),
- ))
- }
- });
-
- v_flex()
- .gap_2()
- .child(
- h_flex()
- .justify_between()
- .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
- .when(provider.is_authenticated(cx), move |this| {
- this.child(
- h_flex().justify_end().child(
- Button::new(
- SharedString::from(format!("new-context-{provider_id}")),
- "Open New Chat",
- )
- .icon_position(IconPosition::Start)
- .icon(IconName::Plus)
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ModalSurface)
- .on_click(open_new_context),
- ),
- )
- }),
- )
- .child(
- div()
- .p(DynamicSpacing::Base08.rems(cx))
- .bg(cx.theme().colors().surface_background)
- .border_1()
- .border_color(cx.theme().colors().border_variant)
- .rounded_md()
- .when(configuration_view.is_none(), |this| {
- this.child(div().child(Label::new(format!(
- "No configuration view for {}",
- provider_name
- ))))
- })
- .when_some(configuration_view, |this, configuration_view| {
- this.child(configuration_view)
- }),
- )
- }
-}
-
-impl Render for ConfigurationView {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let providers = LanguageModelRegistry::read_global(cx).providers();
- let provider_views = providers
- .into_iter()
- .map(|provider| self.render_provider_view(&provider, cx))
- .collect::<Vec<_>>();
-
- let mut element = v_flex()
- .id("assistant-configuration-view")
- .track_focus(&self.focus_handle(cx))
- .bg(cx.theme().colors().editor_background)
- .size_full()
- .overflow_y_scroll()
- .child(
- v_flex()
- .p(DynamicSpacing::Base16.rems(cx))
- .border_b_1()
- .border_color(cx.theme().colors().border)
- .gap_1()
- .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
- .child(
- Label::new(
- "At least one LLM provider must be configured to use the Assistant.",
- )
- .color(Color::Muted),
- ),
- )
- .child(
- v_flex()
- .p(DynamicSpacing::Base16.rems(cx))
- .mt_1()
- .gap_6()
- .flex_1()
- .children(provider_views),
- )
- .into_any();
-
- // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
- // because we couldn't the element to take up the size of the parent.
- canvas(
- move |bounds, cx| {
- element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
- element
- },
- |_, mut element, cx| {
- element.paint(cx);
- },
- )
- .flex_1()
- .w_full()
- }
-}
-
-pub enum ConfigurationViewEvent {
- NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
-}
-
-impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
+impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
impl FocusableView for ConfigurationView {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
@@ -0,0 +1,3547 @@
+use anyhow::Result;
+use assistant_settings::AssistantSettings;
+use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
+use assistant_slash_commands::{
+ selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs,
+ FileSlashCommand,
+};
+use assistant_tool::ToolWorkingSet;
+use client::{proto, zed_urls};
+use collections::{hash_map, BTreeSet, HashMap, HashSet};
+use editor::{
+ actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
+ display_map::{
+ BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
+ CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
+ },
+ scroll::{Autoscroll, AutoscrollStrategy},
+ Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
+ ToOffset as _, ToPoint,
+};
+use editor::{display_map::CreaseId, FoldPlaceholder};
+use fs::Fs;
+use futures::FutureExt;
+use gpui::{
+ div, img, percentage, point, prelude::*, pulsating_between, size, Animation, AnimationExt,
+ AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
+ CursorStyle, Empty, Entity, EventEmitter, FocusHandle, FocusableView, FontWeight,
+ InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage,
+ SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
+ View, WeakModel, WeakView,
+};
+use indexed_docs::IndexedDocsStore;
+use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset};
+use language_model::{LanguageModelImage, LanguageModelRegistry, LanguageModelToolUse, Role};
+use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
+use multi_buffer::MultiBufferRow;
+use picker::Picker;
+use project::{Project, Worktree};
+use rope::Point;
+use serde::{Deserialize, Serialize};
+use settings::{update_settings_file, Settings};
+use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
+use text::SelectionGoal;
+use ui::{
+ prelude::*, ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor,
+ Tooltip,
+};
+use util::{maybe, ResultExt};
+use workspace::searchable::SearchableItemHandle;
+use workspace::{
+ item::{self, FollowableItem, Item, ItemHandle},
+ notifications::NotificationId,
+ pane::{self, SaveIntent},
+ searchable::{SearchEvent, SearchableItem},
+ Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
+ Workspace,
+};
+
+use crate::{
+ humanize_token_count, slash_command::SlashCommandCompletionProvider, slash_command_picker,
+ Assist, AssistantPanel, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand,
+ Content, Context, ContextEvent, ContextId, CopyCode, CycleMessageRole, Edit,
+ InsertDraggedFiles, InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus,
+ Message, MessageId, MessageMetadata, MessageStatus, ParsedSlashCommand,
+ PendingSlashCommandStatus, QuoteSelection, RequestType, Split, ToggleModelSelector,
+};
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+struct ScrollPosition {
+ offset_before_cursor: gpui::Point<f32>,
+ cursor: Anchor,
+}
+
+struct PatchViewState {
+ crease_id: CreaseId,
+ editor: Option<PatchEditorState>,
+ update_task: Option<Task<()>>,
+}
+
+struct PatchEditorState {
+ editor: WeakView<ProposedChangesEditor>,
+ opened_patch: AssistantPatch,
+}
+
+type MessageHeader = MessageMetadata;
+
+#[derive(Clone)]
+enum AssistError {
+ FileRequired,
+ PaymentRequired,
+ MaxMonthlySpendReached,
+ Message(SharedString),
+}
+
+pub struct ContextEditor {
+ pub(crate) context: Model<Context>,
+ fs: Arc<dyn Fs>,
+ slash_commands: Arc<SlashCommandWorkingSet>,
+ tools: Arc<ToolWorkingSet>,
+ workspace: WeakView<Workspace>,
+ project: Model<Project>,
+ lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
+ pub(crate) editor: View<Editor>,
+ blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
+ image_blocks: HashSet<CustomBlockId>,
+ scroll_position: Option<ScrollPosition>,
+ remote_id: Option<workspace::ViewId>,
+ pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
+ invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
+ pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
+ _subscriptions: Vec<Subscription>,
+ patches: HashMap<Range<language::Anchor>, PatchViewState>,
+ active_patch: Option<Range<language::Anchor>>,
+ assistant_panel: WeakView<AssistantPanel>,
+ last_error: Option<AssistError>,
+ show_accept_terms: bool,
+ pub(crate) slash_menu_handle:
+ PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
+ // dragged_file_worktrees is used to keep references to worktrees that were added
+ // when the user drag/dropped an external file onto the context editor. Since
+ // the worktree is not part of the project panel, it would be dropped as soon as
+ // the file is opened. In order to keep the worktree alive for the duration of the
+ // context editor, we keep a reference here.
+ dragged_file_worktrees: Vec<Model<Worktree>>,
+}
+
+pub const DEFAULT_TAB_TITLE: &str = "New Chat";
+const MAX_TAB_TITLE_LEN: usize = 16;
+
+impl ContextEditor {
+ pub(crate) fn for_context(
+ context: Model<Context>,
+ fs: Arc<dyn Fs>,
+ workspace: WeakView<Workspace>,
+ project: Model<Project>,
+ lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
+ assistant_panel: WeakView<AssistantPanel>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let completion_provider = SlashCommandCompletionProvider::new(
+ context.read(cx).slash_commands.clone(),
+ Some(cx.view().downgrade()),
+ Some(workspace.clone()),
+ );
+
+ let editor = cx.new_view(|cx| {
+ let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
+ editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+ editor.set_show_line_numbers(false, cx);
+ editor.set_show_scrollbars(false, cx);
+ editor.set_show_git_diff_gutter(false, cx);
+ editor.set_show_code_actions(false, cx);
+ editor.set_show_runnables(false, cx);
+ editor.set_show_wrap_guides(false, cx);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_completion_provider(Some(Box::new(completion_provider)));
+ editor.set_collaboration_hub(Box::new(project.clone()));
+ editor
+ });
+
+ let _subscriptions = vec![
+ cx.observe(&context, |_, _, cx| cx.notify()),
+ cx.subscribe(&context, Self::handle_context_event),
+ cx.subscribe(&editor, Self::handle_editor_event),
+ cx.subscribe(&editor, Self::handle_editor_search_event),
+ ];
+
+ let sections = context.read(cx).slash_command_output_sections().to_vec();
+ let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
+ let slash_commands = context.read(cx).slash_commands.clone();
+ let tools = context.read(cx).tools.clone();
+ let mut this = Self {
+ context,
+ slash_commands,
+ tools,
+ editor,
+ lsp_adapter_delegate,
+ blocks: Default::default(),
+ image_blocks: Default::default(),
+ scroll_position: None,
+ remote_id: None,
+ fs,
+ workspace,
+ project,
+ pending_slash_command_creases: HashMap::default(),
+ invoked_slash_command_creases: HashMap::default(),
+ pending_tool_use_creases: HashMap::default(),
+ _subscriptions,
+ patches: HashMap::default(),
+ active_patch: None,
+ assistant_panel,
+ last_error: None,
+ show_accept_terms: false,
+ slash_menu_handle: Default::default(),
+ dragged_file_worktrees: Vec::new(),
+ };
+ this.update_message_headers(cx);
+ this.update_image_blocks(cx);
+ this.insert_slash_command_output_sections(sections, false, cx);
+ this.patches_updated(&Vec::new(), &patch_ranges, cx);
+ this
+ }
+
+ pub fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
+ let command_name = DefaultSlashCommand.name();
+ self.editor.update(cx, |editor, cx| {
+ editor.insert(&format!("/{command_name}\n\n"), cx)
+ });
+ let command = self.context.update(cx, |context, cx| {
+ context.reparse(cx);
+ context.parsed_slash_commands()[0].clone()
+ });
+ self.run_command(
+ command.source_range,
+ &command.name,
+ &command.arguments,
+ false,
+ self.workspace.clone(),
+ cx,
+ );
+ }
+
+ fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
+ self.send_to_model(RequestType::Chat, cx);
+ }
+
+ fn edit(&mut self, _: &Edit, cx: &mut ViewContext<Self>) {
+ self.send_to_model(RequestType::SuggestEdits, cx);
+ }
+
+ fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if let Some((_range, patch)) = self.active_patch() {
+ if let Some(editor) = patch
+ .editor
+ .as_ref()
+ .and_then(|state| state.editor.upgrade())
+ {
+ cx.focus_view(&editor);
+ return true;
+ }
+ }
+
+ false
+ }
+
+ fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext<Self>) {
+ let provider = LanguageModelRegistry::read_global(cx).active_provider();
+ if provider
+ .as_ref()
+ .map_or(false, |provider| provider.must_accept_terms(cx))
+ {
+ self.show_accept_terms = true;
+ cx.notify();
+ return;
+ }
+
+ if self.focus_active_patch(cx) {
+ return;
+ }
+
+ self.last_error = None;
+
+ if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
+ self.last_error = Some(AssistError::FileRequired);
+ cx.notify();
+ } else if let Some(user_message) = self
+ .context
+ .update(cx, |context, cx| context.assist(request_type, cx))
+ {
+ let new_selection = {
+ let cursor = user_message
+ .start
+ .to_offset(self.context.read(cx).buffer().read(cx));
+ cursor..cursor
+ };
+ self.editor.update(cx, |editor, cx| {
+ editor.change_selections(
+ Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
+ cx,
+ |selections| selections.select_ranges([new_selection]),
+ );
+ });
+ // Avoid scrolling to the new cursor position so the assistant's output is stable.
+ cx.defer(|this, _| this.scroll_position = None);
+ }
+
+ cx.notify();
+ }
+
+ fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
+ self.last_error = None;
+
+ if self
+ .context
+ .update(cx, |context, cx| context.cancel_last_assist(cx))
+ {
+ return;
+ }
+
+ cx.propagate();
+ }
+
+ fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
+ let cursors = self.cursors(cx);
+ self.context.update(cx, |context, cx| {
+ let messages = context
+ .messages_for_offsets(cursors, cx)
+ .into_iter()
+ .map(|message| message.id)
+ .collect();
+ context.cycle_message_roles(messages, cx)
+ });
+ }
+
+ fn cursors(&self, cx: &mut WindowContext) -> Vec<usize> {
+ let selections = self
+ .editor
+ .update(cx, |editor, cx| editor.selections.all::<usize>(cx));
+ selections
+ .into_iter()
+ .map(|selection| selection.head())
+ .collect()
+ }
+
+ pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
+ if let Some(command) = self.slash_commands.command(name, cx) {
+ self.editor.update(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let newest_cursor = editor.selections.newest::<Point>(cx).head();
+ if newest_cursor.column > 0
+ || snapshot
+ .chars_at(newest_cursor)
+ .next()
+ .map_or(false, |ch| ch != '\n')
+ {
+ editor.move_to_end_of_line(
+ &MoveToEndOfLine {
+ stop_at_soft_wraps: false,
+ },
+ cx,
+ );
+ editor.newline(&Newline, cx);
+ }
+
+ editor.insert(&format!("/{name}"), cx);
+ if command.accepts_arguments() {
+ editor.insert(" ", cx);
+ editor.show_completions(&ShowCompletions::default(), cx);
+ }
+ });
+ });
+ if !command.requires_argument() {
+ self.confirm_command(&ConfirmCommand, cx);
+ }
+ }
+ }
+
+ pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
+ if self.editor.read(cx).has_active_completions_menu() {
+ return;
+ }
+
+ let selections = self.editor.read(cx).selections.disjoint_anchors();
+ let mut commands_by_range = HashMap::default();
+ let workspace = self.workspace.clone();
+ self.context.update(cx, |context, cx| {
+ context.reparse(cx);
+ for selection in selections.iter() {
+ if let Some(command) =
+ context.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.arguments,
+ true,
+ workspace.clone(),
+ cx,
+ );
+ }
+ cx.stop_propagation();
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub fn run_command(
+ &mut self,
+ command_range: Range<language::Anchor>,
+ name: &str,
+ arguments: &[String],
+ ensure_trailing_newline: bool,
+ workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(command) = self.slash_commands.command(name, cx) {
+ let context = self.context.read(cx);
+ let sections = context
+ .slash_command_output_sections()
+ .into_iter()
+ .filter(|section| section.is_valid(context.buffer().read(cx)))
+ .cloned()
+ .collect::<Vec<_>>();
+ let snapshot = context.buffer().read(cx).snapshot();
+ let output = command.run(
+ arguments,
+ §ions,
+ snapshot,
+ workspace,
+ self.lsp_adapter_delegate.clone(),
+ cx,
+ );
+ self.context.update(cx, |context, cx| {
+ context.insert_command_output(
+ command_range,
+ name,
+ output,
+ ensure_trailing_newline,
+ cx,
+ )
+ });
+ }
+ }
+
+ fn handle_context_event(
+ &mut self,
+ _: Model<Context>,
+ event: &ContextEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let context_editor = cx.view().downgrade();
+
+ match event {
+ ContextEvent::MessagesEdited => {
+ self.update_message_headers(cx);
+ self.update_image_blocks(cx);
+ self.context.update(cx, |context, cx| {
+ context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
+ });
+ }
+ ContextEvent::SummaryChanged => {
+ cx.emit(EditorEvent::TitleChanged);
+ self.context.update(cx, |context, cx| {
+ context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
+ });
+ }
+ ContextEvent::StreamedCompletion => {
+ self.editor.update(cx, |editor, cx| {
+ if let Some(scroll_position) = self.scroll_position {
+ let snapshot = editor.snapshot(cx);
+ let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
+ let scroll_top =
+ cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
+ editor.set_scroll_position(
+ point(scroll_position.offset_before_cursor.x, scroll_top),
+ cx,
+ );
+ }
+
+ let new_tool_uses = self
+ .context
+ .read(cx)
+ .pending_tool_uses()
+ .into_iter()
+ .filter(|tool_use| {
+ !self
+ .pending_tool_use_creases
+ .contains_key(&tool_use.source_range)
+ })
+ .cloned()
+ .collect::<Vec<_>>();
+
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
+ let excerpt_id = *excerpt_id;
+
+ let mut buffer_rows_to_fold = BTreeSet::new();
+
+ let creases = new_tool_uses
+ .iter()
+ .map(|tool_use| {
+ let placeholder = FoldPlaceholder {
+ render: render_fold_icon_button(
+ cx.view().downgrade(),
+ IconName::PocketKnife,
+ tool_use.name.clone().into(),
+ ),
+ ..Default::default()
+ };
+ let render_trailer =
+ move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
+
+ let start = buffer
+ .anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
+ .unwrap();
+ let end = buffer
+ .anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
+ .unwrap();
+
+ let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
+ buffer_rows_to_fold.insert(buffer_row);
+
+ self.context.update(cx, |context, cx| {
+ context.insert_content(
+ Content::ToolUse {
+ range: tool_use.source_range.clone(),
+ tool_use: LanguageModelToolUse {
+ id: tool_use.id.clone(),
+ name: tool_use.name.clone(),
+ input: tool_use.input.clone(),
+ },
+ },
+ cx,
+ );
+ });
+
+ Crease::inline(
+ start..end,
+ placeholder,
+ fold_toggle("tool-use"),
+ render_trailer,
+ )
+ })
+ .collect::<Vec<_>>();
+
+ let crease_ids = editor.insert_creases(creases, cx);
+
+ for buffer_row in buffer_rows_to_fold.into_iter().rev() {
+ editor.fold_at(&FoldAt { buffer_row }, cx);
+ }
+
+ self.pending_tool_use_creases.extend(
+ new_tool_uses
+ .iter()
+ .map(|tool_use| tool_use.source_range.clone())
+ .zip(crease_ids),
+ );
+ });
+ }
+ ContextEvent::PatchesUpdated { removed, updated } => {
+ self.patches_updated(removed, updated, cx);
+ }
+ ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
+ self.editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
+
+ editor.remove_creases(
+ removed
+ .iter()
+ .filter_map(|range| self.pending_slash_command_creases.remove(range)),
+ cx,
+ );
+
+ let crease_ids = editor.insert_creases(
+ updated.iter().map(|command| {
+ let workspace = self.workspace.clone();
+ let confirm_command = Arc::new({
+ let context_editor = context_editor.clone();
+ let command = command.clone();
+ move |cx: &mut WindowContext| {
+ context_editor
+ .update(cx, |context_editor, cx| {
+ context_editor.run_command(
+ command.source_range.clone(),
+ &command.name,
+ &command.arguments,
+ false,
+ workspace.clone(),
+ cx,
+ );
+ })
+ .ok();
+ }
+ });
+ let placeholder = FoldPlaceholder {
+ render: Arc::new(move |_, _, _| Empty.into_any()),
+ ..Default::default()
+ };
+ let render_toggle = {
+ let confirm_command = confirm_command.clone();
+ let command = command.clone();
+ move |row, _, _, _cx: &mut WindowContext| {
+ render_pending_slash_command_gutter_decoration(
+ row,
+ &command.status,
+ confirm_command.clone(),
+ )
+ }
+ };
+ let render_trailer = {
+ let command = command.clone();
+ move |row, _unfold, cx: &mut WindowContext| {
+ // TODO: In the future we should investigate how we can expose
+ // this as a hook on the `SlashCommand` trait so that we don't
+ // need to special-case it here.
+ if command.name == DocsSlashCommand::NAME {
+ return render_docs_slash_command_trailer(
+ row,
+ command.clone(),
+ cx,
+ );
+ }
+
+ Empty.into_any()
+ }
+ };
+
+ let start = buffer
+ .anchor_in_excerpt(excerpt_id, command.source_range.start)
+ .unwrap();
+ let end = buffer
+ .anchor_in_excerpt(excerpt_id, command.source_range.end)
+ .unwrap();
+ Crease::inline(start..end, placeholder, render_toggle, render_trailer)
+ }),
+ cx,
+ );
+
+ self.pending_slash_command_creases.extend(
+ updated
+ .iter()
+ .map(|command| command.source_range.clone())
+ .zip(crease_ids),
+ );
+ })
+ }
+ ContextEvent::InvokedSlashCommandChanged { command_id } => {
+ self.update_invoked_slash_command(*command_id, cx);
+ }
+ ContextEvent::SlashCommandOutputSectionAdded { section } => {
+ self.insert_slash_command_output_sections([section.clone()], false, cx);
+ }
+ ContextEvent::UsePendingTools => {
+ let pending_tool_uses = self
+ .context
+ .read(cx)
+ .pending_tool_uses()
+ .into_iter()
+ .filter(|tool_use| tool_use.status.is_idle())
+ .cloned()
+ .collect::<Vec<_>>();
+
+ for tool_use in pending_tool_uses {
+ if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
+ let task = tool.run(tool_use.input, self.workspace.clone(), cx);
+
+ self.context.update(cx, |context, cx| {
+ context.insert_tool_output(tool_use.id.clone(), task, cx);
+ });
+ }
+ }
+ }
+ ContextEvent::ToolFinished {
+ tool_use_id,
+ output_range,
+ } => {
+ self.editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
+ let excerpt_id = *excerpt_id;
+
+ let placeholder = FoldPlaceholder {
+ render: render_fold_icon_button(
+ cx.view().downgrade(),
+ IconName::PocketKnife,
+ format!("Tool Result: {tool_use_id}").into(),
+ ),
+ ..Default::default()
+ };
+ let render_trailer =
+ move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
+
+ let start = buffer
+ .anchor_in_excerpt(excerpt_id, output_range.start)
+ .unwrap();
+ let end = buffer
+ .anchor_in_excerpt(excerpt_id, output_range.end)
+ .unwrap();
+
+ let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
+
+ let crease = Crease::inline(
+ start..end,
+ placeholder,
+ fold_toggle("tool-use"),
+ render_trailer,
+ );
+
+ editor.insert_creases([crease], cx);
+ editor.fold_at(&FoldAt { buffer_row }, cx);
+ });
+ }
+ ContextEvent::Operation(_) => {}
+ ContextEvent::ShowAssistError(error_message) => {
+ self.last_error = Some(AssistError::Message(error_message.clone()));
+ }
+ ContextEvent::ShowPaymentRequiredError => {
+ self.last_error = Some(AssistError::PaymentRequired);
+ }
+ ContextEvent::ShowMaxMonthlySpendReachedError => {
+ self.last_error = Some(AssistError::MaxMonthlySpendReached);
+ }
+ }
+ }
+
+ fn update_invoked_slash_command(
+ &mut self,
+ command_id: InvokedSlashCommandId,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(invoked_slash_command) =
+ self.context.read(cx).invoked_slash_command(&command_id)
+ {
+ if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
+ let run_commands_in_ranges = invoked_slash_command
+ .run_commands_in_ranges
+ .iter()
+ .cloned()
+ .collect::<Vec<_>>();
+ for range in run_commands_in_ranges {
+ let commands = self.context.update(cx, |context, cx| {
+ context.reparse(cx);
+ context
+ .pending_commands_for_range(range.clone(), cx)
+ .to_vec()
+ });
+
+ for command in commands {
+ self.run_command(
+ command.source_range,
+ &command.name,
+ &command.arguments,
+ false,
+ self.workspace.clone(),
+ cx,
+ );
+ }
+ }
+ }
+ }
+
+ self.editor.update(cx, |editor, cx| {
+ if let Some(invoked_slash_command) =
+ self.context.read(cx).invoked_slash_command(&command_id)
+ {
+ if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let (&excerpt_id, _buffer_id, _buffer_snapshot) =
+ buffer.as_singleton().unwrap();
+
+ let start = buffer
+ .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
+ .unwrap();
+ let end = buffer
+ .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
+ .unwrap();
+ editor.remove_folds_with_type(
+ &[start..end],
+ TypeId::of::<PendingSlashCommand>(),
+ false,
+ cx,
+ );
+
+ editor.remove_creases(
+ HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
+ cx,
+ );
+ } else if let hash_map::Entry::Vacant(entry) =
+ self.invoked_slash_command_creases.entry(command_id)
+ {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let (&excerpt_id, _buffer_id, _buffer_snapshot) =
+ buffer.as_singleton().unwrap();
+ let context = self.context.downgrade();
+ let crease_start = buffer
+ .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
+ .unwrap();
+ let crease_end = buffer
+ .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
+ .unwrap();
+ let crease = Crease::inline(
+ crease_start..crease_end,
+ invoked_slash_command_fold_placeholder(command_id, context),
+ fold_toggle("invoked-slash-command"),
+ |_row, _folded, _cx| Empty.into_any(),
+ );
+ let crease_ids = editor.insert_creases([crease.clone()], cx);
+ editor.fold_creases(vec![crease], false, cx);
+ entry.insert(crease_ids[0]);
+ } else {
+ cx.notify()
+ }
+ } else {
+ editor.remove_creases(
+ HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
+ cx,
+ );
+ cx.notify();
+ };
+ });
+ }
+
+ fn patches_updated(
+ &mut self,
+ removed: &Vec<Range<text::Anchor>>,
+ updated: &Vec<Range<text::Anchor>>,
+ cx: &mut ViewContext<ContextEditor>,
+ ) {
+ let this = cx.view().downgrade();
+ let mut editors_to_close = Vec::new();
+
+ self.editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let multibuffer = &snapshot.buffer_snapshot;
+ let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
+
+ let mut removed_crease_ids = Vec::new();
+ let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
+ for range in removed {
+ if let Some(state) = self.patches.remove(range) {
+ let patch_start = multibuffer
+ .anchor_in_excerpt(excerpt_id, range.start)
+ .unwrap();
+ let patch_end = multibuffer
+ .anchor_in_excerpt(excerpt_id, range.end)
+ .unwrap();
+
+ editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
+ ranges_to_unfold.push(patch_start..patch_end);
+ removed_crease_ids.push(state.crease_id);
+ }
+ }
+ editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
+ editor.remove_creases(removed_crease_ids, cx);
+
+ for range in updated {
+ let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
+ continue;
+ };
+
+ let path_count = patch.path_count();
+ let patch_start = multibuffer
+ .anchor_in_excerpt(excerpt_id, patch.range.start)
+ .unwrap();
+ let patch_end = multibuffer
+ .anchor_in_excerpt(excerpt_id, patch.range.end)
+ .unwrap();
+ let render_block: RenderBlock = Arc::new({
+ let this = this.clone();
+ let patch_range = range.clone();
+ move |cx: &mut BlockContext<'_, '_>| {
+ let max_width = cx.max_width;
+ let gutter_width = cx.gutter_dimensions.full_width();
+ let block_id = cx.block_id;
+ let selected = cx.selected;
+ this.update(&mut **cx, |this, cx| {
+ this.render_patch_block(
+ patch_range.clone(),
+ max_width,
+ gutter_width,
+ block_id,
+ selected,
+ cx,
+ )
+ })
+ .ok()
+ .flatten()
+ .unwrap_or_else(|| Empty.into_any())
+ }
+ });
+
+ let height = path_count as u32 + 1;
+ let crease = Crease::block(
+ patch_start..patch_end,
+ height,
+ BlockStyle::Flex,
+ render_block.clone(),
+ );
+
+ let should_refold;
+ if let Some(state) = self.patches.get_mut(&range) {
+ if let Some(editor_state) = &state.editor {
+ if editor_state.opened_patch != patch {
+ state.update_task = Some({
+ let this = this.clone();
+ cx.spawn(|_, cx| async move {
+ Self::update_patch_editor(this.clone(), patch, cx)
+ .await
+ .log_err();
+ })
+ });
+ }
+ }
+
+ should_refold =
+ snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
+ } else {
+ let crease_id = editor.insert_creases([crease.clone()], cx)[0];
+ self.patches.insert(
+ range.clone(),
+ PatchViewState {
+ crease_id,
+ editor: None,
+ update_task: None,
+ },
+ );
+
+ should_refold = true;
+ }
+
+ if should_refold {
+ editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
+ editor.fold_creases(vec![crease], false, cx);
+ }
+ }
+ });
+
+ for editor in editors_to_close {
+ self.close_patch_editor(editor, cx);
+ }
+
+ self.update_active_patch(cx);
+ }
+
+ fn insert_slash_command_output_sections(
+ &mut self,
+ sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
+ expand_result: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let excerpt_id = *buffer.as_singleton().unwrap().0;
+ let mut buffer_rows_to_fold = BTreeSet::new();
+ let mut creases = Vec::new();
+ for section in sections {
+ let start = buffer
+ .anchor_in_excerpt(excerpt_id, section.range.start)
+ .unwrap();
+ let end = buffer
+ .anchor_in_excerpt(excerpt_id, section.range.end)
+ .unwrap();
+ let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
+ buffer_rows_to_fold.insert(buffer_row);
+ creases.push(
+ Crease::inline(
+ start..end,
+ FoldPlaceholder {
+ render: render_fold_icon_button(
+ cx.view().downgrade(),
+ section.icon,
+ section.label.clone(),
+ ),
+ merge_adjacent: false,
+ ..Default::default()
+ },
+ render_slash_command_output_toggle,
+ |_, _, _| Empty.into_any_element(),
+ )
+ .with_metadata(CreaseMetadata {
+ icon: section.icon,
+ label: section.label,
+ }),
+ );
+ }
+
+ editor.insert_creases(creases, cx);
+
+ if expand_result {
+ buffer_rows_to_fold.clear();
+ }
+ for buffer_row in buffer_rows_to_fold.into_iter().rev() {
+ editor.fold_at(&FoldAt { buffer_row }, cx);
+ }
+ });
+ }
+
+ fn handle_editor_event(
+ &mut self,
+ _: View<Editor>,
+ event: &EditorEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
+ let cursor_scroll_position = self.cursor_scroll_position(cx);
+ if *autoscroll {
+ self.scroll_position = cursor_scroll_position;
+ } else if self.scroll_position != cursor_scroll_position {
+ self.scroll_position = None;
+ }
+ }
+ EditorEvent::SelectionsChanged { .. } => {
+ self.scroll_position = self.cursor_scroll_position(cx);
+ self.update_active_patch(cx);
+ }
+ _ => {}
+ }
+ cx.emit(event.clone());
+ }
+
+ fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
+ let patch = self.active_patch.as_ref()?;
+ Some((patch.clone(), self.patches.get(&patch)?))
+ }
+
+ fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
+ let newest_cursor = self.editor.update(cx, |editor, cx| {
+ editor.selections.newest::<Point>(cx).head()
+ });
+ let context = self.context.read(cx);
+
+ let new_patch = context.patch_containing(newest_cursor, cx).cloned();
+
+ if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
+ return;
+ }
+
+ if let Some(old_patch_range) = self.active_patch.take() {
+ if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
+ if let Some(state) = patch_state.editor.take() {
+ if let Some(editor) = state.editor.upgrade() {
+ self.close_patch_editor(editor, cx);
+ }
+ }
+ }
+ }
+
+ if let Some(new_patch) = new_patch {
+ self.active_patch = Some(new_patch.range.clone());
+
+ if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
+ let mut editor = None;
+ if let Some(state) = &patch_state.editor {
+ if let Some(opened_editor) = state.editor.upgrade() {
+ editor = Some(opened_editor);
+ }
+ }
+
+ if let Some(editor) = editor {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace.activate_item(&editor, true, false, cx);
+ })
+ .ok();
+ } else {
+ patch_state.update_task = Some(cx.spawn(move |this, cx| async move {
+ Self::open_patch_editor(this, new_patch, cx).await.log_err();
+ }));
+ }
+ }
+ }
+ }
+
+ fn close_patch_editor(
+ &mut self,
+ editor: View<ProposedChangesEditor>,
+ cx: &mut ViewContext<ContextEditor>,
+ ) {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ if let Some(pane) = workspace.pane_for(&editor) {
+ pane.update(cx, |pane, cx| {
+ let item_id = editor.entity_id();
+ if !editor.read(cx).focus_handle(cx).is_focused(cx) {
+ pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
+ .detach_and_log_err(cx);
+ }
+ });
+ }
+ })
+ .ok();
+ }
+
+ async fn open_patch_editor(
+ this: WeakView<Self>,
+ patch: AssistantPatch,
+ mut cx: AsyncWindowContext,
+ ) -> Result<()> {
+ let project = this.update(&mut cx, |this, _| this.project.clone())?;
+ let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
+
+ let editor = cx.new_view(|cx| {
+ let editor = ProposedChangesEditor::new(
+ patch.title.clone(),
+ resolved_patch
+ .edit_groups
+ .iter()
+ .map(|(buffer, groups)| ProposedChangeLocation {
+ buffer: buffer.clone(),
+ ranges: groups
+ .iter()
+ .map(|group| group.context_range.clone())
+ .collect(),
+ })
+ .collect(),
+ Some(project.clone()),
+ cx,
+ );
+ resolved_patch.apply(&editor, cx);
+ editor
+ })?;
+
+ this.update(&mut cx, |this, cx| {
+ if let Some(patch_state) = this.patches.get_mut(&patch.range) {
+ patch_state.editor = Some(PatchEditorState {
+ editor: editor.downgrade(),
+ opened_patch: patch,
+ });
+ patch_state.update_task.take();
+ }
+
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
+ })
+ .log_err();
+ })?;
+
+ Ok(())
+ }
+
+ async fn update_patch_editor(
+ this: WeakView<Self>,
+ patch: AssistantPatch,
+ mut cx: AsyncWindowContext,
+ ) -> Result<()> {
+ let project = this.update(&mut cx, |this, _| this.project.clone())?;
+ let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
+ this.update(&mut cx, |this, cx| {
+ let patch_state = this.patches.get_mut(&patch.range)?;
+
+ let locations = resolved_patch
+ .edit_groups
+ .iter()
+ .map(|(buffer, groups)| ProposedChangeLocation {
+ buffer: buffer.clone(),
+ ranges: groups
+ .iter()
+ .map(|group| group.context_range.clone())
+ .collect(),
+ })
+ .collect();
+
+ if let Some(state) = &mut patch_state.editor {
+ if let Some(editor) = state.editor.upgrade() {
+ editor.update(cx, |editor, cx| {
+ editor.set_title(patch.title.clone(), cx);
+ editor.reset_locations(locations, cx);
+ resolved_patch.apply(editor, cx);
+ });
+
+ state.opened_patch = patch;
+ } else {
+ patch_state.editor.take();
+ }
+ }
+ patch_state.update_task.take();
+
+ Some(())
+ })?;
+ Ok(())
+ }
+
+ fn handle_editor_search_event(
+ &mut self,
+ _: View<Editor>,
+ event: &SearchEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ cx.emit(event.clone());
+ }
+
+ fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
+ self.editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let cursor = editor.selections.newest_anchor().head();
+ let cursor_row = cursor
+ .to_display_point(&snapshot.display_snapshot)
+ .row()
+ .as_f32();
+ let scroll_position = editor
+ .scroll_manager
+ .anchor()
+ .scroll_position(&snapshot.display_snapshot);
+
+ let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
+ if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
+ Some(ScrollPosition {
+ cursor,
+ offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
+ })
+ } else {
+ None
+ }
+ })
+ }
+
+ fn esc_kbd(cx: &WindowContext) -> Div {
+ let colors = cx.theme().colors().clone();
+
+ h_flex()
+ .items_center()
+ .gap_1()
+ .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
+ .text_size(TextSize::XSmall.rems(cx))
+ .text_color(colors.text_muted)
+ .child("Press")
+ .child(
+ h_flex()
+ .rounded_md()
+ .px_1()
+ .mr_0p5()
+ .border_1()
+ .border_color(theme::color_alpha(colors.border_variant, 0.6))
+ .bg(theme::color_alpha(colors.element_background, 0.6))
+ .child("esc"),
+ )
+ .child("to cancel")
+ }
+
+ fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+
+ let excerpt_id = *buffer.as_singleton().unwrap().0;
+ let mut old_blocks = std::mem::take(&mut self.blocks);
+ let mut blocks_to_remove: HashMap<_, _> = old_blocks
+ .iter()
+ .map(|(message_id, (_, block_id))| (*message_id, *block_id))
+ .collect();
+ let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
+
+ let render_block = |message: MessageMetadata| -> RenderBlock {
+ Arc::new({
+ let context = self.context.clone();
+
+ move |cx| {
+ let message_id = MessageId(message.timestamp);
+ let llm_loading = message.role == Role::Assistant
+ && message.status == MessageStatus::Pending;
+
+ let (label, spinner, note) = match message.role {
+ Role::User => (
+ Label::new("You").color(Color::Default).into_any_element(),
+ None,
+ None,
+ ),
+ Role::Assistant => {
+ let base_label = Label::new("Assistant").color(Color::Info);
+ let mut spinner = None;
+ let mut note = None;
+ let animated_label = if llm_loading {
+ base_label
+ .with_animation(
+ "pulsating-label",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 0.8)),
+ |label, delta| label.alpha(delta),
+ )
+ .into_any_element()
+ } else {
+ base_label.into_any_element()
+ };
+ if llm_loading {
+ spinner = Some(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Info)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| {
+ icon.transform(Transformation::rotate(
+ percentage(delta),
+ ))
+ },
+ )
+ .into_any_element(),
+ );
+ note = Some(Self::esc_kbd(cx).into_any_element());
+ }
+ (animated_label, spinner, note)
+ }
+ Role::System => (
+ Label::new("System")
+ .color(Color::Warning)
+ .into_any_element(),
+ None,
+ None,
+ ),
+ };
+
+ let sender = h_flex()
+ .items_center()
+ .gap_2p5()
+ .child(
+ ButtonLike::new("role")
+ .style(ButtonStyle::Filled)
+ .child(
+ h_flex()
+ .items_center()
+ .gap_1p5()
+ .child(label)
+ .children(spinner),
+ )
+ .tooltip(|cx| {
+ Tooltip::with_meta(
+ "Toggle message role",
+ None,
+ "Available roles: You (User), Assistant, System",
+ cx,
+ )
+ })
+ .on_click({
+ let context = context.clone();
+ move |_, cx| {
+ context.update(cx, |context, cx| {
+ context.cycle_message_roles(
+ HashSet::from_iter(Some(message_id)),
+ cx,
+ )
+ })
+ }
+ }),
+ )
+ .children(note);
+
+ h_flex()
+ .id(("message_header", message_id.as_u64()))
+ .pl(cx.gutter_dimensions.full_width())
+ .h_11()
+ .w_full()
+ .relative()
+ .gap_1p5()
+ .child(sender)
+ .children(match &message.cache {
+ Some(cache) if cache.is_final_anchor => match cache.status {
+ CacheStatus::Cached => Some(
+ div()
+ .id("cached")
+ .child(
+ Icon::new(IconName::DatabaseZap)
+ .size(IconSize::XSmall)
+ .color(Color::Hint),
+ )
+ .tooltip(|cx| {
+ Tooltip::with_meta(
+ "Context Cached",
+ None,
+ "Large messages cached to optimize performance",
+ cx,
+ )
+ })
+ .into_any_element(),
+ ),
+ CacheStatus::Pending => Some(
+ div()
+ .child(
+ Icon::new(IconName::Ellipsis)
+ .size(IconSize::XSmall)
+ .color(Color::Hint),
+ )
+ .into_any_element(),
+ ),
+ },
+ _ => None,
+ })
+ .children(match &message.status {
+ MessageStatus::Error(error) => Some(
+ Button::new("show-error", "Error")
+ .color(Color::Error)
+ .selected_label_color(Color::Error)
+ .selected_icon_color(Color::Error)
+ .icon(IconName::XCircle)
+ .icon_color(Color::Error)
+ .icon_size(IconSize::XSmall)
+ .icon_position(IconPosition::Start)
+ .tooltip(move |cx| Tooltip::text("View Details", cx))
+ .on_click({
+ let context = context.clone();
+ let error = error.clone();
+ move |_, cx| {
+ context.update(cx, |_, cx| {
+ cx.emit(ContextEvent::ShowAssistError(
+ error.clone(),
+ ));
+ });
+ }
+ })
+ .into_any_element(),
+ ),
+ MessageStatus::Canceled => Some(
+ h_flex()
+ .gap_1()
+ .items_center()
+ .child(
+ Icon::new(IconName::XCircle)
+ .color(Color::Disabled)
+ .size(IconSize::XSmall),
+ )
+ .child(
+ Label::new("Canceled")
+ .size(LabelSize::Small)
+ .color(Color::Disabled),
+ )
+ .into_any_element(),
+ ),
+ _ => None,
+ })
+ .into_any_element()
+ }
+ })
+ };
+ let create_block_properties = |message: &Message| BlockProperties {
+ height: 2,
+ style: BlockStyle::Sticky,
+ placement: BlockPlacement::Above(
+ buffer
+ .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
+ .unwrap(),
+ ),
+ priority: usize::MAX,
+ render: render_block(MessageMetadata::from(message)),
+ };
+ let mut new_blocks = vec![];
+ let mut block_index_to_message = vec![];
+ for message in self.context.read(cx).messages(cx) {
+ if let Some(_) = blocks_to_remove.remove(&message.id) {
+ // This is an old message that we might modify.
+ let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
+ debug_assert!(
+ false,
+ "old_blocks should contain a message_id we've just removed."
+ );
+ continue;
+ };
+ // Should we modify it?
+ let message_meta = MessageMetadata::from(&message);
+ if meta != &message_meta {
+ blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
+ *meta = message_meta;
+ }
+ } else {
+ // This is a new message.
+ new_blocks.push(create_block_properties(&message));
+ block_index_to_message.push((message.id, MessageMetadata::from(&message)));
+ }
+ }
+ editor.replace_blocks(blocks_to_replace, None, cx);
+ editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
+
+ let ids = editor.insert_blocks(new_blocks, None, cx);
+ old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
+ |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
+ ));
+ self.blocks = old_blocks;
+ });
+ }
+
+ /// Returns either the selected text, or the content of the Markdown code
+ /// block surrounding the cursor.
+ fn get_selection_or_code_block(
+ context_editor_view: &View<ContextEditor>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<(String, bool)> {
+ const CODE_FENCE_DELIMITER: &'static str = "```";
+
+ let context_editor = context_editor_view.read(cx).editor.clone();
+ context_editor.update(cx, |context_editor, cx| {
+ if context_editor.selections.newest::<Point>(cx).is_empty() {
+ let snapshot = context_editor.buffer().read(cx).snapshot(cx);
+ let (_, _, snapshot) = snapshot.as_singleton()?;
+
+ let head = context_editor.selections.newest::<Point>(cx).head();
+ let offset = snapshot.point_to_offset(head);
+
+ let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
+ let mut text = snapshot
+ .text_for_range(surrounding_code_block_range)
+ .collect::<String>();
+
+ // If there is no newline trailing the closing three-backticks, then
+ // tree-sitter-md extends the range of the content node to include
+ // the backticks.
+ if text.ends_with(CODE_FENCE_DELIMITER) {
+ text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
+ }
+
+ (!text.is_empty()).then_some((text, true))
+ } else {
+ let anchor = context_editor.selections.newest_anchor();
+ let text = context_editor
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .text_for_range(anchor.range())
+ .collect::<String>();
+
+ (!text.is_empty()).then_some((text, false))
+ }
+ })
+ }
+
+ pub fn insert_selection(
+ workspace: &mut Workspace,
+ _: &InsertIntoEditor,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+ return;
+ };
+ let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
+ return;
+ };
+ let Some(active_editor_view) = workspace
+ .active_item(cx)
+ .and_then(|item| item.act_as::<Editor>(cx))
+ else {
+ return;
+ };
+
+ if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
+ active_editor_view.update(cx, |editor, cx| {
+ editor.insert(&text, cx);
+ editor.focus(cx);
+ })
+ }
+ }
+
+ pub fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) {
+ let result = maybe!({
+ let panel = workspace.panel::<AssistantPanel>(cx)?;
+ let context_editor_view = panel.read(cx).active_context_editor(cx)?;
+ Self::get_selection_or_code_block(&context_editor_view, cx)
+ });
+ let Some((text, is_code_block)) = result else {
+ return;
+ };
+
+ cx.write_to_clipboard(ClipboardItem::new_string(text));
+
+ struct CopyToClipboardToast;
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<CopyToClipboardToast>(),
+ format!(
+ "{} copied to clipboard.",
+ if is_code_block {
+ "Code block"
+ } else {
+ "Selection"
+ }
+ ),
+ )
+ .autohide(),
+ cx,
+ );
+ }
+
+ pub fn insert_dragged_files(
+ workspace: &mut Workspace,
+ action: &InsertDraggedFiles,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+ return;
+ };
+ let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
+ return;
+ };
+
+ let project = workspace.project().clone();
+
+ let paths = match action {
+ InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
+ InsertDraggedFiles::ExternalFiles(paths) => {
+ let tasks = paths
+ .clone()
+ .into_iter()
+ .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
+ .collect::<Vec<_>>();
+
+ cx.spawn(move |_, cx| async move {
+ let mut paths = vec![];
+ let mut worktrees = vec![];
+
+ let opened_paths = futures::future::join_all(tasks).await;
+ for (worktree, project_path) in opened_paths.into_iter().flatten() {
+ let Ok(worktree_root_name) =
+ worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string())
+ else {
+ continue;
+ };
+
+ let mut full_path = PathBuf::from(worktree_root_name.clone());
+ full_path.push(&project_path.path);
+ paths.push(full_path);
+ worktrees.push(worktree);
+ }
+
+ (paths, worktrees)
+ })
+ }
+ };
+
+ cx.spawn(|_, mut cx| async move {
+ let (paths, dragged_file_worktrees) = paths.await;
+ let cmd_name = FileSlashCommand.name();
+
+ context_editor_view
+ .update(&mut cx, |context_editor, cx| {
+ let file_argument = paths
+ .into_iter()
+ .map(|path| path.to_string_lossy().to_string())
+ .collect::<Vec<_>>()
+ .join(" ");
+
+ context_editor.editor.update(cx, |editor, cx| {
+ editor.insert("\n", cx);
+ editor.insert(&format!("/{} {}", cmd_name, file_argument), cx);
+ });
+
+ context_editor.confirm_command(&ConfirmCommand, cx);
+
+ context_editor
+ .dragged_file_worktrees
+ .extend(dragged_file_worktrees);
+ })
+ .log_err();
+ })
+ .detach();
+ }
+
+ pub fn quote_selection(
+ workspace: &mut Workspace,
+ _: &QuoteSelection,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+ return;
+ };
+
+ let Some(creases) = selections_creases(workspace, cx) else {
+ return;
+ };
+
+ if creases.is_empty() {
+ 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.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);
+
+ 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);
+
+ editor.insert("\n", cx);
+
+ let fold_placeholder = quote_selection_fold_placeholder(
+ crease_title,
+ cx.view().downgrade(),
+ );
+ let crease = Crease::inline(
+ anchor_before..anchor_after,
+ fold_placeholder,
+ render_quote_selection_output_toggle,
+ |_, _, _| Empty.into_any(),
+ );
+ editor.insert_creases(vec![crease], cx);
+ editor.fold_at(
+ &FoldAt {
+ buffer_row: start_row,
+ },
+ cx,
+ );
+ }
+ })
+ });
+ };
+ });
+ });
+ }
+
+ fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
+ if self.editor.read(cx).selections.count() == 1 {
+ let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
+ cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
+ copied_text,
+ metadata,
+ ));
+ cx.stop_propagation();
+ return;
+ }
+
+ cx.propagate();
+ }
+
+ fn cut(&mut self, _: &editor::actions::Cut, cx: &mut ViewContext<Self>) {
+ if self.editor.read(cx).selections.count() == 1 {
+ let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
+
+ self.editor.update(cx, |editor, cx| {
+ editor.transact(cx, |this, cx| {
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(selections);
+ });
+ this.insert("", cx);
+ cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
+ copied_text,
+ metadata,
+ ));
+ });
+ });
+
+ cx.stop_propagation();
+ return;
+ }
+
+ cx.propagate();
+ }
+
+ fn get_clipboard_contents(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
+ let (snapshot, selection, creases) = self.editor.update(cx, |editor, cx| {
+ let mut selection = editor.selections.newest::<Point>(cx);
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+
+ let is_entire_line = selection.is_empty() || editor.selections.line_mode;
+ if is_entire_line {
+ selection.start = Point::new(selection.start.row, 0);
+ selection.end =
+ cmp::min(snapshot.max_point(), Point::new(selection.start.row + 1, 0));
+ selection.goal = SelectionGoal::None;
+ }
+
+ let selection_start = snapshot.point_to_offset(selection.start);
+
+ (
+ snapshot.clone(),
+ selection.clone(),
+ editor.display_map.update(cx, |display_map, cx| {
+ display_map
+ .snapshot(cx)
+ .crease_snapshot
+ .creases_in_range(
+ MultiBufferRow(selection.start.row)
+ ..MultiBufferRow(selection.end.row + 1),
+ &snapshot,
+ )
+ .filter_map(|crease| {
+ if let Crease::Inline {
+ range, metadata, ..
+ } = &crease
+ {
+ let metadata = metadata.as_ref()?;
+ let start = range
+ .start
+ .to_offset(&snapshot)
+ .saturating_sub(selection_start);
+ let end = range
+ .end
+ .to_offset(&snapshot)
+ .saturating_sub(selection_start);
+
+ let range_relative_to_selection = start..end;
+ if !range_relative_to_selection.is_empty() {
+ return Some(SelectedCreaseMetadata {
+ range_relative_to_selection,
+ crease: metadata.clone(),
+ });
+ }
+ }
+ None
+ })
+ .collect::<Vec<_>>()
+ }),
+ )
+ });
+
+ let selection = selection.map(|point| snapshot.point_to_offset(point));
+ let context = self.context.read(cx);
+
+ let mut text = String::new();
+ for message in context.messages(cx) {
+ if message.offset_range.start >= selection.range().end {
+ break;
+ } else if message.offset_range.end >= selection.range().start {
+ let range = cmp::max(message.offset_range.start, selection.range().start)
+ ..cmp::min(message.offset_range.end, selection.range().end);
+ if !range.is_empty() {
+ for chunk in context.buffer().read(cx).text_for_range(range) {
+ text.push_str(chunk);
+ }
+ if message.offset_range.end < selection.range().end {
+ text.push('\n');
+ }
+ }
+ }
+ }
+
+ (text, CopyMetadata { creases }, vec![selection])
+ }
+
+ fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
+ cx.stop_propagation();
+
+ let images = if let Some(item) = cx.read_from_clipboard() {
+ item.into_entries()
+ .filter_map(|entry| {
+ if let ClipboardEntry::Image(image) = entry {
+ Some(image)
+ } else {
+ None
+ }
+ })
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ let metadata = if let Some(item) = cx.read_from_clipboard() {
+ item.entries().first().and_then(|entry| {
+ if let ClipboardEntry::String(text) = entry {
+ text.metadata_json::<CopyMetadata>()
+ } else {
+ None
+ }
+ })
+ } else {
+ None
+ };
+
+ if images.is_empty() {
+ self.editor.update(cx, |editor, cx| {
+ let paste_position = editor.selections.newest::<usize>(cx).head();
+ editor.paste(action, cx);
+
+ if let Some(metadata) = metadata {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+
+ let mut buffer_rows_to_fold = BTreeSet::new();
+ let weak_editor = cx.view().downgrade();
+ editor.insert_creases(
+ metadata.creases.into_iter().map(|metadata| {
+ let start = buffer.anchor_after(
+ paste_position + metadata.range_relative_to_selection.start,
+ );
+ let end = buffer.anchor_before(
+ paste_position + metadata.range_relative_to_selection.end,
+ );
+
+ let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
+ buffer_rows_to_fold.insert(buffer_row);
+ Crease::inline(
+ start..end,
+ FoldPlaceholder {
+ render: render_fold_icon_button(
+ weak_editor.clone(),
+ metadata.crease.icon,
+ metadata.crease.label.clone(),
+ ),
+ ..Default::default()
+ },
+ render_slash_command_output_toggle,
+ |_, _, _| Empty.into_any(),
+ )
+ .with_metadata(metadata.crease.clone())
+ }),
+ cx,
+ );
+ for buffer_row in buffer_rows_to_fold.into_iter().rev() {
+ editor.fold_at(&FoldAt { buffer_row }, cx);
+ }
+ }
+ });
+ } else {
+ let mut image_positions = Vec::new();
+ self.editor.update(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ let edits = editor
+ .selections
+ .all::<usize>(cx)
+ .into_iter()
+ .map(|selection| (selection.start..selection.end, "\n"));
+ editor.edit(edits, cx);
+
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ for selection in editor.selections.all::<usize>(cx) {
+ image_positions.push(snapshot.anchor_before(selection.end));
+ }
+ });
+ });
+
+ self.context.update(cx, |context, cx| {
+ for image in images {
+ let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
+ else {
+ continue;
+ };
+ let image_id = image.id();
+ let image_task = LanguageModelImage::from_image(image, cx).shared();
+
+ for image_position in image_positions.iter() {
+ context.insert_content(
+ Content::Image {
+ anchor: image_position.text_anchor,
+ image_id,
+ image: image_task.clone(),
+ render_image: render_image.clone(),
+ },
+ cx,
+ );
+ }
+ }
+ });
+ }
+ }
+
+ fn update_image_blocks(&mut self, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let excerpt_id = *buffer.as_singleton().unwrap().0;
+ let old_blocks = std::mem::take(&mut self.image_blocks);
+ let new_blocks = self
+ .context
+ .read(cx)
+ .contents(cx)
+ .filter_map(|content| {
+ if let Content::Image {
+ anchor,
+ render_image,
+ ..
+ } = content
+ {
+ Some((anchor, render_image))
+ } else {
+ None
+ }
+ })
+ .filter_map(|(anchor, render_image)| {
+ const MAX_HEIGHT_IN_LINES: u32 = 8;
+ let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
+ let image = render_image.clone();
+ anchor.is_valid(&buffer).then(|| BlockProperties {
+ placement: BlockPlacement::Above(anchor),
+ height: MAX_HEIGHT_IN_LINES,
+ style: BlockStyle::Sticky,
+ render: Arc::new(move |cx| {
+ let image_size = size_for_image(
+ &image,
+ size(
+ cx.max_width - cx.gutter_dimensions.full_width(),
+ MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
+ ),
+ );
+ h_flex()
+ .pl(cx.gutter_dimensions.full_width())
+ .child(
+ img(image.clone())
+ .object_fit(gpui::ObjectFit::ScaleDown)
+ .w(image_size.width)
+ .h(image_size.height),
+ )
+ .into_any_element()
+ }),
+ priority: 0,
+ })
+ })
+ .collect::<Vec<_>>();
+
+ editor.remove_blocks(old_blocks, None, cx);
+ let ids = editor.insert_blocks(new_blocks, None, cx);
+ self.image_blocks = HashSet::from_iter(ids);
+ });
+ }
+
+ fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
+ self.context.update(cx, |context, cx| {
+ let selections = self.editor.read(cx).selections.disjoint_anchors();
+ for selection in selections.as_ref() {
+ let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
+ let range = selection
+ .map(|endpoint| endpoint.to_offset(&buffer))
+ .range();
+ context.split_message(range, cx);
+ }
+ });
+ }
+
+ fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
+ self.context.update(cx, |context, cx| {
+ context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
+ });
+ }
+
+ pub fn title(&self, cx: &AppContext) -> Cow<str> {
+ self.context
+ .read(cx)
+ .summary()
+ .map(|summary| summary.text.clone())
+ .map(Cow::Owned)
+ .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
+ }
+
+ fn render_patch_block(
+ &mut self,
+ range: Range<text::Anchor>,
+ max_width: Pixels,
+ gutter_width: Pixels,
+ id: BlockId,
+ selected: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<AnyElement> {
+ let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
+ let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
+ let excerpt_id = *excerpt_id;
+ let anchor = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(excerpt_id, range.start)
+ .unwrap();
+
+ let theme = cx.theme().clone();
+ let patch = self.context.read(cx).patch_for_range(&range, cx)?;
+ let paths = patch
+ .paths()
+ .map(|p| SharedString::from(p.to_string()))
+ .collect::<BTreeSet<_>>();
+
+ Some(
+ v_flex()
+ .id(id)
+ .bg(theme.colors().editor_background)
+ .ml(gutter_width)
+ .pb_1()
+ .w(max_width - gutter_width)
+ .rounded_md()
+ .border_1()
+ .border_color(theme.colors().border_variant)
+ .overflow_hidden()
+ .hover(|style| style.border_color(theme.colors().text_accent))
+ .when(selected, |this| {
+ this.border_color(theme.colors().text_accent)
+ })
+ .cursor(CursorStyle::PointingHand)
+ .on_click(cx.listener(move |this, _, cx| {
+ this.editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![anchor..anchor]);
+ });
+ });
+ this.focus_active_patch(cx);
+ }))
+ .child(
+ div()
+ .px_2()
+ .py_1()
+ .overflow_hidden()
+ .text_ellipsis()
+ .border_b_1()
+ .border_color(theme.colors().border_variant)
+ .bg(theme.colors().element_background)
+ .child(
+ Label::new(patch.title.clone())
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .children(paths.into_iter().map(|path| {
+ h_flex()
+ .px_2()
+ .pt_1()
+ .gap_1p5()
+ .child(Icon::new(IconName::File).size(IconSize::Small))
+ .child(Label::new(path).size(LabelSize::Small))
+ }))
+ .when(patch.status == AssistantPatchStatus::Pending, |div| {
+ div.child(
+ h_flex()
+ .pt_1()
+ .px_2()
+ .gap_1()
+ .child(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Muted)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| {
+ icon.transform(Transformation::rotate(percentage(
+ delta,
+ )))
+ },
+ ),
+ )
+ .child(
+ Label::new("Generating…")
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .with_animation(
+ "pulsating-label",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 0.8)),
+ |label, delta| label.alpha(delta),
+ ),
+ ),
+ )
+ })
+ .into_any(),
+ )
+ }
+
+ fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
+ use feature_flags::FeatureFlagAppExt;
+ let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
+ assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
+ });
+
+ if nudge.map_or(false, |value| value) {
+ Some(
+ h_flex()
+ .p_3()
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .bg(cx.theme().colors().editor_background)
+ .justify_between()
+ .child(
+ h_flex()
+ .gap_3()
+ .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
+ .child(Label::new("Zed AI is here! Get started by signing in →")),
+ )
+ .child(
+ Button::new("sign-in", "Sign in")
+ .size(ButtonSize::Compact)
+ .style(ButtonStyle::Filled)
+ .on_click(cx.listener(|this, _event, cx| {
+ let client = this
+ .workspace
+ .update(cx, |workspace, _| workspace.client().clone())
+ .log_err();
+
+ if let Some(client) = client {
+ cx.spawn(|this, mut cx| async move {
+ client.authenticate_and_connect(true, &mut cx).await?;
+ this.update(&mut cx, |_, cx| cx.notify())
+ })
+ .detach_and_log_err(cx)
+ }
+ })),
+ )
+ .into_any_element(),
+ )
+ } else if let Some(configuration_error) = configuration_error(cx) {
+ let label = match configuration_error {
+ ConfigurationError::NoProvider => "No LLM provider selected.",
+ ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
+ };
+ Some(
+ h_flex()
+ .px_3()
+ .py_2()
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .bg(cx.theme().colors().editor_background)
+ .justify_between()
+ .child(
+ h_flex()
+ .gap_3()
+ .child(
+ Icon::new(IconName::Warning)
+ .size(IconSize::Small)
+ .color(Color::Warning),
+ )
+ .child(Label::new(label)),
+ )
+ .child(
+ Button::new("open-configuration", "Configure Providers")
+ .size(ButtonSize::Compact)
+ .icon(Some(IconName::SlidersVertical))
+ .icon_size(IconSize::Small)
+ .icon_position(IconPosition::Start)
+ .style(ButtonStyle::Filled)
+ .on_click({
+ let focus_handle = self.focus_handle(cx).clone();
+ move |_event, cx| {
+ focus_handle.dispatch_action(&ShowConfiguration, cx);
+ }
+ }),
+ )
+ .into_any_element(),
+ )
+ } else {
+ None
+ }
+ }
+
+ fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let focus_handle = self.focus_handle(cx).clone();
+
+ let (style, tooltip) = match token_state(&self.context, cx) {
+ Some(TokenState::NoTokensLeft { .. }) => (
+ ButtonStyle::Tinted(TintColor::Error),
+ Some(Tooltip::text("Token limit reached", cx)),
+ ),
+ Some(TokenState::HasMoreTokens {
+ over_warn_threshold,
+ ..
+ }) => {
+ let (style, tooltip) = if over_warn_threshold {
+ (
+ ButtonStyle::Tinted(TintColor::Warning),
+ Some(Tooltip::text("Token limit is close to exhaustion", cx)),
+ )
+ } else {
+ (ButtonStyle::Filled, None)
+ };
+ (style, tooltip)
+ }
+ None => (ButtonStyle::Filled, None),
+ };
+
+ let provider = LanguageModelRegistry::read_global(cx).active_provider();
+
+ let has_configuration_error = configuration_error(cx).is_some();
+ let needs_to_accept_terms = self.show_accept_terms
+ && provider
+ .as_ref()
+ .map_or(false, |provider| provider.must_accept_terms(cx));
+ let disabled = has_configuration_error || needs_to_accept_terms;
+
+ ButtonLike::new("send_button")
+ .disabled(disabled)
+ .style(style)
+ .when_some(tooltip, |button, tooltip| {
+ button.tooltip(move |_| tooltip.clone())
+ })
+ .layer(ElevationIndex::ModalSurface)
+ .child(Label::new(
+ if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
+ "Chat"
+ } else {
+ "Send"
+ },
+ ))
+ .children(
+ KeyBinding::for_action_in(&Assist, &focus_handle, cx)
+ .map(|binding| binding.into_any_element()),
+ )
+ .on_click(move |_event, cx| {
+ focus_handle.dispatch_action(&Assist, cx);
+ })
+ }
+
+ fn render_edit_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let focus_handle = self.focus_handle(cx).clone();
+
+ let (style, tooltip) = match token_state(&self.context, cx) {
+ Some(TokenState::NoTokensLeft { .. }) => (
+ ButtonStyle::Tinted(TintColor::Error),
+ Some(Tooltip::text("Token limit reached", cx)),
+ ),
+ Some(TokenState::HasMoreTokens {
+ over_warn_threshold,
+ ..
+ }) => {
+ let (style, tooltip) = if over_warn_threshold {
+ (
+ ButtonStyle::Tinted(TintColor::Warning),
+ Some(Tooltip::text("Token limit is close to exhaustion", cx)),
+ )
+ } else {
+ (ButtonStyle::Filled, None)
+ };
+ (style, tooltip)
+ }
+ None => (ButtonStyle::Filled, None),
+ };
+
+ let provider = LanguageModelRegistry::read_global(cx).active_provider();
+
+ let has_configuration_error = configuration_error(cx).is_some();
+ let needs_to_accept_terms = self.show_accept_terms
+ && provider
+ .as_ref()
+ .map_or(false, |provider| provider.must_accept_terms(cx));
+ let disabled = has_configuration_error || needs_to_accept_terms;
+
+ ButtonLike::new("edit_button")
+ .disabled(disabled)
+ .style(style)
+ .when_some(tooltip, |button, tooltip| {
+ button.tooltip(move |_| tooltip.clone())
+ })
+ .layer(ElevationIndex::ModalSurface)
+ .child(Label::new("Suggest Edits"))
+ .children(
+ KeyBinding::for_action_in(&Edit, &focus_handle, cx)
+ .map(|binding| binding.into_any_element()),
+ )
+ .on_click(move |_event, cx| {
+ focus_handle.dispatch_action(&Edit, cx);
+ })
+ }
+
+ fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ slash_command_picker::SlashCommandSelector::new(
+ self.slash_commands.clone(),
+ cx.view().downgrade(),
+ Button::new("trigger", "Add Context")
+ .icon(IconName::Plus)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
+ )
+ }
+
+ fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
+ let last_error = self.last_error.as_ref()?;
+
+ Some(
+ div()
+ .absolute()
+ .right_3()
+ .bottom_12()
+ .max_w_96()
+ .py_2()
+ .px_3()
+ .elevation_2(cx)
+ .occlude()
+ .child(match last_error {
+ AssistError::FileRequired => self.render_file_required_error(cx),
+ AssistError::PaymentRequired => self.render_payment_required_error(cx),
+ AssistError::MaxMonthlySpendReached => {
+ self.render_max_monthly_spend_reached_error(cx)
+ }
+ AssistError::Message(error_message) => {
+ self.render_assist_error(error_message, cx)
+ }
+ })
+ .into_any(),
+ )
+ }
+
+ fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .gap_1p5()
+ .items_center()
+ .child(Icon::new(IconName::Warning).color(Color::Warning))
+ .child(
+ Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
+ ),
+ )
+ .child(
+ div()
+ .id("error-message")
+ .max_h_24()
+ .overflow_y_scroll()
+ .child(Label::new(
+ "To include files, type /file or /tab in your prompt.",
+ )),
+ )
+ .child(
+ h_flex()
+ .justify_end()
+ .mt_1()
+ .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
+ |this, _, cx| {
+ this.last_error = None;
+ cx.notify();
+ },
+ ))),
+ )
+ .into_any()
+ }
+
+ fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+ const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
+
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .gap_1p5()
+ .items_center()
+ .child(Icon::new(IconName::XCircle).color(Color::Error))
+ .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
+ )
+ .child(
+ div()
+ .id("error-message")
+ .max_h_24()
+ .overflow_y_scroll()
+ .child(Label::new(ERROR_MESSAGE)),
+ )
+ .child(
+ h_flex()
+ .justify_end()
+ .mt_1()
+ .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
+ |this, _, cx| {
+ this.last_error = None;
+ cx.open_url(&zed_urls::account_url(cx));
+ cx.notify();
+ },
+ )))
+ .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
+ |this, _, cx| {
+ this.last_error = None;
+ cx.notify();
+ },
+ ))),
+ )
+ .into_any()
+ }
+
+ fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+ const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
+
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .gap_1p5()
+ .items_center()
+ .child(Icon::new(IconName::XCircle).color(Color::Error))
+ .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
+ )
+ .child(
+ div()
+ .id("error-message")
+ .max_h_24()
+ .overflow_y_scroll()
+ .child(Label::new(ERROR_MESSAGE)),
+ )
+ .child(
+ h_flex()
+ .justify_end()
+ .mt_1()
+ .child(
+ Button::new("subscribe", "Update Monthly Spend Limit").on_click(
+ cx.listener(|this, _, cx| {
+ this.last_error = None;
+ cx.open_url(&zed_urls::account_url(cx));
+ cx.notify();
+ }),
+ ),
+ )
+ .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
+ |this, _, cx| {
+ this.last_error = None;
+ cx.notify();
+ },
+ ))),
+ )
+ .into_any()
+ }
+
+ fn render_assist_error(
+ &self,
+ error_message: &SharedString,
+ cx: &mut ViewContext<Self>,
+ ) -> AnyElement {
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .gap_1p5()
+ .items_center()
+ .child(Icon::new(IconName::XCircle).color(Color::Error))
+ .child(
+ Label::new("Error interacting with language model")
+ .weight(FontWeight::MEDIUM),
+ ),
+ )
+ .child(
+ div()
+ .id("error-message")
+ .max_h_32()
+ .overflow_y_scroll()
+ .child(Label::new(error_message.clone())),
+ )
+ .child(
+ h_flex()
+ .justify_end()
+ .mt_1()
+ .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
+ |this, _, cx| {
+ this.last_error = None;
+ cx.notify();
+ },
+ ))),
+ )
+ .into_any()
+ }
+}
+
+/// Returns the contents of the *outermost* fenced code block that contains the given offset.
+fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
+ const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
+ const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
+
+ let layer = snapshot.syntax_layers().next()?;
+
+ let root_node = layer.node();
+ let mut cursor = root_node.walk();
+
+ // Go to the first child for the given offset
+ while cursor.goto_first_child_for_byte(offset).is_some() {
+ // If we're at the end of the node, go to the next one.
+ // Example: if you have a fenced-code-block, and you're on the start of the line
+ // right after the closing ```, you want to skip the fenced-code-block and
+ // go to the next sibling.
+ if cursor.node().end_byte() == offset {
+ cursor.goto_next_sibling();
+ }
+
+ if cursor.node().start_byte() > offset {
+ break;
+ }
+
+ // We found the fenced code block.
+ if cursor.node().kind() == CODE_BLOCK_NODE {
+ // Now we need to find the child node that contains the code.
+ cursor.goto_first_child();
+ loop {
+ if cursor.node().kind() == CODE_BLOCK_CONTENT {
+ return Some(cursor.node().byte_range());
+ }
+ if !cursor.goto_next_sibling() {
+ break;
+ }
+ }
+ }
+ }
+
+ None
+}
+
+fn render_fold_icon_button(
+ editor: WeakView<Editor>,
+ icon: IconName,
+ label: SharedString,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement> {
+ Arc::new(move |fold_id, fold_range, _cx| {
+ let editor = editor.clone();
+ ButtonLike::new(fold_id)
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ElevatedSurface)
+ .child(Icon::new(icon))
+ .child(Label::new(label.clone()).single_line())
+ .on_click(move |_, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ let buffer_start = fold_range
+ .start
+ .to_point(&editor.buffer().read(cx).read(cx));
+ let buffer_row = MultiBufferRow(buffer_start.row);
+ editor.unfold_at(&UnfoldAt { buffer_row }, cx);
+ })
+ .ok();
+ })
+ .into_any_element()
+ })
+}
+
+type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
+
+fn render_slash_command_output_toggle(
+ row: MultiBufferRow,
+ is_folded: bool,
+ fold: ToggleFold,
+ _cx: &mut WindowContext,
+) -> AnyElement {
+ Disclosure::new(
+ ("slash-command-output-fold-indicator", row.0 as u64),
+ !is_folded,
+ )
+ .toggle_state(is_folded)
+ .on_click(move |_e, cx| fold(!is_folded, cx))
+ .into_any_element()
+}
+
+pub fn fold_toggle(
+ name: &'static str,
+) -> impl Fn(
+ MultiBufferRow,
+ bool,
+ Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
+ &mut WindowContext,
+) -> AnyElement {
+ move |row, is_folded, fold, _cx| {
+ Disclosure::new((name, row.0 as u64), !is_folded)
+ .toggle_state(is_folded)
+ .on_click(move |_e, cx| fold(!is_folded, cx))
+ .into_any_element()
+ }
+}
+
+fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) -> FoldPlaceholder {
+ FoldPlaceholder {
+ render: Arc::new({
+ move |fold_id, fold_range, _cx| {
+ let editor = editor.clone();
+ ButtonLike::new(fold_id)
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ElevatedSurface)
+ .child(Icon::new(IconName::TextSnippet))
+ .child(Label::new(title.clone()).single_line())
+ .on_click(move |_, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ let buffer_start = fold_range
+ .start
+ .to_point(&editor.buffer().read(cx).read(cx));
+ let buffer_row = MultiBufferRow(buffer_start.row);
+ editor.unfold_at(&UnfoldAt { buffer_row }, cx);
+ })
+ .ok();
+ })
+ .into_any_element()
+ }
+ }),
+ merge_adjacent: false,
+ ..Default::default()
+ }
+}
+
+fn render_quote_selection_output_toggle(
+ row: MultiBufferRow,
+ is_folded: bool,
+ fold: ToggleFold,
+ _cx: &mut WindowContext,
+) -> AnyElement {
+ Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
+ .toggle_state(is_folded)
+ .on_click(move |_e, cx| fold(!is_folded, cx))
+ .into_any_element()
+}
+
+fn render_pending_slash_command_gutter_decoration(
+ row: MultiBufferRow,
+ status: &PendingSlashCommandStatus,
+ confirm_command: Arc<dyn Fn(&mut WindowContext)>,
+) -> AnyElement {
+ let mut icon = IconButton::new(
+ ("slash-command-gutter-decoration", row.0),
+ ui::IconName::TriangleRight,
+ )
+ .on_click(move |_e, cx| confirm_command(cx))
+ .icon_size(ui::IconSize::Small)
+ .size(ui::ButtonSize::None);
+
+ match status {
+ PendingSlashCommandStatus::Idle => {
+ icon = icon.icon_color(Color::Muted);
+ }
+ PendingSlashCommandStatus::Running { .. } => {
+ icon = icon.toggle_state(true);
+ }
+ PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
+ }
+
+ icon.into_any_element()
+}
+
+fn render_docs_slash_command_trailer(
+ row: MultiBufferRow,
+ command: ParsedSlashCommand,
+ cx: &mut WindowContext,
+) -> AnyElement {
+ if command.arguments.is_empty() {
+ return Empty.into_any();
+ }
+ let args = DocsSlashCommandArgs::parse(&command.arguments);
+
+ let Some(store) = args
+ .provider()
+ .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
+ else {
+ return Empty.into_any();
+ };
+
+ let Some(package) = args.package() else {
+ return Empty.into_any();
+ };
+
+ let mut children = Vec::new();
+
+ if store.is_indexing(&package) {
+ children.push(
+ div()
+ .id(("crates-being-indexed", row.0))
+ .child(Icon::new(IconName::ArrowCircle).with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(4)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ ))
+ .tooltip({
+ let package = package.clone();
+ move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
+ })
+ .into_any_element(),
+ );
+ }
+
+ if let Some(latest_error) = store.latest_error_for_package(&package) {
+ children.push(
+ div()
+ .id(("latest-error", row.0))
+ .child(
+ Icon::new(IconName::Warning)
+ .size(IconSize::Small)
+ .color(Color::Warning),
+ )
+ .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx))
+ .into_any_element(),
+ )
+ }
+
+ let is_indexing = store.is_indexing(&package);
+ let latest_error = store.latest_error_for_package(&package);
+
+ if !is_indexing && latest_error.is_none() {
+ return Empty.into_any();
+ }
+
+ h_flex().gap_2().children(children).into_any_element()
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct CopyMetadata {
+ creases: Vec<SelectedCreaseMetadata>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct SelectedCreaseMetadata {
+ range_relative_to_selection: Range<usize>,
+ crease: CreaseMetadata,
+}
+
+impl EventEmitter<EditorEvent> for ContextEditor {}
+impl EventEmitter<SearchEvent> for ContextEditor {}
+
+impl Render for ContextEditor {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let provider = LanguageModelRegistry::read_global(cx).active_provider();
+ let accept_terms = if self.show_accept_terms {
+ provider
+ .as_ref()
+ .and_then(|provider| provider.render_accept_terms(cx))
+ } else {
+ None
+ };
+
+ v_flex()
+ .key_context("ContextEditor")
+ .capture_action(cx.listener(ContextEditor::cancel))
+ .capture_action(cx.listener(ContextEditor::save))
+ .capture_action(cx.listener(ContextEditor::copy))
+ .capture_action(cx.listener(ContextEditor::cut))
+ .capture_action(cx.listener(ContextEditor::paste))
+ .capture_action(cx.listener(ContextEditor::cycle_message_role))
+ .capture_action(cx.listener(ContextEditor::confirm_command))
+ .on_action(cx.listener(ContextEditor::edit))
+ .on_action(cx.listener(ContextEditor::assist))
+ .on_action(cx.listener(ContextEditor::split))
+ .size_full()
+ .children(self.render_notice(cx))
+ .child(
+ div()
+ .flex_grow()
+ .bg(cx.theme().colors().editor_background)
+ .child(self.editor.clone()),
+ )
+ .when_some(accept_terms, |this, element| {
+ this.child(
+ div()
+ .absolute()
+ .right_3()
+ .bottom_12()
+ .max_w_96()
+ .py_2()
+ .px_3()
+ .elevation_2(cx)
+ .bg(cx.theme().colors().surface_background)
+ .occlude()
+ .child(element),
+ )
+ })
+ .children(self.render_last_error(cx))
+ .child(
+ h_flex().w_full().relative().child(
+ h_flex()
+ .p_2()
+ .w_full()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .bg(cx.theme().colors().editor_background)
+ .child(h_flex().gap_1().child(self.render_inject_context_menu(cx)))
+ .child(
+ h_flex()
+ .w_full()
+ .justify_end()
+ .when(
+ AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
+ |buttons| {
+ buttons
+ .items_center()
+ .gap_1p5()
+ .child(self.render_edit_button(cx))
+ .child(
+ Label::new("or")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ },
+ )
+ .child(self.render_send_button(cx)),
+ ),
+ ),
+ )
+ }
+}
+
+impl FocusableView for ContextEditor {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.editor.focus_handle(cx)
+ }
+}
+
+impl Item for ContextEditor {
+ type Event = editor::EditorEvent;
+
+ fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
+ Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
+ }
+
+ fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
+ match event {
+ EditorEvent::Edited { .. } => {
+ f(item::ItemEvent::Edit);
+ }
+ EditorEvent::TitleChanged => {
+ f(item::ItemEvent::UpdateTab);
+ }
+ _ => {}
+ }
+ }
+
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
+ Some(self.title(cx).to_string().into())
+ }
+
+ fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(handle.clone()))
+ }
+
+ fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ Item::set_nav_history(editor, nav_history, cx)
+ })
+ }
+
+ fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
+ self.editor
+ .update(cx, |editor, cx| Item::navigate(editor, data, cx))
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, Item::deactivated)
+ }
+
+ fn act_as_type<'a>(
+ &'a self,
+ type_id: TypeId,
+ self_handle: &'a View<Self>,
+ _: &'a AppContext,
+ ) -> Option<AnyView> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle.to_any())
+ } else if type_id == TypeId::of::<Editor>() {
+ Some(self.editor.to_any())
+ } else {
+ None
+ }
+ }
+
+ fn include_in_nav_history() -> bool {
+ false
+ }
+}
+
+impl SearchableItem for ContextEditor {
+ type Match = <Editor as SearchableItem>::Match;
+
+ fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ editor.clear_matches(cx);
+ });
+ }
+
+ fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.update_matches(matches, cx));
+ }
+
+ fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
+ self.editor
+ .update(cx, |editor, cx| editor.query_suggestion(cx))
+ }
+
+ fn activate_match(
+ &mut self,
+ index: usize,
+ matches: &[Self::Match],
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.editor.update(cx, |editor, cx| {
+ editor.activate_match(index, matches, cx);
+ });
+ }
+
+ fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.select_matches(matches, cx));
+ }
+
+ fn replace(
+ &mut self,
+ identifier: &Self::Match,
+ query: &project::search::SearchQuery,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.editor
+ .update(cx, |editor, cx| editor.replace(identifier, query, cx));
+ }
+
+ fn find_matches(
+ &mut self,
+ query: Arc<project::search::SearchQuery>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Vec<Self::Match>> {
+ self.editor
+ .update(cx, |editor, cx| editor.find_matches(query, cx))
+ }
+
+ fn active_match_index(
+ &mut self,
+ matches: &[Self::Match],
+ cx: &mut ViewContext<Self>,
+ ) -> Option<usize> {
+ self.editor
+ .update(cx, |editor, cx| editor.active_match_index(matches, cx))
+ }
+}
+
+impl FollowableItem for ContextEditor {
+ fn remote_id(&self) -> Option<workspace::ViewId> {
+ self.remote_id
+ }
+
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
+ let context = self.context.read(cx);
+ Some(proto::view::Variant::ContextEditor(
+ proto::view::ContextEditor {
+ context_id: context.id().to_proto(),
+ editor: if let Some(proto::view::Variant::Editor(proto)) =
+ self.editor.read(cx).to_state_proto(cx)
+ {
+ Some(proto)
+ } else {
+ None
+ },
+ },
+ ))
+ }
+
+ fn from_state_proto(
+ workspace: View<Workspace>,
+ id: workspace::ViewId,
+ state: &mut Option<proto::view::Variant>,
+ cx: &mut WindowContext,
+ ) -> Option<Task<Result<View<Self>>>> {
+ let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
+ return None;
+ };
+ let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
+ unreachable!()
+ };
+
+ let context_id = ContextId::from_proto(state.context_id);
+ let editor_state = state.editor?;
+
+ let (project, panel) = workspace.update(cx, |workspace, cx| {
+ Some((
+ workspace.project().clone(),
+ workspace.panel::<AssistantPanel>(cx)?,
+ ))
+ })?;
+
+ let context_editor =
+ panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
+
+ Some(cx.spawn(|mut cx| async move {
+ let context_editor = context_editor.await?;
+ context_editor
+ .update(&mut cx, |context_editor, cx| {
+ context_editor.remote_id = Some(id);
+ context_editor.editor.update(cx, |editor, cx| {
+ editor.apply_update_proto(
+ &project,
+ proto::update_view::Variant::Editor(proto::update_view::Editor {
+ selections: editor_state.selections,
+ pending_selection: editor_state.pending_selection,
+ scroll_top_anchor: editor_state.scroll_top_anchor,
+ scroll_x: editor_state.scroll_y,
+ scroll_y: editor_state.scroll_y,
+ ..Default::default()
+ }),
+ cx,
+ )
+ })
+ })?
+ .await?;
+ Ok(context_editor)
+ }))
+ }
+
+ fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
+ Editor::to_follow_event(event)
+ }
+
+ fn add_event_to_update_proto(
+ &self,
+ event: &Self::Event,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &WindowContext,
+ ) -> bool {
+ self.editor
+ .read(cx)
+ .add_event_to_update_proto(event, update, cx)
+ }
+
+ fn apply_update_proto(
+ &mut self,
+ project: &Model<Project>,
+ message: proto::update_view::Variant,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ self.editor.update(cx, |editor, cx| {
+ editor.apply_update_proto(project, message, cx)
+ })
+ }
+
+ fn is_project_item(&self, _cx: &WindowContext) -> bool {
+ true
+ }
+
+ fn set_leader_peer_id(
+ &mut self,
+ leader_peer_id: Option<proto::PeerId>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.editor.update(cx, |editor, cx| {
+ editor.set_leader_peer_id(leader_peer_id, cx)
+ })
+ }
+
+ fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
+ if existing.context.read(cx).id() == self.context.read(cx).id() {
+ Some(item::Dedup::KeepExisting)
+ } else {
+ None
+ }
+ }
+}
+
+pub struct ContextEditorToolbarItem {
+ active_context_editor: Option<WeakView<ContextEditor>>,
+ model_summary_editor: View<Editor>,
+ language_model_selector: View<LanguageModelSelector>,
+ language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
+}
+
+impl ContextEditorToolbarItem {
+ pub fn new(
+ workspace: &Workspace,
+ model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
+ model_summary_editor: View<Editor>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ Self {
+ active_context_editor: None,
+ model_summary_editor,
+ language_model_selector: cx.new_view(|cx| {
+ let fs = workspace.app_state().fs.clone();
+ LanguageModelSelector::new(
+ move |model, cx| {
+ update_settings_file::<AssistantSettings>(
+ fs.clone(),
+ cx,
+ move |settings, _| settings.set_model(model.clone()),
+ );
+ },
+ cx,
+ )
+ }),
+ language_model_selector_menu_handle: model_selector_menu_handle,
+ }
+ }
+
+ fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
+ let context = &self
+ .active_context_editor
+ .as_ref()?
+ .upgrade()?
+ .read(cx)
+ .context;
+ let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
+ TokenState::NoTokensLeft {
+ max_token_count,
+ token_count,
+ } => (Color::Error, token_count, max_token_count),
+ TokenState::HasMoreTokens {
+ max_token_count,
+ token_count,
+ over_warn_threshold,
+ } => {
+ let color = if over_warn_threshold {
+ Color::Warning
+ } else {
+ Color::Muted
+ };
+ (color, token_count, max_token_count)
+ }
+ };
+ Some(
+ h_flex()
+ .gap_0p5()
+ .child(
+ Label::new(humanize_token_count(token_count))
+ .size(LabelSize::Small)
+ .color(token_count_color),
+ )
+ .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
+ .child(
+ Label::new(humanize_token_count(max_token_count))
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ }
+}
+
+impl Render for ContextEditorToolbarItem {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let left_side = h_flex()
+ .group("chat-title-group")
+ .gap_1()
+ .items_center()
+ .flex_grow()
+ .child(
+ div()
+ .w_full()
+ .when(self.active_context_editor.is_some(), |left_side| {
+ left_side.child(self.model_summary_editor.clone())
+ }),
+ )
+ .child(
+ div().visible_on_hover("chat-title-group").child(
+ IconButton::new("regenerate-context", IconName::RefreshTitle)
+ .shape(ui::IconButtonShape::Square)
+ .tooltip(|cx| Tooltip::text("Regenerate Title", cx))
+ .on_click(cx.listener(move |_, _, cx| {
+ cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
+ })),
+ ),
+ );
+ let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
+ let active_model = LanguageModelRegistry::read_global(cx).active_model();
+ let right_side = h_flex()
+ .gap_2()
+ // TODO display this in a nicer way, once we have a design for it.
+ // .children({
+ // let project = self
+ // .workspace
+ // .upgrade()
+ // .map(|workspace| workspace.read(cx).project().downgrade());
+ //
+ // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
+ // project.and_then(|project| db.remaining_summaries(&project, cx))
+ // });
+ // scan_items_remaining
+ // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
+ // })
+ .child(
+ LanguageModelSelectorPopoverMenu::new(
+ self.language_model_selector.clone(),
+ ButtonLike::new("active-model")
+ .style(ButtonStyle::Subtle)
+ .child(
+ h_flex()
+ .w_full()
+ .gap_0p5()
+ .child(
+ div()
+ .overflow_x_hidden()
+ .flex_grow()
+ .whitespace_nowrap()
+ .child(match (active_provider, active_model) {
+ (Some(provider), Some(model)) => h_flex()
+ .gap_1()
+ .child(
+ Icon::new(
+ model
+ .icon()
+ .unwrap_or_else(|| provider.icon()),
+ )
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ )
+ .child(
+ Label::new(model.name().0)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .into_any_element(),
+ _ => Label::new("No model selected")
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .into_any_element(),
+ }),
+ )
+ .child(
+ Icon::new(IconName::ChevronDown)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ ),
+ )
+ .tooltip(move |cx| {
+ Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
+ }),
+ )
+ .with_handle(self.language_model_selector_menu_handle.clone()),
+ )
+ .children(self.render_remaining_tokens(cx));
+
+ h_flex()
+ .px_0p5()
+ .size_full()
+ .gap_2()
+ .justify_between()
+ .child(left_side)
+ .child(right_side)
+ }
+}
+
+impl ToolbarItemView for ContextEditorToolbarItem {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) -> ToolbarItemLocation {
+ self.active_context_editor = active_pane_item
+ .and_then(|item| item.act_as::<ContextEditor>(cx))
+ .map(|editor| editor.downgrade());
+ cx.notify();
+ if self.active_context_editor.is_none() {
+ ToolbarItemLocation::Hidden
+ } else {
+ ToolbarItemLocation::PrimaryRight
+ }
+ }
+
+ fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
+ cx.notify();
+ }
+}
+
+impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
+
+pub enum ContextEditorToolbarItemEvent {
+ RegenerateSummary,
+}
+impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
+
+enum PendingSlashCommand {}
+
+fn invoked_slash_command_fold_placeholder(
+ command_id: InvokedSlashCommandId,
+ context: WeakModel<Context>,
+) -> FoldPlaceholder {
+ FoldPlaceholder {
+ constrain_width: false,
+ merge_adjacent: false,
+ render: Arc::new(move |fold_id, _, cx| {
+ let Some(context) = context.upgrade() else {
+ return Empty.into_any();
+ };
+
+ let Some(command) = context.read(cx).invoked_slash_command(&command_id) else {
+ return Empty.into_any();
+ };
+
+ h_flex()
+ .id(fold_id)
+ .px_1()
+ .ml_6()
+ .gap_2()
+ .bg(cx.theme().colors().surface_background)
+ .rounded_md()
+ .child(Label::new(format!("/{}", command.name.clone())))
+ .map(|parent| match &command.status {
+ InvokedSlashCommandStatus::Running(_) => {
+ parent.child(Icon::new(IconName::ArrowCircle).with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(4)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ ))
+ }
+ InvokedSlashCommandStatus::Error(message) => parent.child(
+ Label::new(format!("error: {message}"))
+ .single_line()
+ .color(Color::Error),
+ ),
+ InvokedSlashCommandStatus::Finished => parent,
+ })
+ .into_any_element()
+ }),
+ type_tag: Some(TypeId::of::<PendingSlashCommand>()),
+ }
+}
+
+enum TokenState {
+ NoTokensLeft {
+ max_token_count: usize,
+ token_count: usize,
+ },
+ HasMoreTokens {
+ max_token_count: usize,
+ token_count: usize,
+ over_warn_threshold: bool,
+ },
+}
+
+fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
+ const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
+
+ let model = LanguageModelRegistry::read_global(cx).active_model()?;
+ let token_count = context.read(cx).token_count()?;
+ let max_token_count = model.max_token_count();
+
+ let remaining_tokens = max_token_count as isize - token_count as isize;
+ let token_state = if remaining_tokens <= 0 {
+ TokenState::NoTokensLeft {
+ max_token_count,
+ token_count,
+ }
+ } else {
+ let over_warn_threshold =
+ token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
+ TokenState::HasMoreTokens {
+ max_token_count,
+ token_count,
+ over_warn_threshold,
+ }
+ };
+ Some(token_state)
+}
+
+fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
+ let image_size = data
+ .size(0)
+ .map(|dimension| Pixels::from(u32::from(dimension)));
+ let image_ratio = image_size.width / image_size.height;
+ let bounds_ratio = max_size.width / max_size.height;
+
+ if image_size.width > max_size.width || image_size.height > max_size.height {
+ if bounds_ratio > image_ratio {
+ size(
+ image_size.width * (max_size.height / image_size.height),
+ max_size.height,
+ )
+ } else {
+ size(
+ max_size.width,
+ image_size.height * (max_size.width / image_size.width),
+ )
+ }
+ } else {
+ size(image_size.width, image_size.height)
+ }
+}
+
+enum ConfigurationError {
+ NoProvider,
+ ProviderNotAuthenticated,
+}
+
+fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
+ let provider = LanguageModelRegistry::read_global(cx).active_provider();
+ let is_authenticated = provider
+ .as_ref()
+ .map_or(false, |provider| provider.is_authenticated(cx));
+
+ if provider.is_some() && is_authenticated {
+ return None;
+ }
+
+ if provider.is_none() {
+ return Some(ConfigurationError::NoProvider);
+ }
+
+ if !is_authenticated {
+ return Some(ConfigurationError::ProviderNotAuthenticated);
+ }
+
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{AppContext, Context};
+ use language::Buffer;
+ use unindent::Unindent;
+
+ #[gpui::test]
+ fn test_find_code_blocks(cx: &mut AppContext) {
+ let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
+
+ let buffer = cx.new_model(|cx| {
+ let text = r#"
+ line 0
+ line 1
+ ```rust
+ fn main() {}
+ ```
+ line 5
+ line 6
+ line 7
+ ```go
+ func main() {}
+ ```
+ line 11
+ ```
+ this is plain text code block
+ ```
+
+ ```go
+ func another() {}
+ ```
+ line 19
+ "#
+ .unindent();
+ let mut buffer = Buffer::local(text, cx);
+ buffer.set_language(Some(markdown.clone()), cx);
+ buffer
+ });
+ let snapshot = buffer.read(cx).snapshot();
+
+ let code_blocks = vec![
+ Point::new(3, 0)..Point::new(4, 0),
+ Point::new(9, 0)..Point::new(10, 0),
+ Point::new(13, 0)..Point::new(14, 0),
+ Point::new(17, 0)..Point::new(18, 0),
+ ]
+ .into_iter()
+ .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
+ .collect::<Vec<_>>();
+
+ let expected_results = vec![
+ (0, None),
+ (1, None),
+ (2, Some(code_blocks[0].clone())),
+ (3, Some(code_blocks[0].clone())),
+ (4, Some(code_blocks[0].clone())),
+ (5, None),
+ (6, None),
+ (7, None),
+ (8, Some(code_blocks[1].clone())),
+ (9, Some(code_blocks[1].clone())),
+ (10, Some(code_blocks[1].clone())),
+ (11, None),
+ (12, Some(code_blocks[2].clone())),
+ (13, Some(code_blocks[2].clone())),
+ (14, Some(code_blocks[2].clone())),
+ (15, None),
+ (16, Some(code_blocks[3].clone())),
+ (17, Some(code_blocks[3].clone())),
+ (18, Some(code_blocks[3].clone())),
+ (19, None),
+ ];
+
+ for (row, expected) in expected_results {
+ let offset = snapshot.point_to_offset(Point::new(row, 0));
+ let range = find_surrounding_code_block(&snapshot, offset);
+ assert_eq!(range, expected, "unexpected result on row {:?}", row);
+ }
+ }
+}
@@ -0,0 +1,249 @@
+use std::sync::Arc;
+
+use gpui::{
+ AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, View, WeakView,
+};
+use picker::{Picker, PickerDelegate};
+use project::Project;
+use ui::utils::{format_distance_from_now, DateTimeType};
+use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
+use workspace::Item;
+
+use crate::context_editor::DEFAULT_TAB_TITLE;
+use crate::{AssistantPanel, ContextStore, RemoteContextMetadata, SavedContextMetadata};
+
+#[derive(Clone)]
+pub enum ContextMetadata {
+ Remote(RemoteContextMetadata),
+ Saved(SavedContextMetadata),
+}
+
+enum SavedContextPickerEvent {
+ Confirmed(ContextMetadata),
+}
+
+pub struct ContextHistory {
+ picker: View<Picker<SavedContextPickerDelegate>>,
+ _subscriptions: Vec<Subscription>,
+ assistant_panel: WeakView<AssistantPanel>,
+}
+
+impl ContextHistory {
+ pub fn new(
+ project: Model<Project>,
+ context_store: Model<ContextStore>,
+ assistant_panel: WeakView<AssistantPanel>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let picker = cx.new_view(|cx| {
+ Picker::uniform_list(
+ SavedContextPickerDelegate::new(project, context_store.clone()),
+ cx,
+ )
+ .modal(false)
+ .max_height(None)
+ });
+
+ let _subscriptions = vec![
+ cx.observe(&context_store, |this, _, cx| {
+ this.picker.update(cx, |picker, cx| picker.refresh(cx));
+ }),
+ cx.subscribe(&picker, Self::handle_picker_event),
+ ];
+
+ Self {
+ picker,
+ _subscriptions,
+ assistant_panel,
+ }
+ }
+
+ fn handle_picker_event(
+ &mut self,
+ _: View<Picker<SavedContextPickerDelegate>>,
+ event: &SavedContextPickerEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let SavedContextPickerEvent::Confirmed(context) = event;
+ self.assistant_panel
+ .update(cx, |assistant_panel, cx| match context {
+ ContextMetadata::Remote(metadata) => {
+ assistant_panel
+ .open_remote_context(metadata.id.clone(), cx)
+ .detach_and_log_err(cx);
+ }
+ ContextMetadata::Saved(metadata) => {
+ assistant_panel
+ .open_saved_context(metadata.path.clone(), cx)
+ .detach_and_log_err(cx);
+ }
+ })
+ .ok();
+ }
+}
+
+impl Render for ContextHistory {
+ fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
+ div().size_full().child(self.picker.clone())
+ }
+}
+
+impl FocusableView for ContextHistory {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<()> for ContextHistory {}
+
+impl Item for ContextHistory {
+ type Event = ();
+
+ fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
+ Some("History".into())
+ }
+}
+
+struct SavedContextPickerDelegate {
+ store: Model<ContextStore>,
+ project: Model<Project>,
+ matches: Vec<ContextMetadata>,
+ selected_index: usize,
+}
+
+impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
+
+impl SavedContextPickerDelegate {
+ fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
+ Self {
+ project,
+ store,
+ matches: Vec::new(),
+ selected_index: 0,
+ }
+ }
+}
+
+impl PickerDelegate for SavedContextPickerDelegate {
+ type ListItem = ListItem;
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix;
+ }
+
+ fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+ "Search...".into()
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+ let search = self.store.read(cx).search(query, cx);
+ cx.spawn(|this, mut cx| async move {
+ let matches = search.await;
+ this.update(&mut cx, |this, cx| {
+ let host_contexts = this.delegate.store.read(cx).host_contexts();
+ this.delegate.matches = host_contexts
+ .iter()
+ .cloned()
+ .map(ContextMetadata::Remote)
+ .chain(matches.into_iter().map(ContextMetadata::Saved))
+ .collect();
+ this.delegate.selected_index = 0;
+ cx.notify();
+ })
+ .ok();
+ })
+ }
+
+ fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
+ if let Some(metadata) = self.matches.get(self.selected_index) {
+ cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
+ }
+ }
+
+ fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let context = self.matches.get(ix)?;
+ let item = match context {
+ ContextMetadata::Remote(context) => {
+ let host_user = self.project.read(cx).host().and_then(|collaborator| {
+ self.project
+ .read(cx)
+ .user_store()
+ .read(cx)
+ .get_cached_user(collaborator.user_id)
+ });
+ div()
+ .flex()
+ .w_full()
+ .justify_between()
+ .gap_2()
+ .child(
+ h_flex().flex_1().overflow_x_hidden().child(
+ Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
+ .size(LabelSize::Small),
+ ),
+ )
+ .child(
+ h_flex()
+ .gap_2()
+ .children(if let Some(host_user) = host_user {
+ vec![
+ Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
+ Label::new(format!("Shared by @{}", host_user.github_login))
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .into_any_element(),
+ ]
+ } else {
+ vec![Label::new("Shared by host")
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .into_any_element()]
+ }),
+ )
+ }
+ ContextMetadata::Saved(context) => div()
+ .flex()
+ .w_full()
+ .justify_between()
+ .gap_2()
+ .child(
+ h_flex()
+ .flex_1()
+ .child(Label::new(context.title.clone()).size(LabelSize::Small))
+ .overflow_x_hidden(),
+ )
+ .child(
+ Label::new(format_distance_from_now(
+ DateTimeType::Local(context.mtime),
+ false,
+ true,
+ true,
+ ))
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ ),
+ };
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(item),
+ )
+ }
+}
@@ -1,4 +1,4 @@
-use crate::assistant_panel::ContextEditor;
+use crate::context_editor::ContextEditor;
use anyhow::Result;
use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::SlashCommand;
@@ -5,7 +5,7 @@ use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
-use crate::assistant_panel::ContextEditor;
+use crate::context_editor::ContextEditor;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {