From d135ec2b73b6ce4fb48a91147cda3780fb4418f4 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 16 Dec 2024 11:02:54 +0100 Subject: [PATCH 01/32] completions: Restore tab behavior when both visible (#22069) This reverts part of #21858 by changing how `tab` works again: - If both, completions and inline completions, are visible, then `tab` accepts the completion and `shif-tab` the inline completion. - If only one of them is shown, then `tab` accepts it. I'm not a fan of this solution, but I think it's a short-term fix that avoids breaking people's `tab` muscle memory. Release Notes: - (These release notes invalidate the release notes contained in: https://github.com/zed-industries/zed/pull/21858) - Changed how inline completions (Copilot, Supermaven, ...) and normal completions (from language servers) interact. Zed will now also show inline completions when the completion menu is visible. The user can accept the inline completion with `` and the active entry in the completion menu with ``. --- assets/keymaps/default-linux.json | 13 ++++++++----- assets/keymaps/default-macos.json | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index f93c459ec6985e..5c300e82883160 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -468,21 +468,24 @@ } }, { - "context": "Editor && showing_completions", + "context": "Editor && !inline_completion && showing_completions", "use_key_equivalents": true, "bindings": { - "enter": "editor::ConfirmCompletion" + "enter": "editor::ConfirmCompletion", + "tab": "editor::ComposeCompletion" } }, { - "context": "Editor && !inline_completion && showing_completions", + "context": "Editor && inline_completion && showing_completions", "use_key_equivalents": true, "bindings": { - "tab": "editor::ComposeCompletion" + "enter": "editor::ConfirmCompletion", + "tab": "editor::ComposeCompletion", + "shift-tab": "editor::AcceptInlineCompletion" } }, { - "context": "Editor && inline_completion", + "context": "Editor && inline_completion && !showing_completions", "use_key_equivalents": true, "bindings": { "tab": "editor::AcceptInlineCompletion" diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index f821bc982d3ac0..a3f35dccdd4e78 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -539,21 +539,24 @@ } }, { - "context": "Editor && showing_completions", + "context": "Editor && !inline_completion && showing_completions", "use_key_equivalents": true, "bindings": { - "enter": "editor::ConfirmCompletion" + "enter": "editor::ConfirmCompletion", + "tab": "editor::ComposeCompletion" } }, { - "context": "Editor && !inline_completion && showing_completions", + "context": "Editor && inline_completion && showing_completions", "use_key_equivalents": true, "bindings": { - "tab": "editor::ComposeCompletion" + "enter": "editor::ConfirmCompletion", + "tab": "editor::ComposeCompletion", + "shift-tab": "editor::AcceptInlineCompletion" } }, { - "context": "Editor && inline_completion", + "context": "Editor && inline_completion && !showing_completions", "use_key_equivalents": true, "bindings": { "tab": "editor::AcceptInlineCompletion" From 040d9ae2229c64557feecabc22a89f0cf23a7260 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 16 Dec 2024 11:22:20 +0100 Subject: [PATCH 02/32] zeta: Prevent diff popover from going offscreen (#22070) https://github.com/user-attachments/assets/4ce806f1-d790-41d0-9825-e68055281446 Release Notes: - N/A --- crates/editor/src/element.rs | 43 ++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d36885459ad53b..eafa2938994776 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2794,7 +2794,7 @@ impl EditorElement { style: &EditorStyle, cx: &mut WindowContext, ) -> Option { - const PADDING_X: Pixels = Pixels(25.); + const PADDING_X: Pixels = Pixels(24.); const PADDING_Y: Pixels = Pixels(2.); let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?; @@ -2897,6 +2897,7 @@ impl EditorElement { } let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx); + let line_count = text.lines().count() + 1; let longest_row = editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); @@ -2914,7 +2915,8 @@ impl EditorElement { .width }; - let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights); + let styled_text = + gpui::StyledText::new(text).with_highlights(&style.text, highlights); let mut element = div() .bg(cx.theme().colors().editor_background) @@ -2922,15 +2924,38 @@ impl EditorElement { .border_color(cx.theme().colors().border) .rounded_md() .px_1() - .child(text) + .child(styled_text) .into_any(); - let origin = text_bounds.origin - + point( - longest_line_width + PADDING_X - scroll_pixel_position.x, - edit_start.row().as_f32() * line_height - scroll_pixel_position.y, - ); - element.prepaint_as_root(origin, AvailableSpace::min_size(), cx); + let element_bounds = element.layout_as_root(AvailableSpace::min_size(), cx); + let is_fully_visible = + editor_width >= longest_line_width + PADDING_X + element_bounds.width; + + let origin = if is_fully_visible { + text_bounds.origin + + point( + longest_line_width + PADDING_X - scroll_pixel_position.x, + edit_start.row().as_f32() * line_height - scroll_pixel_position.y, + ) + } else { + let target_above = + DisplayRow(edit_start.row().0.saturating_sub(line_count as u32)); + let row_target = if visible_row_range + .contains(&DisplayRow(target_above.0.saturating_sub(1))) + { + target_above + } else { + DisplayRow(edit_end.row().0 + 1) + }; + + text_bounds.origin + + point( + -scroll_pixel_position.x, + row_target.as_f32() * line_height - scroll_pixel_position.y, + ) + }; + + element.prepaint_as_root(origin, element_bounds.into(), cx); Some(element) } } From 38c0aa303e8c58238bceea9ee719ef2716158e81 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 16 Dec 2024 11:23:20 +0100 Subject: [PATCH 03/32] vim: Don't dismiss inline completion when switching to normal mode (#22075) I'm not sure about this yet. On one hand: it's nice that the completion doesn't just disappear when I hit escape because I was typing and in the flow. On the other hand: no other inline completion provider keeps the suggestion when leaving insert mode. I'm going to merge this so we can get it into nightly and try it out for the next couple of days. cc @ConradIrwin Release Notes: - vim: Do not dismiss inline completions when leaving insert/replace mode with ``. --- crates/editor/src/editor.rs | 7 +++++-- crates/vim/src/insert.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1ab978f3566e0c..f0a4320c66ad56 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2429,7 +2429,7 @@ impl Editor { cx.notify(); return; } - if self.dismiss_menus_and_popups(true, cx) { + if self.dismiss_menus_and_popups(false, true, cx) { return; } @@ -2444,6 +2444,7 @@ impl Editor { pub fn dismiss_menus_and_popups( &mut self, + keep_inline_completion: bool, should_report_inline_completion_event: bool, cx: &mut ViewContext, ) -> bool { @@ -2467,7 +2468,9 @@ impl Editor { return true; } - if self.discard_inline_completion(should_report_inline_completion_event, cx) { + if !keep_inline_completion + && self.discard_inline_completion(should_report_inline_completion_event, cx) + { return true; } diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index b1e7af9b105794..061f96a46e5b4f 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -22,7 +22,7 @@ impl Vim { if count <= 1 || Vim::globals(cx).dot_replaying { self.create_mark("^".into(), false, cx); self.update_editor(cx, |_, editor, cx| { - editor.dismiss_menus_and_popups(false, cx); + editor.dismiss_menus_and_popups(true, false, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, mut cursor, _| { *cursor.column_mut() = cursor.column().saturating_sub(1); From 62b3acee5fcc454fdad46eff87a44d0c083bdfbe Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Mon, 16 Dec 2024 18:35:54 +0530 Subject: [PATCH 04/32] Project panel: Deselect entries on remaining blank space click + Remove hover color for selected entries (#22073) Closes #22072 Clicking on the remaining space now allows a single click to deselect all selected items. Check the issue for a preview of the current state and how it works in VSCode. Bonus: I found the hover color on selected items to be distracting. When I have many entries selected and hover over them, it becomes hard to tell if a particular entry is selected while the mouse pointer is on it. This PR removes hover coloring for selected entries, mimicking how VSCode handles it. This PR: zed Release Notes: - Clicking on empty space in the Project Panel now deselects all selected items. --- crates/project_panel/src/project_panel.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2714e70c6e949c..1f4fd50f41051c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3224,7 +3224,9 @@ impl ProjectPanel { .border_1() .border_r_2() .border_color(border_color) - .hover(|style| style.bg(bg_hover_color)) + .when(!is_marked && !is_active, |div| { + div.hover(|style| style.bg(bg_hover_color)) + }) .when(is_local, |div| { div.on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, cx| { @@ -3897,6 +3899,11 @@ impl Render for ProjectPanel { this.hide_scrollbar(cx); } })) + .on_click(cx.listener(|this, _event, cx| { + cx.stop_propagation(); + this.selection = None; + this.marked_entries.clear(); + })) .key_context(self.dispatch_context(cx)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_prev)) From ce727fbc0758cc0176d1c854b96599e6e8ed40ae Mon Sep 17 00:00:00 2001 From: Helge Mahrt <5497139+helgemahrt@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:27:14 +0100 Subject: [PATCH 05/32] workspace: Fix doc comments (#22063) Happened to see that the doc comments here were not correct while implementing something else. Release Notes: - N/A --- crates/workspace/src/workspace_settings.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 118287cc837c46..cd1aab7f247906 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -87,15 +87,15 @@ pub enum RestoreOnStartupBehavior { pub struct WorkspaceSettingsContent { /// Active pane styling settings. pub active_pane_modifiers: Option, - // Direction to split horizontally. - // - // Default: "up" + /// Direction to split horizontally. + /// + /// Default: "up" pub pane_split_direction_horizontal: Option, - // Direction to split vertically. - // - // Default: "left" + /// Direction to split vertically. + /// + /// Default: "left" pub pane_split_direction_vertical: Option, - // Centered layout related settings. + /// Centered layout related settings. pub centered_layout: Option, /// Whether or not to prompt the user to confirm before closing the application. /// From ea012075fc8bc80a4b5804e73b0e65887a0b23b3 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 16 Dec 2024 15:10:33 +0100 Subject: [PATCH 06/32] Trigger completions even if inline completion is visible (#22077) This is related to #22069 and #21858: before both of these PRs, we would only ever show inline completions OR completions, never both at the same time. Now we show both at the same, but we still had this piece of logic here, that prevented non-inline completions from showing up if there was already an inline completion. With this change, it's possible to get LSP completions without having to dismiss inline completions before. Release Notes: - Inline completions (Copilot, Supermaven, ...) don't stop other completions from showing up anymore. Both can now be visible at the same time. --------- Co-authored-by: Bennet --- crates/copilot/src/copilot_completion_provider.rs | 11 +++++++++++ crates/editor/src/editor.rs | 4 +--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 949e2178a6f76d..730401adc77c21 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -373,6 +373,17 @@ mod tests { // Ensure existing inline completion is interpolated when inserting again. cx.simulate_keystroke("c"); + // We still request a normal LSP completion, but we interpolate the + // existing inline completion. + drop(handle_completion_request( + &mut cx, + indoc! {" + one.c|<> + two + three + "}, + vec!["ompletion_a", "ompletion_b"], + )); executor.run_until_parked(); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f0a4320c66ad56..0979f1cc7018ee 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2843,7 +2843,6 @@ impl Editor { ); } - let had_active_inline_completion = this.has_active_inline_completion(); this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| { s.select(new_selections) }); @@ -2864,8 +2863,7 @@ impl Editor { this.show_signature_help(&ShowSignatureHelp, cx); } - let trigger_in_words = !had_active_inline_completion; - this.trigger_completion_on_input(&text, trigger_in_words, cx); + this.trigger_completion_on_input(&text, true, cx); linked_editing_ranges::refresh_linked_ranges(this, cx); this.refresh_inline_completion(true, false, cx); }); From bc113e4b5176fbced7f7ffbd8fc11143cdbb834f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 16 Dec 2024 16:15:58 +0200 Subject: [PATCH 07/32] Move task centering code closer to user input (#22082) Follow-up of https://github.com/zed-industries/zed/pull/22004 * Reuse center terminals for tasks, when requested * Extend task templates with `RevealTarget`, moving it from `TaskSpawnTarget` into the core library * Use `reveal_target` instead of `target` to avoid misinterpretations in the task template context * Do not expose `SpawnInTerminal` to user interface, avoid it implementing `Serialize` and `Deserialize` * Remove `NewCenterTask` action, extending `task::Spawn` interface instead * Do not require any extra unrelated parameters during task resolution, instead, use task overrides on the resolved tasks on the modal side * Add keybindings for opening the task modal in the `RevealTarget::Center` mode Release Notes: - N/A --- Cargo.lock | 2 +- assets/keymaps/default-linux.json | 5 +- assets/keymaps/default-macos.json | 5 +- assets/settings/initial_tasks.json | 10 +- crates/editor/src/editor.rs | 2 +- crates/project/src/task_inventory.rs | 45 ++--- crates/task/src/lib.rs | 19 +- crates/task/src/task_template.rs | 49 +++-- crates/tasks_ui/src/lib.rs | 57 ++++-- crates/tasks_ui/src/modal.rs | 64 ++++-- crates/terminal_view/src/terminal_panel.rs | 218 +++++++++++++++------ crates/terminal_view/src/terminal_view.rs | 94 +-------- crates/workspace/Cargo.toml | 1 - crates/workspace/src/tasks.rs | 22 +-- crates/zed_actions/Cargo.toml | 1 + crates/zed_actions/src/lib.rs | 41 ++-- docs/src/tasks.md | 6 +- 17 files changed, 356 insertions(+), 285 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80b4203938bdbf..01b2f33866392f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15603,7 +15603,6 @@ dependencies = [ "ui", "util", "uuid", - "zed_actions", ] [[package]] @@ -16108,6 +16107,7 @@ name = "zed_actions" version = "0.1.0" dependencies = [ "gpui", + "schemars", "serde", ] diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 5c300e82883160..3de58a5d9d39da 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -426,7 +426,10 @@ "ctrl-shift-r": "task::Rerun", "ctrl-alt-r": "task::Rerun", "alt-t": "task::Rerun", - "alt-shift-t": "task::Spawn" + "alt-shift-t": "task::Spawn", + "alt-shift-r": ["task::Spawn", { "reveal_target": "center" }] + // also possible to spawn tasks by name: + // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] } }, // Bindings from Sublime Text diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a3f35dccdd4e78..321aa283690c07 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -495,8 +495,9 @@ "bindings": { "cmd-shift-r": "task::Spawn", "cmd-alt-r": "task::Rerun", - "alt-t": "task::Spawn", - "alt-shift-t": "task::Spawn" + "ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }] + // also possible to spawn tasks by name: + // "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] } }, // Bindings from Sublime Text diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 31808ac6324c05..a49b83402063d6 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -15,10 +15,14 @@ // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. "allow_concurrent_runs": false, // What to do with the terminal pane and tab, after the command was started: - // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) - // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it - // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there + // * `always` — always show the task's pane, and focus the corresponding tab in it (default) + // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it + // * `never` — do not alter focus, but still add/reuse the task's tab in its pane "reveal": "always", + // Where to place the task's terminal item after starting the task: + // * `dock` — in the terminal dock, "regular" terminal items' place (default) + // * `center` — in the central pane group, "main" editor area + "reveal_target": "dock", // What to do with the terminal pane and tab, after the command had finished: // * `never` — Do nothing when the command finishes (default) // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0979f1cc7018ee..2bda8878754510 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -522,7 +522,7 @@ impl RunnableTasks { ) -> impl Iterator + 'a { self.templates.iter().filter_map(|(kind, template)| { template - .resolve_task(&kind.to_id_base(), Default::default(), cx) + .resolve_task(&kind.to_id_base(), cx) .map(|task| (kind.clone(), task)) }) } diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index c70a72f0e14818..2a31710df6aee1 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -184,7 +184,7 @@ impl Inventory { let id_base = kind.to_id_base(); Some(( kind, - task.resolve_task(&id_base, Default::default(), task_context)?, + task.resolve_task(&id_base, task_context)?, not_used_score, )) }) @@ -378,7 +378,7 @@ mod test_inventory { use crate::Inventory; - use super::{task_source_kind_preference, TaskSourceKind}; + use super::TaskSourceKind; pub(super) fn task_template_names( inventory: &Model, @@ -409,7 +409,7 @@ mod test_inventory { let id_base = task_source_kind.to_id_base(); inventory.task_scheduled( task_source_kind.clone(), - task.resolve_task(&id_base, Default::default(), &TaskContext::default()) + task.resolve_task(&id_base, &TaskContext::default()) .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")), ); }); @@ -427,31 +427,12 @@ mod test_inventory { .into_iter() .filter_map(|(source_kind, task)| { let id_base = source_kind.to_id_base(); - Some(( - source_kind, - task.resolve_task(&id_base, Default::default(), task_context)?, - )) + Some((source_kind, task.resolve_task(&id_base, task_context)?)) }) .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label)) .collect() }) } - - pub(super) async fn list_tasks_sorted_by_last_used( - inventory: &Model, - worktree: Option, - cx: &mut TestAppContext, - ) -> Vec<(TaskSourceKind, String)> { - let (used, current) = inventory.update(cx, |inventory, cx| { - inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx) - }); - let mut all = used; - all.extend(current); - all.into_iter() - .map(|(source_kind, task)| (source_kind, task.resolved_label)) - .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone())) - .collect() - } } /// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file. @@ -877,7 +858,7 @@ mod tests { TaskStore::init(None); } - pub(super) async fn resolved_task_names( + async fn resolved_task_names( inventory: &Model, worktree: Option, cx: &mut TestAppContext, @@ -905,4 +886,20 @@ mod tests { )) .unwrap() } + + async fn list_tasks_sorted_by_last_used( + inventory: &Model, + worktree: Option, + cx: &mut TestAppContext, + ) -> Vec<(TaskSourceKind, String)> { + let (used, current) = inventory.update(cx, |inventory, cx| { + inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx) + }); + let mut all = used; + all.extend(current); + all.into_iter() + .map(|(source_kind, task)| (source_kind, task.resolved_label)) + .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone())) + .collect() + } } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index c5ad843679fbdf..7b81ae078cae7a 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -15,14 +15,15 @@ use std::str::FromStr; pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates}; pub use vscode_format::VsCodeTaskFile; +pub use zed_actions::RevealTarget; /// Task identifier, unique within the application. /// Based on it, task reruns and terminal tabs are managed. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] pub struct TaskId(pub String); /// Contains all information needed by Zed to spawn a new terminal tab for the given task. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SpawnInTerminal { /// Id of the task to use when determining task tab affinity. pub id: TaskId, @@ -47,6 +48,8 @@ pub struct SpawnInTerminal { pub allow_concurrent_runs: bool, /// What to do with the terminal pane and tab, after the command was started. pub reveal: RevealStrategy, + /// Where to show tasks' terminal output. + pub reveal_target: RevealTarget, /// What to do with the terminal pane and tab, after the command had finished. pub hide: HideStrategy, /// Which shell to use when spawning the task. @@ -57,15 +60,6 @@ pub struct SpawnInTerminal { pub show_command: bool, } -/// An action for spawning a specific task -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct NewCenterTask { - /// The specification of the task to spawn. - pub action: SpawnInTerminal, -} - -gpui::impl_actions!(tasks, [NewCenterTask]); - /// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResolvedTask { @@ -84,9 +78,6 @@ pub struct ResolvedTask { /// Further actions that need to take place after the resolved task is spawned, /// with all task variables resolved. pub resolved: Option, - - /// where to sawn the task in the UI, either in the terminal panel or in the center pane - pub target: zed_actions::TaskSpawnTarget, } impl ResolvedTask { diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index c2a4e1878b3a69..a4a02494e511c5 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -9,7 +9,7 @@ use sha2::{Digest, Sha256}; use util::{truncate_and_remove_front, ResultExt}; use crate::{ - ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName, + ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName, ZED_VARIABLE_NAME_PREFIX, }; @@ -42,10 +42,16 @@ pub struct TaskTemplate { #[serde(default)] pub allow_concurrent_runs: bool, /// What to do with the terminal pane and tab, after the command was started: - /// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) - /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there + /// * `always` — always show the task's pane, and focus the corresponding tab in it (default) + // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it + // * `never` — do not alter focus, but still add/reuse the task's tab in its pane #[serde(default)] pub reveal: RevealStrategy, + /// Where to place the task's terminal item after starting the task. + /// * `dock` — in the terminal dock, "regular" terminal items' place (default). + /// * `center` — in the central pane group, "main" editor area. + #[serde(default)] + pub reveal_target: RevealTarget, /// What to do with the terminal pane and tab, after the command had finished: /// * `never` — do nothing when the command finishes (default) /// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it @@ -70,12 +76,12 @@ pub struct TaskTemplate { #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum RevealStrategy { - /// Always show the terminal pane, add and focus the corresponding task's tab in it. + /// Always show the task's pane, and focus the corresponding tab in it. #[default] Always, - /// Always show the terminal pane, add the task's tab in it, but don't focus it. + /// Always show the task's pane, add the task's tab in it, but don't focus it. NoFocus, - /// Do not change terminal pane focus, but still add/reuse the task's tab there. + /// Do not alter focus, but still add/reuse the task's tab in its pane. Never, } @@ -115,12 +121,7 @@ impl TaskTemplate { /// /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources), /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details. - pub fn resolve_task( - &self, - id_base: &str, - target: zed_actions::TaskSpawnTarget, - cx: &TaskContext, - ) -> Option { + pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option { if self.label.trim().is_empty() || self.command.trim().is_empty() { return None; } @@ -219,7 +220,6 @@ impl TaskTemplate { Some(ResolvedTask { id: id.clone(), substituted_variables, - target, original_task: self.clone(), resolved_label: full_label.clone(), resolved: Some(SpawnInTerminal { @@ -241,6 +241,7 @@ impl TaskTemplate { use_new_terminal: self.use_new_terminal, allow_concurrent_runs: self.allow_concurrent_runs, reveal: self.reveal, + reveal_target: self.reveal_target, hide: self.hide, shell: self.shell.clone(), show_summary: self.show_summary, @@ -388,7 +389,7 @@ mod tests { }, ] { assert_eq!( - task_with_blank_property.resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()), + task_with_blank_property.resolve_task(TEST_ID_BASE, &TaskContext::default()), None, "should not resolve task with blank label and/or command: {task_with_blank_property:?}" ); @@ -406,7 +407,7 @@ mod tests { let resolved_task = |task_template: &TaskTemplate, task_cx| { let resolved_task = task_template - .resolve_task(TEST_ID_BASE, Default::default(), task_cx) + .resolve_task(TEST_ID_BASE, task_cx) .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}")); assert_substituted_variables(&resolved_task, Vec::new()); resolved_task @@ -532,7 +533,6 @@ mod tests { for i in 0..15 { let resolved_task = task_with_all_variables.resolve_task( TEST_ID_BASE, - Default::default(), &TaskContext { cwd: None, task_variables: TaskVariables::from_iter(all_variables.clone()), @@ -621,7 +621,6 @@ mod tests { let removed_variable = not_all_variables.remove(i); let resolved_task_attempt = task_with_all_variables.resolve_task( TEST_ID_BASE, - Default::default(), &TaskContext { cwd: None, task_variables: TaskVariables::from_iter(not_all_variables), @@ -638,10 +637,10 @@ mod tests { label: "My task".into(), command: "echo".into(), args: vec!["$PATH".into()], - ..Default::default() + ..TaskTemplate::default() }; let resolved_task = task - .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) + .resolve_task(TEST_ID_BASE, &TaskContext::default()) .unwrap(); assert_substituted_variables(&resolved_task, Vec::new()); let resolved = resolved_task.resolved.unwrap(); @@ -656,10 +655,10 @@ mod tests { label: "My task".into(), command: "echo".into(), args: vec!["$ZED_VARIABLE".into()], - ..Default::default() + ..TaskTemplate::default() }; assert!(task - .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) + .resolve_task(TEST_ID_BASE, &TaskContext::default()) .is_none()); } @@ -709,7 +708,7 @@ mod tests { .enumerate() { let resolved = symbol_dependent_task - .resolve_task(TEST_ID_BASE, Default::default(), &cx) + .resolve_task(TEST_ID_BASE, &cx) .unwrap_or_else(|| panic!("Failed to resolve task {symbol_dependent_task:?}")); assert_eq!( resolved.substituted_variables, @@ -751,9 +750,7 @@ mod tests { context .task_variables .insert(VariableName::Symbol, "my-symbol".to_string()); - assert!(faulty_go_test - .resolve_task("base", Default::default(), &context) - .is_some()); + assert!(faulty_go_test.resolve_task("base", &context).is_some()); } #[test] @@ -812,7 +809,7 @@ mod tests { }; let resolved = template - .resolve_task(TEST_ID_BASE, Default::default(), &context) + .resolve_task(TEST_ID_BASE, &context) .unwrap() .resolved .unwrap(); diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 0d278bc2f4c6f6..8616b4266a39bb 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -1,9 +1,9 @@ use ::settings::Settings; use editor::{tasks::task_context, Editor}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; -use modal::TasksModal; +use modal::{TaskOverrides, TasksModal}; use project::{Location, WorktreeId}; -use task::TaskId; +use task::{RevealTarget, TaskId}; use workspace::tasks::schedule_task; use workspace::{tasks::schedule_resolved_task, Workspace}; @@ -11,7 +11,6 @@ mod modal; mod settings; pub use modal::{Rerun, Spawn}; -use zed_actions::TaskSpawnTarget; pub fn init(cx: &mut AppContext) { settings::TaskSettings::register(cx); @@ -54,7 +53,6 @@ pub fn init(cx: &mut AppContext) { task_source_kind, &original_task, &task_context, - Default::default(), false, cx, ) @@ -81,7 +79,7 @@ pub fn init(cx: &mut AppContext) { ); } } else { - toggle_modal(workspace, cx).detach(); + toggle_modal(workspace, None, cx).detach(); }; }); }, @@ -90,14 +88,25 @@ pub fn init(cx: &mut AppContext) { } fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext) { - match &action.task_name { - Some(name) => spawn_task_with_name(name.clone(), action.target.unwrap_or_default(), cx) - .detach_and_log_err(cx), - None => toggle_modal(workspace, cx).detach(), + match action { + Spawn::ByName { + task_name, + reveal_target, + } => { + let overrides = reveal_target.map(|reveal_target| TaskOverrides { + reveal_target: Some(reveal_target), + }); + spawn_task_with_name(task_name.clone(), overrides, cx).detach_and_log_err(cx) + } + Spawn::ViaModal { reveal_target } => toggle_modal(workspace, *reveal_target, cx).detach(), } } -fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> { +fn toggle_modal( + workspace: &mut Workspace, + reveal_target: Option, + cx: &mut ViewContext<'_, Workspace>, +) -> AsyncTask<()> { let task_store = workspace.project().read(cx).task_store().clone(); let workspace_handle = workspace.weak_handle(); let can_open_modal = workspace.project().update(cx, |project, cx| { @@ -110,7 +119,15 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) workspace .update(&mut cx, |workspace, cx| { workspace.toggle_modal(cx, |cx| { - TasksModal::new(task_store.clone(), task_context, workspace_handle, cx) + TasksModal::new( + task_store.clone(), + task_context, + reveal_target.map(|target| TaskOverrides { + reveal_target: Some(target), + }), + workspace_handle, + cx, + ) }) }) .ok(); @@ -122,7 +139,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) fn spawn_task_with_name( name: String, - task_target: TaskSpawnTarget, + overrides: Option, cx: &mut ViewContext, ) -> AsyncTask> { cx.spawn(|workspace, mut cx| async move { @@ -157,14 +174,18 @@ fn spawn_task_with_name( let did_spawn = workspace .update(&mut cx, |workspace, cx| { - let (task_source_kind, target_task) = + let (task_source_kind, mut target_task) = tasks.into_iter().find(|(_, task)| task.label == name)?; + if let Some(overrides) = &overrides { + if let Some(target_override) = overrides.reveal_target { + target_task.reveal_target = target_override; + } + } schedule_task( workspace, task_source_kind, &target_task, &task_context, - task_target, false, cx, ); @@ -174,7 +195,13 @@ fn spawn_task_with_name( if !did_spawn { workspace .update(&mut cx, |workspace, cx| { - spawn_task_or_modal(workspace, &Spawn::default(), cx); + spawn_task_or_modal( + workspace, + &Spawn::ViaModal { + reveal_target: overrides.and_then(|overrides| overrides.reveal_target), + }, + cx, + ); }) .ok(); } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 521115cf5f3567..5595feaca7255a 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -9,7 +9,7 @@ use gpui::{ }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{task_store::TaskStore, TaskSourceKind}; -use task::{ResolvedTask, TaskContext, TaskTemplate}; +use task::{ResolvedTask, RevealTarget, TaskContext, TaskTemplate}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, @@ -24,6 +24,7 @@ pub use zed_actions::{Rerun, Spawn}; pub(crate) struct TasksModalDelegate { task_store: Model, candidates: Option>, + task_overrides: Option, last_used_candidate_index: Option, divider_index: Option, matches: Vec, @@ -34,12 +35,28 @@ pub(crate) struct TasksModalDelegate { placeholder_text: Arc, } +/// Task template amendments to do before resolving the context. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct TaskOverrides { + /// See [`RevealTarget`]. + pub(crate) reveal_target: Option, +} + impl TasksModalDelegate { fn new( task_store: Model, task_context: TaskContext, + task_overrides: Option, workspace: WeakView, ) -> Self { + let placeholder_text = if let Some(TaskOverrides { + reveal_target: Some(RevealTarget::Center), + }) = &task_overrides + { + Arc::from("Find a task, or run a command in the central pane") + } else { + Arc::from("Find a task, or run a command") + }; Self { task_store, workspace, @@ -50,7 +67,8 @@ impl TasksModalDelegate { selected_index: 0, prompt: String::default(), task_context, - placeholder_text: Arc::from("Find a task, or run a command"), + task_overrides, + placeholder_text, } } @@ -61,14 +79,20 @@ impl TasksModalDelegate { let source_kind = TaskSourceKind::UserInput; let id_base = source_kind.to_id_base(); - let new_oneshot = TaskTemplate { + let mut new_oneshot = TaskTemplate { label: self.prompt.clone(), command: self.prompt.clone(), ..TaskTemplate::default() }; + if let Some(TaskOverrides { + reveal_target: Some(reveal_target), + }) = &self.task_overrides + { + new_oneshot.reveal_target = *reveal_target; + } Some(( source_kind, - new_oneshot.resolve_task(&id_base, Default::default(), &self.task_context)?, + new_oneshot.resolve_task(&id_base, &self.task_context)?, )) } @@ -100,12 +124,13 @@ impl TasksModal { pub(crate) fn new( task_store: Model, task_context: TaskContext, + task_overrides: Option, workspace: WeakView, cx: &mut ViewContext, ) -> Self { let picker = cx.new_view(|cx| { Picker::uniform_list( - TasksModalDelegate::new(task_store, task_context, workspace), + TasksModalDelegate::new(task_store, task_context, task_overrides, workspace), cx, ) }); @@ -257,9 +282,17 @@ impl PickerDelegate for TasksModalDelegate { .as_ref() .map(|candidates| candidates[ix].clone()) }); - let Some((task_source_kind, task)) = task else { + let Some((task_source_kind, mut task)) = task else { return; }; + if let Some(TaskOverrides { + reveal_target: Some(reveal_target), + }) = &self.task_overrides + { + if let Some(resolved_task) = &mut task.resolved { + resolved_task.reveal_target = *reveal_target; + } + } self.workspace .update(cx, |workspace, cx| { @@ -396,9 +429,18 @@ impl PickerDelegate for TasksModalDelegate { } fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext>) { - let Some((task_source_kind, task)) = self.spawn_oneshot() else { + let Some((task_source_kind, mut task)) = self.spawn_oneshot() else { return; }; + + if let Some(TaskOverrides { + reveal_target: Some(reveal_target), + }) = self.task_overrides + { + if let Some(resolved_task) = &mut task.resolved { + resolved_task.reveal_target = reveal_target; + } + } self.workspace .update(cx, |workspace, cx| { schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx); @@ -682,9 +724,9 @@ mod tests { "No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)" ); - cx.dispatch_action(Spawn { - task_name: Some("example task".to_string()), - target: None, + cx.dispatch_action(Spawn::ByName { + task_name: "example task".to_string(), + reveal_target: None, }); let tasks_picker = workspace.update(cx, |workspace, cx| { workspace @@ -995,7 +1037,7 @@ mod tests { workspace: &View, cx: &mut VisualTestContext, ) -> View> { - cx.dispatch_action(Spawn::default()); + cx.dispatch_action(Spawn::modal()); workspace.update(cx, |workspace, cx| { workspace .active_modal::(cx) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2bcd8feebc630c..a4f5e7df4f7dac 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -12,7 +12,7 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; use gpui::{ - actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter, + actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -20,7 +20,7 @@ use itertools::Itertools; use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use settings::Settings; -use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId}; +use task::{RevealStrategy, RevealTarget, Shell, SpawnInTerminal, TaskId}; use terminal::{ terminal_settings::{TerminalDockPosition, TerminalSettings}, Terminal, @@ -40,7 +40,7 @@ use workspace::{ SplitUp, SwapPaneInDirection, ToggleZoom, Workspace, }; -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use zed_actions::InlineAssist; const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; @@ -53,11 +53,7 @@ pub fn init(cx: &mut AppContext) { workspace.register_action(TerminalPanel::new_terminal); workspace.register_action(TerminalPanel::open_terminal); workspace.register_action(|workspace, _: &ToggleFocus, cx| { - if workspace - .panel::(cx) - .as_ref() - .is_some_and(|panel| panel.read(cx).enabled) - { + if is_enabled_in_workspace(workspace, cx) { workspace.toggle_panel_focus::(cx); } }); @@ -76,7 +72,6 @@ pub struct TerminalPanel { pending_serialization: Task>, pending_terminals_to_add: usize, deferred_tasks: HashMap>, - enabled: bool, assistant_enabled: bool, assistant_tab_bar_button: Option, } @@ -86,7 +81,6 @@ impl TerminalPanel { let project = workspace.project(); let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx); let center = PaneGroup::new(pane.clone()); - let enabled = project.read(cx).supports_terminal(cx); cx.focus_view(&pane); let terminal_panel = Self { center, @@ -98,7 +92,6 @@ impl TerminalPanel { height: None, pending_terminals_to_add: 0, deferred_tasks: HashMap::default(), - enabled, assistant_enabled: false, assistant_tab_bar_button: None, }; @@ -492,8 +485,8 @@ impl TerminalPanel { !use_new_terminal, "Should have handled 'allow_concurrent_runs && use_new_terminal' case above" ); - this.update(&mut cx, |this, cx| { - this.replace_terminal( + this.update(&mut cx, |terminal_panel, cx| { + terminal_panel.replace_terminal( spawn_task, task_pane, existing_item_index, @@ -620,7 +613,17 @@ impl TerminalPanel { cx: &mut ViewContext, ) -> Task>> { let reveal = spawn_task.reveal; - self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx) + let reveal_target = spawn_task.reveal_target; + let kind = TerminalKind::Task(spawn_task); + match reveal_target { + RevealTarget::Center => self + .workspace + .update(cx, |workspace, cx| { + Self::add_center_terminal(workspace, kind, cx) + }) + .unwrap_or_else(|e| Task::ready(Err(e))), + RevealTarget::Dock => self.add_terminal(kind, reveal, cx), + } } /// Create a new Terminal in the current working directory or the user's home directory @@ -647,24 +650,40 @@ impl TerminalPanel { label: &str, cx: &mut AppContext, ) -> Vec<(usize, View, View)> { + let Some(workspace) = self.workspace.upgrade() else { + return Vec::new(); + }; + + let pane_terminal_views = |pane: View| { + pane.read(cx) + .items() + .enumerate() + .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) + .filter_map(|(index, terminal_view)| { + let task_state = terminal_view.read(cx).terminal().read(cx).task()?; + if &task_state.full_label == label { + Some((index, terminal_view)) + } else { + None + } + }) + .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view)) + }; + self.center .panes() .into_iter() - .flat_map(|pane| { - pane.read(cx) - .items() - .enumerate() - .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) - .filter_map(|(index, terminal_view)| { - let task_state = terminal_view.read(cx).terminal().read(cx).task()?; - if &task_state.full_label == label { - Some((index, terminal_view)) - } else { - None - } - }) - .map(|(index, terminal_view)| (index, pane.clone(), terminal_view)) - }) + .cloned() + .flat_map(pane_terminal_views) + .chain( + workspace + .read(cx) + .panes() + .into_iter() + .cloned() + .flat_map(pane_terminal_views), + ) + .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id()) .collect() } @@ -680,14 +699,48 @@ impl TerminalPanel { }) } + pub fn add_center_terminal( + workspace: &mut Workspace, + kind: TerminalKind, + cx: &mut ViewContext, + ) -> Task>> { + if !is_enabled_in_workspace(workspace, cx) { + return Task::ready(Err(anyhow!( + "terminal not yet supported for remote projects" + ))); + } + let window = cx.window_handle(); + let project = workspace.project().downgrade(); + cx.spawn(move |workspace, mut cx| async move { + let terminal = project + .update(&mut cx, |project, cx| { + project.create_terminal(kind, window, cx) + })? + .await?; + + workspace.update(&mut cx, |workspace, cx| { + let view = cx.new_view(|cx| { + TerminalView::new( + terminal.clone(), + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + }); + workspace.add_item_to_active_pane(Box::new(view), None, true, cx); + })?; + Ok(terminal) + }) + } + fn add_terminal( &mut self, kind: TerminalKind, reveal_strategy: RevealStrategy, cx: &mut ViewContext, ) -> Task>> { - if !self.enabled { - return Task::ready(Err(anyhow::anyhow!( + if !self.is_enabled(cx) { + return Task::ready(Err(anyhow!( "terminal not yet supported for remote projects" ))); } @@ -786,10 +839,11 @@ impl TerminalPanel { cx: &mut ViewContext<'_, Self>, ) -> Task> { let reveal = spawn_task.reveal; + let reveal_target = spawn_task.reveal_target; let window = cx.window_handle(); let task_workspace = self.workspace.clone(); - cx.spawn(move |this, mut cx| async move { - let project = this + cx.spawn(move |terminal_panel, mut cx| async move { + let project = terminal_panel .update(&mut cx, |this, cx| { this.workspace .update(cx, |workspace, _| workspace.project().clone()) @@ -811,32 +865,68 @@ impl TerminalPanel { .ok()?; match reveal { - RevealStrategy::Always => { - this.update(&mut cx, |this, cx| { - this.activate_terminal_view(&task_pane, terminal_item_index, true, cx) - }) - .ok()?; - - cx.spawn(|mut cx| async move { + RevealStrategy::Always => match reveal_target { + RevealTarget::Center => { task_workspace - .update(&mut cx, |workspace, cx| workspace.focus_panel::(cx)) - .ok() - }) - .detach(); - } - RevealStrategy::NoFocus => { - this.update(&mut cx, |this, cx| { - this.activate_terminal_view(&task_pane, terminal_item_index, false, cx) - }) - .ok()?; + .update(&mut cx, |workspace, cx| { + workspace + .active_item(cx) + .context("retrieving active terminal item in the workspace") + .log_err()? + .focus_handle(cx) + .focus(cx); + Some(()) + }) + .ok()??; + } + RevealTarget::Dock => { + terminal_panel + .update(&mut cx, |terminal_panel, cx| { + terminal_panel.activate_terminal_view( + &task_pane, + terminal_item_index, + true, + cx, + ) + }) + .ok()?; - cx.spawn(|mut cx| async move { + cx.spawn(|mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.focus_panel::(cx)) + .ok() + }) + .detach(); + } + }, + RevealStrategy::NoFocus => match reveal_target { + RevealTarget::Center => { task_workspace - .update(&mut cx, |workspace, cx| workspace.open_panel::(cx)) - .ok() - }) - .detach(); - } + .update(&mut cx, |workspace, cx| { + workspace.active_pane().focus_handle(cx).focus(cx); + }) + .ok()?; + } + RevealTarget::Dock => { + terminal_panel + .update(&mut cx, |terminal_panel, cx| { + terminal_panel.activate_terminal_view( + &task_pane, + terminal_item_index, + false, + cx, + ) + }) + .ok()?; + + cx.spawn(|mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.open_panel::(cx)) + .ok() + }) + .detach(); + } + }, RevealStrategy::Never => {} } @@ -851,6 +941,16 @@ impl TerminalPanel { pub fn assistant_enabled(&self) -> bool { self.assistant_enabled } + + fn is_enabled(&self, cx: &WindowContext) -> bool { + self.workspace.upgrade().map_or(false, |workspace| { + is_enabled_in_workspace(workspace.read(cx), cx) + }) + } +} + +fn is_enabled_in_workspace(workspace: &Workspace, cx: &WindowContext) -> bool { + workspace.project().read(cx).supports_terminal(cx) } pub fn new_terminal_pane( @@ -1235,7 +1335,7 @@ impl Panel for TerminalPanel { return; }; - this.add_terminal(kind, RevealStrategy::Never, cx) + this.add_terminal(kind, RevealStrategy::Always, cx) .detach_and_log_err(cx) }) } @@ -1259,7 +1359,9 @@ impl Panel for TerminalPanel { } fn icon(&self, cx: &WindowContext) -> Option { - if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button { + if (self.is_enabled(cx) || !self.has_no_terminals(cx)) + && TerminalSettings::get_global(cx).button + { Some(IconName::Terminal) } else { None diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index fb46c5ae955f90..9cc7b3ccec4744 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -14,7 +14,6 @@ use gpui::{ use language::Bias; use persistence::TERMINAL_DB; use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project}; -use task::{NewCenterTask, RevealStrategy}; use terminal::{ alacritty_terminal::{ index::Point, @@ -31,7 +30,6 @@ use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip}; use util::{paths::PathWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams}, - notifications::NotifyResultExt, register_serializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace, @@ -46,7 +44,7 @@ use zed_actions::InlineAssist; use std::{ cmp, - ops::{ControlFlow, RangeInclusive}, + ops::RangeInclusive, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -81,7 +79,6 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace.register_action(TerminalView::deploy); - workspace.register_action(TerminalView::deploy_center_task); }) .detach(); } @@ -129,61 +126,6 @@ impl FocusableView for TerminalView { } impl TerminalView { - pub fn deploy_center_task( - workspace: &mut Workspace, - task: &NewCenterTask, - cx: &mut ViewContext, - ) { - let reveal_strategy: RevealStrategy = task.action.reveal; - let mut spawn_task = task.action.clone(); - - let is_local = workspace.project().read(cx).is_local(); - - if let ControlFlow::Break(_) = - TerminalPanel::fill_command(is_local, &task.action, &mut spawn_task) - { - return; - } - - let kind = TerminalKind::Task(spawn_task); - - let project = workspace.project().clone(); - let database_id = workspace.database_id(); - cx.spawn(|workspace, mut cx| async move { - let terminal = cx - .update(|cx| { - let window = cx.window_handle(); - project.update(cx, |project, cx| project.create_terminal(kind, window, cx)) - })? - .await?; - - let terminal_view = cx.new_view(|cx| { - TerminalView::new(terminal.clone(), workspace.clone(), database_id, cx) - })?; - - cx.update(|cx| { - let focus_item = match reveal_strategy { - RevealStrategy::Always => true, - RevealStrategy::Never | RevealStrategy::NoFocus => false, - }; - - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane( - Box::new(terminal_view), - None, - focus_item, - cx, - ); - })?; - - anyhow::Ok(()) - })??; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - ///Create a new Terminal in the current working directory or the user's home directory pub fn deploy( workspace: &mut Workspace, @@ -191,38 +133,8 @@ impl TerminalView { cx: &mut ViewContext, ) { let working_directory = default_working_directory(workspace, cx); - - let window = cx.window_handle(); - let project = workspace.project().downgrade(); - cx.spawn(move |workspace, mut cx| async move { - let terminal = project - .update(&mut cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(working_directory), window, cx) - }) - .ok()? - .await; - let terminal = workspace - .update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx)) - .ok() - .flatten()?; - - workspace - .update(&mut cx, |workspace, cx| { - let view = cx.new_view(|cx| { - TerminalView::new( - terminal, - workspace.weak_handle(), - workspace.database_id(), - cx, - ) - }); - workspace.add_item_to_active_pane(Box::new(view), None, true, cx); - }) - .ok(); - - Some(()) - }) - .detach() + TerminalPanel::add_center_terminal(workspace, TerminalKind::Shell(working_directory), cx) + .detach_and_log_err(cx); } pub fn new( diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 6bd2382f35b9ca..3b17ed8dabd387 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -61,7 +61,6 @@ ui.workspace = true util.workspace = true uuid.workspace = true strum.workspace = true -zed_actions.workspace = true [dev-dependencies] call = { workspace = true, features = ["test-support"] } diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 7cf6ebcae86092..33b3c1fa8045c9 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -1,8 +1,7 @@ use project::TaskSourceKind; use remote::ConnectionState; -use task::{NewCenterTask, ResolvedTask, TaskContext, TaskTemplate}; +use task::{ResolvedTask, TaskContext, TaskTemplate}; use ui::ViewContext; -use zed_actions::TaskSpawnTarget; use crate::Workspace; @@ -11,7 +10,6 @@ pub fn schedule_task( task_source_kind: TaskSourceKind, task_to_resolve: &TaskTemplate, task_cx: &TaskContext, - task_target: zed_actions::TaskSpawnTarget, omit_history: bool, cx: &mut ViewContext<'_, Workspace>, ) { @@ -29,7 +27,7 @@ pub fn schedule_task( } if let Some(spawn_in_terminal) = - task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_target, task_cx) + task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx) { schedule_resolved_task( workspace, @@ -48,7 +46,6 @@ pub fn schedule_resolved_task( omit_history: bool, cx: &mut ViewContext<'_, Workspace>, ) { - let target = resolved_task.target; if let Some(spawn_in_terminal) = resolved_task.resolved.take() { if !omit_history { resolved_task.resolved = Some(spawn_in_terminal.clone()); @@ -63,17 +60,8 @@ pub fn schedule_resolved_task( }); } - match target { - TaskSpawnTarget::Center => { - cx.dispatch_action(Box::new(NewCenterTask { - action: spawn_in_terminal, - })); - } - TaskSpawnTarget::Dock => { - cx.emit(crate::Event::SpawnTask { - action: Box::new(spawn_in_terminal), - }); - } - } + cx.emit(crate::Event::SpawnTask { + action: Box::new(spawn_in_terminal), + }); } } diff --git a/crates/zed_actions/Cargo.toml b/crates/zed_actions/Cargo.toml index ee279cde654f72..1bf26dc4f028b5 100644 --- a/crates/zed_actions/Cargo.toml +++ b/crates/zed_actions/Cargo.toml @@ -10,4 +10,5 @@ workspace = true [dependencies] gpui.workspace = true +schemars.workspace = true serde.workspace = true diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index b823b38bbce11d..3a9d5d72217aaa 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -1,5 +1,6 @@ use gpui::{actions, impl_actions}; -use serde::Deserialize; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; // If the zed binary doesn't use anything in this crate, it will be optimized away // and the actions won't initialize. So we just provide an empty initialization function @@ -90,33 +91,39 @@ pub struct OpenRecent { gpui::impl_actions!(projects, [OpenRecent]); gpui::actions!(projects, [OpenRemote]); -#[derive(PartialEq, Eq, Clone, Copy, Deserialize, Default, Debug)] +/// Where to spawn the task in the UI. +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum TaskSpawnTarget { +pub enum RevealTarget { + /// In the central pane group, "main" editor area. Center, + /// In the terminal dock, "regular" terminal items' place. #[default] Dock, } /// Spawn a task with name or open tasks modal -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct Spawn { - #[serde(default)] - /// Name of the task to spawn. - /// If it is not set, a modal with a list of available tasks is opened instead. - /// Defaults to None. - pub task_name: Option, - /// Which part of the UI the task should be spawned in. - /// Defaults to Dock. - #[serde(default)] - pub target: Option, +#[derive(Debug, PartialEq, Clone, Deserialize)] +#[serde(untagged)] +pub enum Spawn { + /// Spawns a task by the name given. + ByName { + task_name: String, + #[serde(default)] + reveal_target: Option, + }, + /// Spawns a task via modal's selection. + ViaModal { + /// Selected task's `reveal_target` property override. + #[serde(default)] + reveal_target: Option, + }, } impl Spawn { pub fn modal() -> Self { - Self { - task_name: None, - target: None, + Self::ViaModal { + reveal_target: None, } } } diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 6399c6f5e003b2..45f5a4e0946b7c 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -17,9 +17,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. "allow_concurrent_runs": false, // What to do with the terminal pane and tab, after the command was started: - // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) - // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it - // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there + // * `always` — always show the task's pane, and focus the corresponding tab in it (default) + // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it + // * `never` — do not alter focus, but still add/reuse the task's tab in its pane "reveal": "always", // What to do with the terminal pane and tab, after the command had finished: // * `never` — Do nothing when the command finishes (default) From 2562b488b17e0df6a00c598b302d6e891e8c5a3c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:54:06 -0300 Subject: [PATCH 08/32] Refine interaction in foldable multibuffer header (#22084) - Ensuring that the fold button is big enough to avoid clicking on the header as a whole (and then moving to the actual file) - Adding tooltips to the fold button - Refining the container structure so that the tooltip for the folder button and the header click don't overlap - Adding keybindings to tooltips https://github.com/user-attachments/assets/82284b59-3025-4d6d-b916-ad4d1ecdb119 Release Notes: - N/A --- crates/editor/src/element.rs | 94 ++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index eafa2938994776..d0ed31dc7d7dbf 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -22,7 +22,7 @@ use crate::{ EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, - SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, + SoftWrap, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use client::ParticipantIndex; @@ -2398,6 +2398,8 @@ impl EditorElement { .as_ref() .and_then(|path| Some(path.parent()?.to_string_lossy().to_string() + "/")); + let focus_handle = self.editor.focus_handle(cx); + div() .px(header_padding) .pt(header_padding) @@ -2405,29 +2407,43 @@ impl EditorElement { .h(FILE_HEADER_HEIGHT as f32 * cx.line_height()) .child( h_flex() - .id("path header block") .size_full() + .gap_2() .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .px(gpui::px(12.)) + .pl_0p5() + .pr_4() .rounded_md() .shadow_md() .border_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_subheader_background) - .justify_between() .hover(|style| style.bg(cx.theme().colors().element_hover)) - .child( - h_flex() - .gap_3() - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx) - .map(Icon::from_path); - header.child( + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(cx.theme().colors().element_selected)) + .rounded_sm() + .child( ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .size(ButtonSize::Large) + .width(px(30.).into()) .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in( + "Toggle Excerpt Fold", + &ToggleFold, + &focus_handle, + cx, + ) + } + }) .on_click(move |_, cx| { if is_folded { editor.update(cx, |editor, cx| { @@ -2439,8 +2455,14 @@ impl EditorElement { }); } }), - ) - }) + ), + ) + }) + .child( + h_flex() + .id("path header block") + .size_full() + .justify_between() .child( h_flex() .gap_2() @@ -2456,21 +2478,31 @@ impl EditorElement { .text_color(cx.theme().colors().text_muted), ) }), - ), - ) - .child(Icon::new(IconName::ArrowUpRight)) - .cursor_pointer() - .tooltip(|cx| Tooltip::for_action("Jump to File", &OpenExcerpts, cx)) - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click(cx.listener_for(&self.editor, { - move |editor, e: &ClickEvent, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.down.modifiers.secondary(), - cx, - ); - } - })), + ) + .child(Icon::new(IconName::ArrowUpRight)) + .cursor_pointer() + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in( + "Jump To File", + &OpenExcerpts, + &focus_handle, + cx, + ) + } + }) + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(cx.listener_for(&self.editor, { + move |editor, e: &ClickEvent, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.down.modifiers.secondary(), + cx, + ); + } + })), + ), ) } From 188c55c8a6673789bd863741d47608f9bb02684e Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:00:26 -0500 Subject: [PATCH 09/32] docs: Fix context_servers key for example extension manifest (#22079) Pretty sure this isn't meant to be kebab-case. Release Notes: - N/A --- docs/src/extensions/context-servers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/extensions/context-servers.md b/docs/src/extensions/context-servers.md index 6e61987384b5ff..cb29690971b941 100644 --- a/docs/src/extensions/context-servers.md +++ b/docs/src/extensions/context-servers.md @@ -13,7 +13,7 @@ This extension can be [installed as a dev extension](./developing-extensions.htm A given extension may provide one or more context servers. Each context server must be registered in the `extension.toml`: ```toml -[context-servers.my-context-server] +[context_servers.my-context-server] ``` Then, in the Rust code for your extension, implement the `context_server_command` method on your extension: From 88f7942f117f95f8edaf84e85888afd36351eaa2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 11:50:57 -0500 Subject: [PATCH 10/32] assistant2: Add support for referencing other threads as context (#22092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the ability to reference other threads as context: Screenshot 2024-12-16 at 11 29 54 AM Screenshot 2024-12-16 at 11 29 35 AM Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 24 +- crates/assistant2/src/context.rs | 1 + crates/assistant2/src/context_picker.rs | 34 ++- .../context_picker/thread_context_picker.rs | 209 ++++++++++++++++++ crates/assistant2/src/message_editor.rs | 8 +- crates/assistant2/src/thread.rs | 13 ++ 6 files changed, 278 insertions(+), 11 deletions(-) create mode 100644 crates/assistant2/src/context_picker/thread_context_picker.rs diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index cc40e6e6b07fba..2ab28077f111b3 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -94,7 +94,9 @@ impl AssistantPanel { cx, ) }), - message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)), + message_editor: cx.new_view(|cx| { + MessageEditor::new(workspace, thread_store.downgrade(), thread.clone(), cx) + }), tools, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), @@ -123,8 +125,14 @@ impl AssistantPanel { cx, ) }); - self.message_editor = - cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx)); + self.message_editor = cx.new_view(|cx| { + MessageEditor::new( + self.workspace.clone(), + self.thread_store.downgrade(), + thread, + cx, + ) + }); self.message_editor.focus_handle(cx).focus(cx); } @@ -146,8 +154,14 @@ impl AssistantPanel { cx, ) }); - self.message_editor = - cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx)); + self.message_editor = cx.new_view(|cx| { + MessageEditor::new( + self.workspace.clone(), + self.thread_store.downgrade(), + thread, + cx, + ) + }); self.message_editor.focus_handle(cx).focus(cx); } diff --git a/crates/assistant2/src/context.rs b/crates/assistant2/src/context.rs index 577d87166ff686..414093dc3136ac 100644 --- a/crates/assistant2/src/context.rs +++ b/crates/assistant2/src/context.rs @@ -24,4 +24,5 @@ pub struct Context { pub enum ContextKind { File, FetchedUrl, + Thread, } diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs index f78e617a340a5a..0ff5f534b365c2 100644 --- a/crates/assistant2/src/context_picker.rs +++ b/crates/assistant2/src/context_picker.rs @@ -1,11 +1,12 @@ mod fetch_context_picker; mod file_context_picker; +mod thread_context_picker; use std::sync::Arc; use gpui::{ AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View, - WeakView, + WeakModel, WeakView, }; use picker::{Picker, PickerDelegate}; use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip}; @@ -14,13 +15,16 @@ use workspace::Workspace; use crate::context_picker::fetch_context_picker::FetchContextPicker; use crate::context_picker::file_context_picker::FileContextPicker; +use crate::context_picker::thread_context_picker::ThreadContextPicker; use crate::message_editor::MessageEditor; +use crate::thread_store::ThreadStore; #[derive(Debug, Clone)] enum ContextPickerMode { Default, File(View), Fetch(View), + Thread(View), } pub(super) struct ContextPicker { @@ -31,13 +35,15 @@ pub(super) struct ContextPicker { impl ContextPicker { pub fn new( workspace: WeakView, + thread_store: WeakModel, message_editor: WeakView, cx: &mut ViewContext, ) -> Self { let delegate = ContextPickerDelegate { context_picker: cx.view().downgrade(), - workspace: workspace.clone(), - message_editor: message_editor.clone(), + workspace, + thread_store, + message_editor, entries: vec![ ContextPickerEntry { name: "directory".into(), @@ -54,6 +60,11 @@ impl ContextPicker { description: "Fetch content from URL".into(), icon: IconName::Globe, }, + ContextPickerEntry { + name: "thread".into(), + description: "Insert any thread".into(), + icon: IconName::MessageBubbles, + }, ], selected_ix: 0, }; @@ -81,6 +92,7 @@ impl FocusableView for ContextPicker { ContextPickerMode::Default => self.picker.focus_handle(cx), ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx), ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx), + ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx), } } } @@ -94,6 +106,7 @@ impl Render for ContextPicker { ContextPickerMode::Default => parent.child(self.picker.clone()), ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()), ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()), + ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()), }) } } @@ -108,6 +121,7 @@ struct ContextPickerEntry { pub(crate) struct ContextPickerDelegate { context_picker: WeakView, workspace: WeakView, + thread_store: WeakModel, message_editor: WeakView, entries: Vec, selected_ix: usize, @@ -162,6 +176,16 @@ impl PickerDelegate for ContextPickerDelegate { ) })); } + "thread" => { + this.mode = ContextPickerMode::Thread(cx.new_view(|cx| { + ThreadContextPicker::new( + self.thread_store.clone(), + self.context_picker.clone(), + self.message_editor.clone(), + cx, + ) + })); + } _ => {} } @@ -175,7 +199,9 @@ impl PickerDelegate for ContextPickerDelegate { self.context_picker .update(cx, |this, cx| match this.mode { ContextPickerMode::Default => cx.emit(DismissEvent), - ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {} + ContextPickerMode::File(_) + | ContextPickerMode::Fetch(_) + | ContextPickerMode::Thread(_) => {} }) .log_err(); } diff --git a/crates/assistant2/src/context_picker/thread_context_picker.rs b/crates/assistant2/src/context_picker/thread_context_picker.rs new file mode 100644 index 00000000000000..61b1ba0f058056 --- /dev/null +++ b/crates/assistant2/src/context_picker/thread_context_picker.rs @@ -0,0 +1,209 @@ +use std::sync::Arc; + +use fuzzy::StringMatchCandidate; +use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView}; +use picker::{Picker, PickerDelegate}; +use ui::{prelude::*, ListItem}; + +use crate::context::ContextKind; +use crate::context_picker::ContextPicker; +use crate::message_editor::MessageEditor; +use crate::thread::ThreadId; +use crate::thread_store::ThreadStore; + +pub struct ThreadContextPicker { + picker: View>, +} + +impl ThreadContextPicker { + pub fn new( + thread_store: WeakModel, + context_picker: WeakView, + message_editor: WeakView, + cx: &mut ViewContext, + ) -> Self { + let delegate = + ThreadContextPickerDelegate::new(thread_store, context_picker, message_editor); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); + + ThreadContextPicker { picker } + } +} + +impl FocusableView for ThreadContextPicker { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for ThreadContextPicker { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + self.picker.clone() + } +} + +#[derive(Debug, Clone)] +struct ThreadContextEntry { + id: ThreadId, + summary: SharedString, +} + +pub struct ThreadContextPickerDelegate { + thread_store: WeakModel, + context_picker: WeakView, + message_editor: WeakView, + matches: Vec, + selected_index: usize, +} + +impl ThreadContextPickerDelegate { + pub fn new( + thread_store: WeakModel, + context_picker: WeakView, + message_editor: WeakView, + ) -> Self { + ThreadContextPickerDelegate { + thread_store, + context_picker, + message_editor, + matches: Vec::new(), + selected_index: 0, + } + } +} + +impl PickerDelegate for ThreadContextPickerDelegate { + 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>) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + "Search threads…".into() + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + let Ok(threads) = self.thread_store.update(cx, |this, cx| { + this.threads(cx) + .into_iter() + .map(|thread| { + const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread"); + + let id = thread.read(cx).id().clone(); + let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY); + ThreadContextEntry { id, summary } + }) + .collect::>() + }) else { + return Task::ready(()); + }; + + let executor = cx.background_executor().clone(); + let search_task = cx.background_executor().spawn(async move { + if query.is_empty() { + threads + } else { + let candidates = threads + .iter() + .enumerate() + .map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary)) + .collect::>(); + let matches = fuzzy::match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + executor, + ) + .await; + + matches + .into_iter() + .map(|mat| threads[mat.candidate_id].clone()) + .collect() + } + }); + + cx.spawn(|this, mut cx| async move { + let matches = search_task.await; + this.update(&mut cx, |this, cx| { + this.delegate.matches = matches; + this.delegate.selected_index = 0; + cx.notify(); + }) + .ok(); + }) + } + + fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + let entry = &self.matches[self.selected_index]; + + let Some(thread_store) = self.thread_store.upgrade() else { + return; + }; + + let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx)) + else { + return; + }; + + self.message_editor + .update(cx, |message_editor, cx| { + let text = thread.update(cx, |thread, _cx| { + let mut text = String::new(); + + for message in thread.messages() { + text.push_str(match message.role { + language_model::Role::User => "User:", + language_model::Role::Assistant => "Assistant:", + language_model::Role::System => "System:", + }); + text.push('\n'); + + text.push_str(&message.text); + text.push('\n'); + } + + text + }); + + message_editor.insert_context(ContextKind::Thread, entry.summary.clone(), text); + }) + .ok(); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.context_picker + .update(cx, |this, cx| { + this.reset_mode(); + cx.emit(DismissEvent); + }) + .ok(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut ViewContext>, + ) -> Option { + let thread = &self.matches[ix]; + + Some( + ListItem::new(ix) + .inset(true) + .toggle_state(selected) + .child(thread.summary.clone()), + ) + } +} diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 7cb605cd62e441..f21caf8a7645c6 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView}; +use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView}; use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use settings::Settings; @@ -15,6 +15,7 @@ use workspace::Workspace; use crate::context::{Context, ContextId, ContextKind}; use crate::context_picker::ContextPicker; use crate::thread::{RequestKind, Thread}; +use crate::thread_store::ThreadStore; use crate::ui::ContextPill; use crate::{Chat, ToggleModelSelector}; @@ -32,6 +33,7 @@ pub struct MessageEditor { impl MessageEditor { pub fn new( workspace: WeakView, + thread_store: WeakModel, thread: Model, cx: &mut ViewContext, ) -> Self { @@ -46,7 +48,9 @@ impl MessageEditor { }), context: Vec::new(), next_context_id: ContextId(0), - context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)), + context_picker: cx.new_view(|cx| { + ContextPicker::new(workspace.clone(), thread_store.clone(), weak_self, cx) + }), context_picker_handle: PopoverMenuHandle::default(), language_model_selector: cx.new_view(|cx| { LanguageModelSelector::new( diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 8234a0e8af6a10..73d022c664d7e6 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -194,6 +194,7 @@ impl Thread { if let Some(context) = self.context_for_message(message.id) { let mut file_context = String::new(); let mut fetch_context = String::new(); + let mut thread_context = String::new(); for context in context.iter() { match context.kind { @@ -207,6 +208,12 @@ impl Thread { fetch_context.push_str(&context.text); fetch_context.push('\n'); } + ContextKind::Thread => { + thread_context.push_str(&context.name); + thread_context.push('\n'); + thread_context.push_str(&context.text); + thread_context.push('\n'); + } } } @@ -221,6 +228,12 @@ impl Thread { context_text.push_str(&fetch_context); } + if !thread_context.is_empty() { + context_text + .push_str("The following previous conversation threads are available\n"); + context_text.push_str(&thread_context); + } + request_message .content .push(MessageContent::Text(context_text)) From ff2ad63037e0a33447f94f764720dbb86fc2b19c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 16 Dec 2024 19:23:01 +0200 Subject: [PATCH 11/32] Allow splitting terminal items in the central pane group (#22088) Follow-up of https://github.com/zed-industries/zed/pull/22004 Closes https://github.com/zed-industries/zed/issues/22078 Release Notes: - Fixed splitting terminal items in the center --- crates/project/src/terminals.rs | 332 +++++++++++---------- crates/terminal/src/terminal.rs | 3 + crates/terminal_view/src/persistence.rs | 8 +- crates/terminal_view/src/terminal_panel.rs | 156 +++++----- crates/terminal_view/src/terminal_view.rs | 59 +++- docs/src/tasks.md | 2 +- 6 files changed, 299 insertions(+), 261 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 6bee662a7542bb..779bc2c4a04680 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -94,7 +94,6 @@ impl Project { } } }; - let ssh_details = self.ssh_details(cx); let mut settings_location = None; if let Some(path) = path.as_ref() { @@ -107,10 +106,57 @@ impl Project { } let settings = TerminalSettings::get(settings_location, cx).clone(); + cx.spawn(move |project, mut cx| async move { + let python_venv_directory = if let Some(path) = path.clone() { + project + .update(&mut cx, |this, cx| { + this.python_venv_directory(path, settings.detect_venv.clone(), cx) + })? + .await + } else { + None + }; + project.update(&mut cx, |project, cx| { + project.create_terminal_with_venv(kind, python_venv_directory, window, cx) + })? + }) + } + + pub fn create_terminal_with_venv( + &mut self, + kind: TerminalKind, + python_venv_directory: Option, + window: AnyWindowHandle, + cx: &mut ModelContext, + ) -> Result> { + let this = &mut *self; + let path: Option> = match &kind { + TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), + TerminalKind::Task(spawn_task) => { + if let Some(cwd) = &spawn_task.cwd { + Some(Arc::from(cwd.as_ref())) + } else { + this.active_project_directory(cx) + } + } + }; + let ssh_details = this.ssh_details(cx); + + let mut settings_location = None; + if let Some(path) = path.as_ref() { + if let Some((worktree, _)) = this.find_worktree(path, cx) { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); + } + } + let settings = TerminalSettings::get(settings_location, cx).clone(); + let (completion_tx, completion_rx) = bounded(1); // Start with the environment that we might have inherited from the Zed CLI. - let mut env = self + let mut env = this .environment .read(cx) .get_cli_environment() @@ -125,165 +171,141 @@ impl Project { None }; - cx.spawn(move |this, mut cx| async move { - let python_venv_directory = if let Some(path) = path.clone() { - this.update(&mut cx, |this, cx| { - this.python_venv_directory(path, settings.detect_venv.clone(), cx) - })? - .await - } else { - None - }; - let mut python_venv_activate_command = None; - - let (spawn_task, shell) = match kind { - TerminalKind::Shell(_) => { - if let Some(python_venv_directory) = python_venv_directory { - python_venv_activate_command = this - .update(&mut cx, |this, _| { - this.python_activate_command( - &python_venv_directory, - &settings.detect_venv, - ) - }) - .ok() - .flatten(); - } + let mut python_venv_activate_command = None; - match &ssh_details { - Some((host, ssh_command)) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - - // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed - // to properly display colors. - // We do not have the luxury of assuming the host has it installed, - // so we set it to a default that does not break the highlighting via ssh. - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - - let (program, args) = - wrap_for_ssh(ssh_command, None, path.as_deref(), env, None); - env = HashMap::default(); - ( - Option::::None, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => (None, settings.shell.clone()), - } + let (spawn_task, shell) = match kind { + TerminalKind::Shell(_) => { + if let Some(python_venv_directory) = &python_venv_directory { + python_venv_activate_command = + this.python_activate_command(python_venv_directory, &settings.detect_venv); } - TerminalKind::Task(spawn_task) => { - let task_state = Some(TaskState { - id: spawn_task.id, - full_label: spawn_task.full_label, - label: spawn_task.label, - command_label: spawn_task.command_label, - hide: spawn_task.hide, - status: TaskStatus::Running, - show_summary: spawn_task.show_summary, - show_command: spawn_task.show_command, - completion_rx, - }); - - env.extend(spawn_task.env); - - if let Some(venv_path) = &python_venv_directory { - env.insert( - "VIRTUAL_ENV".to_string(), - venv_path.to_string_lossy().to_string(), - ); - } - match &ssh_details { - Some((host, ssh_command)) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = wrap_for_ssh( - ssh_command, - Some((&spawn_task.command, &spawn_task.args)), - path.as_deref(), - env, - python_venv_directory, - ); - env = HashMap::default(); - ( - task_state, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => { - if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join("bin")).log_err(); - } - - ( - task_state, - Shell::WithArguments { - program: spawn_task.command, - args: spawn_task.args, - title_override: None, - }, - ) - } + match &ssh_details { + Some((host, ssh_command)) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + + // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed + // to properly display colors. + // We do not have the luxury of assuming the host has it installed, + // so we set it to a default that does not break the highlighting via ssh. + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + + let (program, args) = + wrap_for_ssh(&ssh_command, None, path.as_deref(), env, None); + env = HashMap::default(); + ( + Option::::None, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) } + None => (None, settings.shell.clone()), } - }; - let terminal = this.update(&mut cx, |this, cx| { - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - spawn_task, - shell, - env, - settings.cursor_shape.unwrap_or_default(), - settings.alternate_scroll, - settings.max_scroll_history_lines, - ssh_details.is_some(), - window, - completion_tx, - cx, - ) - .map(|builder| { - let terminal_handle = cx.new_model(|cx| builder.subscribe(cx)); - - this.terminals - .local_handles - .push(terminal_handle.downgrade()); - - let id = terminal_handle.entity_id(); - cx.observe_release(&terminal_handle, move |project, _terminal, cx| { - let handles = &mut project.terminals.local_handles; - - if let Some(index) = handles - .iter() - .position(|terminal| terminal.entity_id() == id) - { - handles.remove(index); - cx.notify(); - } - }) - .detach(); + } + TerminalKind::Task(spawn_task) => { + let task_state = Some(TaskState { + id: spawn_task.id, + full_label: spawn_task.full_label, + label: spawn_task.label, + command_label: spawn_task.command_label, + hide: spawn_task.hide, + status: TaskStatus::Running, + show_summary: spawn_task.show_summary, + show_command: spawn_task.show_command, + completion_rx, + }); - if let Some(activate_command) = python_venv_activate_command { - this.activate_python_virtual_environment( - activate_command, - &terminal_handle, - cx, + env.extend(spawn_task.env); + + if let Some(venv_path) = &python_venv_directory { + env.insert( + "VIRTUAL_ENV".to_string(), + venv_path.to_string_lossy().to_string(), + ); + } + + match &ssh_details { + Some((host, ssh_command)) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + let (program, args) = wrap_for_ssh( + &ssh_command, + Some((&spawn_task.command, &spawn_task.args)), + path.as_deref(), + env, + python_venv_directory.as_deref(), ); + env = HashMap::default(); + ( + task_state, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) + } + None => { + if let Some(venv_path) = &python_venv_directory { + add_environment_path(&mut env, &venv_path.join("bin")).log_err(); + } + + ( + task_state, + Shell::WithArguments { + program: spawn_task.command, + args: spawn_task.args, + title_override: None, + }, + ) } - terminal_handle - }) - })?; + } + } + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + python_venv_directory, + spawn_task, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + ssh_details.is_some(), + window, + completion_tx, + cx, + ) + .map(|builder| { + let terminal_handle = cx.new_model(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; + + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } + }) + .detach(); - terminal + if let Some(activate_command) = python_venv_activate_command { + this.activate_python_virtual_environment(activate_command, &terminal_handle, cx); + } + terminal_handle }) } @@ -418,9 +440,9 @@ impl Project { &self, command: String, terminal_handle: &Model, - cx: &mut ModelContext, + cx: &mut AppContext, ) { - terminal_handle.update(cx, |this, _| this.input_bytes(command.into_bytes())); + terminal_handle.update(cx, |terminal, _| terminal.input_bytes(command.into_bytes())); } pub fn local_terminal_handles(&self) -> &Vec> { @@ -433,7 +455,7 @@ pub fn wrap_for_ssh( command: Option<(&String, &Vec)>, path: Option<&Path>, env: HashMap, - venv_directory: Option, + venv_directory: Option<&Path>, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { let command = Cow::Borrowed(command.as_str()); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6610ac567d522f..51a95bc3ee2eeb 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -324,6 +324,7 @@ impl TerminalBuilder { #[allow(clippy::too_many_arguments)] pub fn new( working_directory: Option, + python_venv_directory: Option, task: Option, shell: Shell, mut env: HashMap, @@ -471,6 +472,7 @@ impl TerminalBuilder { word_regex: RegexSearch::new(WORD_REGEX).unwrap(), vi_mode_enabled: false, is_ssh_terminal, + python_venv_directory, }; Ok(TerminalBuilder { @@ -619,6 +621,7 @@ pub struct Terminal { pub breadcrumb_text: String, pub pty_info: PtyProcessInfo, title_override: Option, + pub python_venv_directory: Option, scroll_px: Pixels, next_link_id: usize, selection_phase: SelectionPhase, diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index f4653014a146ae..4e88cb951548ff 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -251,7 +251,13 @@ async fn deserialize_pane_group( let terminal = terminal.await.ok()?; pane.update(cx, |pane, cx| { let terminal_view = Box::new(cx.new_view(|cx| { - TerminalView::new(terminal, workspace.clone(), Some(workspace_id), cx) + TerminalView::new( + terminal, + workspace.clone(), + Some(workspace_id), + project.downgrade(), + cx, + ) })); pane.add_item(terminal_view, true, false, None, cx); }) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index a4f5e7df4f7dac..da1bb7c4068fd5 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -335,24 +335,13 @@ impl TerminalPanel { self.serialize(cx); } pane::Event::Split(direction) => { - let new_pane = self.new_pane_with_cloned_active_terminal(cx); + let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else { + return; + }; let pane = pane.clone(); let direction = *direction; - cx.spawn(move |terminal_panel, mut cx| async move { - let Some(new_pane) = new_pane.await else { - return; - }; - terminal_panel - .update(&mut cx, |terminal_panel, cx| { - terminal_panel - .center - .split(&pane, &new_pane, direction) - .log_err(); - cx.focus_view(&new_pane); - }) - .ok(); - }) - .detach(); + self.center.split(&pane, &new_pane, direction).log_err(); + cx.focus_view(&new_pane); } pane::Event::Focus => { self.active_pane = pane.clone(); @@ -365,63 +354,56 @@ impl TerminalPanel { fn new_pane_with_cloned_active_terminal( &mut self, cx: &mut ViewContext, - ) -> Task>> { - let Some(workspace) = self.workspace.clone().upgrade() else { - return Task::ready(None); - }; - let database_id = workspace.read(cx).database_id(); + ) -> Option> { + let workspace = self.workspace.clone().upgrade()?; + let workspace = workspace.read(cx); + let database_id = workspace.database_id(); let weak_workspace = self.workspace.clone(); - let project = workspace.read(cx).project().clone(); - let working_directory = self + let project = workspace.project().clone(); + let (working_directory, python_venv_directory) = self .active_pane .read(cx) .active_item() .and_then(|item| item.downcast::()) - .and_then(|terminal_view| { - terminal_view - .read(cx) - .terminal() - .read(cx) - .working_directory() + .map(|terminal_view| { + let terminal = terminal_view.read(cx).terminal().read(cx); + ( + terminal + .working_directory() + .or_else(|| default_working_directory(workspace, cx)), + terminal.python_venv_directory.clone(), + ) }) - .or_else(|| default_working_directory(workspace.read(cx), cx)); + .unwrap_or((None, None)); let kind = TerminalKind::Shell(working_directory); let window = cx.window_handle(); - cx.spawn(move |terminal_panel, mut cx| async move { - let terminal = project - .update(&mut cx, |project, cx| { - project.create_terminal(kind, window, cx) - }) - .log_err()? - .await - .log_err()?; - - let terminal_view = Box::new( - cx.new_view(|cx| { - TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx) - }) - .ok()?, - ); - let pane = terminal_panel - .update(&mut cx, |terminal_panel, cx| { - let pane = new_terminal_pane( - weak_workspace, - project, - terminal_panel.active_pane.read(cx).is_zoomed(), - cx, - ); - terminal_panel.apply_tab_bar_buttons(&pane, cx); - pane - }) - .ok()?; - - pane.update(&mut cx, |pane, cx| { - pane.add_item(terminal_view, true, true, None, cx); + let terminal = project + .update(cx, |project, cx| { + project.create_terminal_with_venv(kind, python_venv_directory, window, cx) }) .ok()?; - Some(pane) - }) + let terminal_view = Box::new(cx.new_view(|cx| { + TerminalView::new( + terminal.clone(), + weak_workspace.clone(), + database_id, + project.downgrade(), + cx, + ) + })); + let pane = new_terminal_pane( + weak_workspace, + project, + self.active_pane.read(cx).is_zoomed(), + cx, + ); + self.apply_tab_bar_buttons(&pane, cx); + pane.update(cx, |pane, cx| { + pane.add_item(terminal_view, true, true, None, cx); + }); + + Some(pane) } pub fn open_terminal( @@ -724,6 +706,7 @@ impl TerminalPanel { terminal.clone(), workspace.weak_handle(), workspace.database_id(), + workspace.project().downgrade(), cx, ) }); @@ -739,17 +722,19 @@ impl TerminalPanel { reveal_strategy: RevealStrategy, cx: &mut ViewContext, ) -> Task>> { - if !self.is_enabled(cx) { - return Task::ready(Err(anyhow!( - "terminal not yet supported for remote projects" - ))); - } - let workspace = self.workspace.clone(); self.pending_terminals_to_add += 1; cx.spawn(|terminal_panel, mut cx| async move { - let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?; + if workspace.update(&mut cx, |workspace, cx| { + !is_enabled_in_workspace(workspace, cx) + })? { + anyhow::bail!("terminal not yet supported for remote projects"); + } + let pane = terminal_panel.update(&mut cx, |terminal_panel, _| { + terminal_panel.pending_terminals_to_add += 1; + terminal_panel.active_pane.clone() + })?; let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?; let window = cx.window_handle(); let terminal = project @@ -763,6 +748,7 @@ impl TerminalPanel { terminal.clone(), workspace.weak_handle(), workspace.database_id(), + workspace.project().downgrade(), cx, ) })); @@ -1218,25 +1204,19 @@ impl Render for TerminalPanel { if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { cx.focus_view(&pane); } else { - let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx); - cx.spawn(|terminal_panel, mut cx| async move { - if let Some(new_pane) = new_pane.await { - terminal_panel - .update(&mut cx, |terminal_panel, cx| { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - cx.focus_view(&new_pane); - }) - .ok(); - } - }) - .detach(); + if let Some(new_pane) = + terminal_panel.new_pane_with_cloned_active_terminal(cx) + { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + cx.focus_view(&new_pane); + } } })) .on_action(cx.listener( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9cc7b3ccec4744..9f101fe0575b09 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -9,7 +9,7 @@ use gpui::{ anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, View, - VisualContext, WeakView, + VisualContext, WeakModel, WeakView, }; use language::Bias; use persistence::TERMINAL_DB; @@ -97,6 +97,7 @@ pub struct BlockContext<'a, 'b> { pub struct TerminalView { terminal: Model, workspace: WeakView, + project: WeakModel, focus_handle: FocusHandle, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, @@ -141,6 +142,7 @@ impl TerminalView { terminal: Model, workspace: WeakView, workspace_id: Option, + project: WeakModel, cx: &mut ViewContext, ) -> Self { let workspace_handle = workspace.clone(); @@ -160,6 +162,7 @@ impl TerminalView { Self { terminal, workspace: workspace_handle, + project, has_bell: false, focus_handle, context_menu: None, @@ -1075,21 +1078,37 @@ impl Item for TerminalView { fn clone_on_split( &self, - _workspace_id: Option, - _cx: &mut ViewContext, + workspace_id: Option, + cx: &mut ViewContext, ) -> Option> { - //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the shell. There might be - //solutions to this, but they are non-trivial and require more IPC - - // Some(TerminalContainer::new( - // Err(anyhow::anyhow!("failed to instantiate terminal")), - // workspace_id, - // cx, - // )) - - // TODO - None + let window = cx.window_handle(); + let terminal = self + .project + .update(cx, |project, cx| { + let terminal = self.terminal().read(cx); + let working_directory = terminal + .working_directory() + .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf())); + let python_venv_directory = terminal.python_venv_directory.clone(); + project.create_terminal_with_venv( + TerminalKind::Shell(working_directory), + python_venv_directory, + window, + cx, + ) + }) + .ok()? + .log_err()?; + + Some(cx.new_view(|cx| { + TerminalView::new( + terminal, + self.workspace.clone(), + workspace_id, + self.project.clone(), + cx, + ) + })) } fn is_dirty(&self, cx: &gpui::AppContext) -> bool { @@ -1218,7 +1237,15 @@ impl SerializableItem for TerminalView { })? .await?; cx.update(|cx| { - cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx)) + cx.new_view(|cx| { + TerminalView::new( + terminal, + workspace, + Some(workspace_id), + project.downgrade(), + cx, + ) + }) }) }) } diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 45f5a4e0946b7c..ae2c383bafbb0e 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -173,7 +173,7 @@ This could be useful for launching a terminal application that you want to use i "bindings": { "alt-g": [ "task::Spawn", - { "task_name": "start lazygit", "target": "center" } + { "task_name": "start lazygit", "reveal_target": "center" } ] } } From caefdcd7f104c777cca664b1ee0f7d3300e892d5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 12:45:01 -0500 Subject: [PATCH 12/32] assistant2: Factor out `ContextStrip` (#22096) This PR factors a `ContextStrip` view out of the `MessageEditor` so that we can use it in other places. Release Notes: - N/A --- crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/context_picker.rs | 14 +-- .../context_picker/fetch_context_picker.rs | 18 ++-- .../src/context_picker/file_context_picker.rs | 46 ++++---- .../context_picker/thread_context_picker.rs | 18 ++-- crates/assistant2/src/context_strip.rs | 101 ++++++++++++++++++ crates/assistant2/src/message_editor.rs | 85 ++------------- 7 files changed, 156 insertions(+), 127 deletions(-) create mode 100644 crates/assistant2/src/context_strip.rs diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 5a7a88f2f385e4..d7d37fc78bb328 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -3,6 +3,7 @@ mod assistant_panel; mod assistant_settings; mod context; mod context_picker; +mod context_strip; mod inline_assistant; mod message_editor; mod prompts; diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs index 0ff5f534b365c2..5766801bb2dc2b 100644 --- a/crates/assistant2/src/context_picker.rs +++ b/crates/assistant2/src/context_picker.rs @@ -16,7 +16,7 @@ use workspace::Workspace; use crate::context_picker::fetch_context_picker::FetchContextPicker; use crate::context_picker::file_context_picker::FileContextPicker; use crate::context_picker::thread_context_picker::ThreadContextPicker; -use crate::message_editor::MessageEditor; +use crate::context_strip::ContextStrip; use crate::thread_store::ThreadStore; #[derive(Debug, Clone)] @@ -36,14 +36,14 @@ impl ContextPicker { pub fn new( workspace: WeakView, thread_store: WeakModel, - message_editor: WeakView, + context_strip: WeakView, cx: &mut ViewContext, ) -> Self { let delegate = ContextPickerDelegate { context_picker: cx.view().downgrade(), workspace, thread_store, - message_editor, + context_strip, entries: vec![ ContextPickerEntry { name: "directory".into(), @@ -122,7 +122,7 @@ pub(crate) struct ContextPickerDelegate { context_picker: WeakView, workspace: WeakView, thread_store: WeakModel, - message_editor: WeakView, + context_strip: WeakView, entries: Vec, selected_ix: usize, } @@ -161,7 +161,7 @@ impl PickerDelegate for ContextPickerDelegate { FileContextPicker::new( self.context_picker.clone(), self.workspace.clone(), - self.message_editor.clone(), + self.context_strip.clone(), cx, ) })); @@ -171,7 +171,7 @@ impl PickerDelegate for ContextPickerDelegate { FetchContextPicker::new( self.context_picker.clone(), self.workspace.clone(), - self.message_editor.clone(), + self.context_strip.clone(), cx, ) })); @@ -181,7 +181,7 @@ impl PickerDelegate for ContextPickerDelegate { ThreadContextPicker::new( self.thread_store.clone(), self.context_picker.clone(), - self.message_editor.clone(), + self.context_strip.clone(), cx, ) })); diff --git a/crates/assistant2/src/context_picker/fetch_context_picker.rs b/crates/assistant2/src/context_picker/fetch_context_picker.rs index 352d3cd057b672..dd0a9d3d5a2f26 100644 --- a/crates/assistant2/src/context_picker/fetch_context_picker.rs +++ b/crates/assistant2/src/context_picker/fetch_context_picker.rs @@ -13,7 +13,7 @@ use workspace::Workspace; use crate::context::ContextKind; use crate::context_picker::ContextPicker; -use crate::message_editor::MessageEditor; +use crate::context_strip::ContextStrip; pub struct FetchContextPicker { picker: View>, @@ -23,10 +23,10 @@ impl FetchContextPicker { pub fn new( context_picker: WeakView, workspace: WeakView, - message_editor: WeakView, + context_strip: WeakView, cx: &mut ViewContext, ) -> Self { - let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor); + let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_strip); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } @@ -55,7 +55,7 @@ enum ContentType { pub struct FetchContextPickerDelegate { context_picker: WeakView, workspace: WeakView, - message_editor: WeakView, + context_strip: WeakView, url: String, } @@ -63,12 +63,12 @@ impl FetchContextPickerDelegate { pub fn new( context_picker: WeakView, workspace: WeakView, - message_editor: WeakView, + context_strip: WeakView, ) -> Self { FetchContextPickerDelegate { context_picker, workspace, - message_editor, + context_strip, url: String::new(), } } @@ -189,9 +189,9 @@ impl PickerDelegate for FetchContextPickerDelegate { this.update(&mut cx, |this, cx| { this.delegate - .message_editor - .update(cx, |message_editor, _cx| { - message_editor.insert_context(ContextKind::FetchedUrl, url, text); + .context_strip + .update(cx, |context_strip, _cx| { + context_strip.insert_context(ContextKind::FetchedUrl, url, text); }) })??; diff --git a/crates/assistant2/src/context_picker/file_context_picker.rs b/crates/assistant2/src/context_picker/file_context_picker.rs index c5441bb92a672d..a0e89aa7000c20 100644 --- a/crates/assistant2/src/context_picker/file_context_picker.rs +++ b/crates/assistant2/src/context_picker/file_context_picker.rs @@ -14,7 +14,7 @@ use workspace::Workspace; use crate::context::ContextKind; use crate::context_picker::ContextPicker; -use crate::message_editor::MessageEditor; +use crate::context_strip::ContextStrip; pub struct FileContextPicker { picker: View>, @@ -24,10 +24,10 @@ impl FileContextPicker { pub fn new( context_picker: WeakView, workspace: WeakView, - message_editor: WeakView, + context_strip: WeakView, cx: &mut ViewContext, ) -> Self { - let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor); + let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_strip); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } @@ -49,7 +49,7 @@ impl Render for FileContextPicker { pub struct FileContextPickerDelegate { context_picker: WeakView, workspace: WeakView, - message_editor: WeakView, + context_strip: WeakView, matches: Vec, selected_index: usize, } @@ -58,12 +58,12 @@ impl FileContextPickerDelegate { pub fn new( context_picker: WeakView, workspace: WeakView, - message_editor: WeakView, + context_strip: WeakView, ) -> Self { Self { context_picker, workspace, - message_editor, + context_strip, matches: Vec::new(), selected_index: 0, } @@ -214,24 +214,22 @@ impl PickerDelegate for FileContextPickerDelegate { let buffer = open_buffer_task.await?; this.update(&mut cx, |this, cx| { - this.delegate - .message_editor - .update(cx, |message_editor, cx| { - let mut text = String::new(); - text.push_str(&codeblock_fence_for_path(Some(&path), None)); - text.push_str(&buffer.read(cx).text()); - if !text.ends_with('\n') { - text.push('\n'); - } - - text.push_str("```\n"); - - message_editor.insert_context( - ContextKind::File, - path.to_string_lossy().to_string(), - text, - ); - }) + this.delegate.context_strip.update(cx, |context_strip, cx| { + let mut text = String::new(); + text.push_str(&codeblock_fence_for_path(Some(&path), None)); + text.push_str(&buffer.read(cx).text()); + if !text.ends_with('\n') { + text.push('\n'); + } + + text.push_str("```\n"); + + context_strip.insert_context( + ContextKind::File, + path.to_string_lossy().to_string(), + text, + ); + }) })??; anyhow::Ok(()) diff --git a/crates/assistant2/src/context_picker/thread_context_picker.rs b/crates/assistant2/src/context_picker/thread_context_picker.rs index 61b1ba0f058056..57b2ee9ce06c5c 100644 --- a/crates/assistant2/src/context_picker/thread_context_picker.rs +++ b/crates/assistant2/src/context_picker/thread_context_picker.rs @@ -7,7 +7,7 @@ use ui::{prelude::*, ListItem}; use crate::context::ContextKind; use crate::context_picker::ContextPicker; -use crate::message_editor::MessageEditor; +use crate::context_strip::ContextStrip; use crate::thread::ThreadId; use crate::thread_store::ThreadStore; @@ -19,11 +19,11 @@ impl ThreadContextPicker { pub fn new( thread_store: WeakModel, context_picker: WeakView, - message_editor: WeakView, + context_strip: WeakView, cx: &mut ViewContext, ) -> Self { let delegate = - ThreadContextPickerDelegate::new(thread_store, context_picker, message_editor); + ThreadContextPickerDelegate::new(thread_store, context_picker, context_strip); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); ThreadContextPicker { picker } @@ -51,7 +51,7 @@ struct ThreadContextEntry { pub struct ThreadContextPickerDelegate { thread_store: WeakModel, context_picker: WeakView, - message_editor: WeakView, + context_strip: WeakView, matches: Vec, selected_index: usize, } @@ -60,12 +60,12 @@ impl ThreadContextPickerDelegate { pub fn new( thread_store: WeakModel, context_picker: WeakView, - message_editor: WeakView, + context_strip: WeakView, ) -> Self { ThreadContextPickerDelegate { thread_store, context_picker, - message_editor, + context_strip, matches: Vec::new(), selected_index: 0, } @@ -157,8 +157,8 @@ impl PickerDelegate for ThreadContextPickerDelegate { return; }; - self.message_editor - .update(cx, |message_editor, cx| { + self.context_strip + .update(cx, |context_strip, cx| { let text = thread.update(cx, |thread, _cx| { let mut text = String::new(); @@ -177,7 +177,7 @@ impl PickerDelegate for ThreadContextPickerDelegate { text }); - message_editor.insert_context(ContextKind::Thread, entry.summary.clone(), text); + context_strip.insert_context(ContextKind::Thread, entry.summary.clone(), text); }) .ok(); } diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs new file mode 100644 index 00000000000000..fd5b23a16aeba5 --- /dev/null +++ b/crates/assistant2/src/context_strip.rs @@ -0,0 +1,101 @@ +use std::rc::Rc; + +use gpui::{View, WeakModel, WeakView}; +use ui::{prelude::*, IconButtonShape, PopoverMenu, PopoverMenuHandle, Tooltip}; +use workspace::Workspace; + +use crate::context::{Context, ContextId, ContextKind}; +use crate::context_picker::ContextPicker; +use crate::thread_store::ThreadStore; +use crate::ui::ContextPill; + +pub struct ContextStrip { + context: Vec, + next_context_id: ContextId, + context_picker: View, + pub(crate) context_picker_handle: PopoverMenuHandle, +} + +impl ContextStrip { + pub fn new( + workspace: WeakView, + thread_store: WeakModel, + cx: &mut ViewContext, + ) -> Self { + let weak_self = cx.view().downgrade(); + + Self { + context: Vec::new(), + next_context_id: ContextId(0), + context_picker: cx.new_view(|cx| { + ContextPicker::new(workspace.clone(), thread_store.clone(), weak_self, cx) + }), + context_picker_handle: PopoverMenuHandle::default(), + } + } + + pub fn drain(&mut self) -> Vec { + self.context.drain(..).collect() + } + + pub fn insert_context( + &mut self, + kind: ContextKind, + name: impl Into, + text: impl Into, + ) { + self.context.push(Context { + id: self.next_context_id.post_inc(), + name: name.into(), + kind, + text: text.into(), + }); + } +} + +impl Render for ContextStrip { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let context_picker = self.context_picker.clone(); + + h_flex() + .flex_wrap() + .gap_2() + .child( + PopoverMenu::new("context-picker") + .menu(move |_cx| Some(context_picker.clone())) + .trigger( + IconButton::new("add-context", IconName::Plus) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small), + ) + .attach(gpui::AnchorCorner::TopLeft) + .anchor(gpui::AnchorCorner::BottomLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(-16.0), + }) + .with_handle(self.context_picker_handle.clone()), + ) + .children(self.context.iter().map(|context| { + ContextPill::new(context.clone()).on_remove({ + let context = context.clone(); + Rc::new(cx.listener(move |this, _event, cx| { + this.context.retain(|other| other.id != context.id); + cx.notify(); + })) + }) + })) + .when(!self.context.is_empty(), |parent| { + parent.child( + IconButton::new("remove-all-context", IconName::Eraser) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip(move |cx| Tooltip::text("Remove All Context", cx)) + .on_click(cx.listener(|this, _event, cx| { + this.context.clear(); + cx.notify(); + })), + ) + }) + } +} diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index f21caf8a7645c6..f27d0789bb77b1 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,31 +1,21 @@ -use std::rc::Rc; - use editor::{Editor, EditorElement, EditorStyle}; use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView}; use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use settings::Settings; use theme::ThemeSettings; -use ui::{ - prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding, - PopoverMenu, PopoverMenuHandle, Tooltip, -}; +use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, Tooltip}; use workspace::Workspace; -use crate::context::{Context, ContextId, ContextKind}; -use crate::context_picker::ContextPicker; +use crate::context_strip::ContextStrip; use crate::thread::{RequestKind, Thread}; use crate::thread_store::ThreadStore; -use crate::ui::ContextPill; use crate::{Chat, ToggleModelSelector}; pub struct MessageEditor { thread: Model, editor: View, - context: Vec, - next_context_id: ContextId, - context_picker: View, - pub(crate) context_picker_handle: PopoverMenuHandle, + context_strip: View, language_model_selector: View, use_tools: bool, } @@ -37,7 +27,6 @@ impl MessageEditor { thread: Model, cx: &mut ViewContext, ) -> Self { - let weak_self = cx.view().downgrade(); Self { thread, editor: cx.new_view(|cx| { @@ -46,12 +35,8 @@ impl MessageEditor { editor }), - context: Vec::new(), - next_context_id: ContextId(0), - context_picker: cx.new_view(|cx| { - ContextPicker::new(workspace.clone(), thread_store.clone(), weak_self, cx) - }), - context_picker_handle: PopoverMenuHandle::default(), + context_strip: cx + .new_view(|cx| ContextStrip::new(workspace.clone(), thread_store.clone(), cx)), language_model_selector: cx.new_view(|cx| { LanguageModelSelector::new( |model, _cx| { @@ -64,20 +49,6 @@ impl MessageEditor { } } - pub fn insert_context( - &mut self, - kind: ContextKind, - name: impl Into, - text: impl Into, - ) { - self.context.push(Context { - id: self.next_context_id.post_inc(), - name: name.into(), - kind, - text: text.into(), - }); - } - fn chat(&mut self, _: &Chat, cx: &mut ViewContext) { self.send_to_model(RequestKind::Chat, cx); } @@ -104,7 +75,7 @@ impl MessageEditor { editor.clear(cx); text }); - let context = self.context.drain(..).collect::>(); + let context = self.context_strip.update(cx, |this, _cx| this.drain()); self.thread.update(cx, |thread, cx| { thread.insert_user_message(user_message, context, cx); @@ -190,7 +161,6 @@ impl Render for MessageEditor { let font_size = TextSize::Default.rems(cx); let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; let focus_handle = self.editor.focus_handle(cx); - let context_picker = self.context_picker.clone(); v_flex() .key_context("MessageEditor") @@ -199,48 +169,7 @@ impl Render for MessageEditor { .gap_2() .p_2() .bg(cx.theme().colors().editor_background) - .child( - h_flex() - .flex_wrap() - .gap_2() - .child( - PopoverMenu::new("context-picker") - .menu(move |_cx| Some(context_picker.clone())) - .trigger( - IconButton::new("add-context", IconName::Plus) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small), - ) - .attach(gpui::AnchorCorner::TopLeft) - .anchor(gpui::AnchorCorner::BottomLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(-16.0), - }) - .with_handle(self.context_picker_handle.clone()), - ) - .children(self.context.iter().map(|context| { - ContextPill::new(context.clone()).on_remove({ - let context = context.clone(); - Rc::new(cx.listener(move |this, _event, cx| { - this.context.retain(|other| other.id != context.id); - cx.notify(); - })) - }) - })) - .when(!self.context.is_empty(), |parent| { - parent.child( - IconButton::new("remove-all-context", IconName::Eraser) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip(move |cx| Tooltip::text("Remove All Context", cx)) - .on_click(cx.listener(|this, _event, cx| { - this.context.clear(); - cx.notify(); - })), - ) - }), - ) + .child(self.context_strip.clone()) .child({ let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { From eff61ee764153b56cc1d5c7c8c5d7f20727147bd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 13:26:11 -0500 Subject: [PATCH 13/32] assistant2: Remove `WeakView` optionality for inline assist (#22099) This PR removes the optionality for the `WeakView` that we pass to the inline assist. This was always `Some` in practice, so it seems we don't need to have it be an `Option`. Release Notes: - N/A --- crates/assistant2/src/inline_assistant.rs | 20 ++++++++----------- .../src/terminal_inline_assistant.rs | 12 ++++------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index 4d4f6aaaf6ef0c..020e20291375c3 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -212,12 +212,12 @@ impl InlineAssistant { let handle_assist = |cx: &mut ViewContext| match inline_assist_target { InlineAssistTarget::Editor(active_editor) => { InlineAssistant::update_global(cx, |assistant, cx| { - assistant.assist(&active_editor, Some(cx.view().downgrade()), cx) + assistant.assist(&active_editor, cx.view().downgrade(), cx) }) } InlineAssistTarget::Terminal(active_terminal) => { TerminalInlineAssistant::update_global(cx, |assistant, cx| { - assistant.assist(&active_terminal, Some(cx.view().downgrade()), cx) + assistant.assist(&active_terminal, cx.view().downgrade(), cx) }) } }; @@ -264,7 +264,7 @@ impl InlineAssistant { pub fn assist( &mut self, editor: &View, - workspace: Option>, + workspace: WeakView, cx: &mut WindowContext, ) { let (snapshot, initial_selections) = editor.update(cx, |editor, cx| { @@ -429,7 +429,7 @@ impl InlineAssistant { initial_prompt: String, initial_transaction_id: Option, focus: bool, - workspace: Option>, + workspace: WeakView, cx: &mut WindowContext, ) -> InlineAssistId { let assist_group_id = self.next_assist_group_id.post_inc(); @@ -2166,7 +2166,7 @@ pub struct InlineAssist { decorations: Option, codegen: Model, _subscriptions: Vec, - workspace: Option>, + workspace: WeakView, } impl InlineAssist { @@ -2180,7 +2180,7 @@ impl InlineAssist { end_block_id: CustomBlockId, range: Range, codegen: Model, - workspace: Option>, + workspace: WeakView, cx: &mut WindowContext, ) -> Self { let prompt_editor_focus_handle = prompt_editor.focus_handle(cx); @@ -2240,11 +2240,7 @@ impl InlineAssist { if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) { if assist.decorations.is_none() { - if let Some(workspace) = assist - .workspace - .as_ref() - .and_then(|workspace| workspace.upgrade()) - { + if let Some(workspace) = assist.workspace.upgrade() { let error = format!("Inline assistant error: {}", error); workspace.update(cx, |workspace, cx| { struct InlineAssistantError; @@ -3387,7 +3383,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { "Fix Diagnostics".into(), None, true, - Some(workspace), + workspace, cx, ); assistant.start_assist(assist_id, cx); diff --git a/crates/assistant2/src/terminal_inline_assistant.rs b/crates/assistant2/src/terminal_inline_assistant.rs index 1c5574e1d96f53..e3b81979a86ebf 100644 --- a/crates/assistant2/src/terminal_inline_assistant.rs +++ b/crates/assistant2/src/terminal_inline_assistant.rs @@ -82,7 +82,7 @@ impl TerminalInlineAssistant { pub fn assist( &mut self, terminal_view: &View, - workspace: Option>, + workspace: WeakView, cx: &mut WindowContext, ) { let terminal = terminal_view.read(cx).terminal().clone(); @@ -361,7 +361,7 @@ struct TerminalInlineAssist { terminal: WeakView, prompt_editor: Option>, codegen: Model, - workspace: Option>, + workspace: WeakView, _subscriptions: Vec, } @@ -370,7 +370,7 @@ impl TerminalInlineAssist { assist_id: TerminalInlineAssistId, terminal: &View, prompt_editor: View, - workspace: Option>, + workspace: WeakView, cx: &mut WindowContext, ) -> Self { let codegen = prompt_editor.read(cx).codegen.clone(); @@ -396,11 +396,7 @@ impl TerminalInlineAssist { if let CodegenStatus::Error(error) = &codegen.read(cx).status { if assist.prompt_editor.is_none() { - if let Some(workspace) = assist - .workspace - .as_ref() - .and_then(|workspace| workspace.upgrade()) - { + if let Some(workspace) = assist.workspace.upgrade() { let error = format!("Terminal inline assistant error: {}", error); workspace.update(cx, |workspace, cx| { From 426f94b310f73e34cdb98e34f8a58bad004f04bb Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 16 Dec 2024 13:39:40 -0500 Subject: [PATCH 14/32] git_ui: Update todos (#22100) `todo!()` -> `TODO` Release Notes: - N/A --- crates/git_ui/src/git_panel.rs | 18 +++++++++--------- crates/git_ui/src/git_ui.rs | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ea7585d9785402..d64dd5ba502c96 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -188,12 +188,12 @@ impl GitPanel { } fn should_show_scrollbar(_cx: &AppContext) -> bool { - // todo!(): plug into settings + // TODO: plug into settings true } fn should_autohide_scrollbar(_cx: &AppContext) -> bool { - // todo!(): plug into settings + // TODO: plug into settings true } @@ -255,34 +255,34 @@ impl GitPanel { impl GitPanel { fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext) { - // todo!(): Implement stage all + // TODO: Implement stage all println!("Stage all triggered"); } fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext) { - // todo!(): Implement unstage all + // TODO: Implement unstage all println!("Unstage all triggered"); } fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext) { - // todo!(): Implement discard all + // TODO: Implement discard all println!("Discard all triggered"); } /// Commit all staged changes fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext) { - // todo!(): Implement commit all staged + // TODO: Implement commit all staged println!("Commit staged changes triggered"); } /// Commit all changes, regardless of whether they are staged or not fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext) { - // todo!(): Implement commit all changes + // TODO: Implement commit all changes println!("Commit all changes triggered"); } fn all_staged(&self) -> bool { - // todo!(): Implement all_staged + // TODO: Implement all_staged true } @@ -378,7 +378,7 @@ impl GitPanel { } } - // todo!(): Update expanded directory state + // TODO: Update expanded directory state fn update_visible_entries( &mut self, new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 19aa554073918f..5aa9a361fa11db 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -41,7 +41,7 @@ const REMOVED_COLOR: Hsla = Hsla { a: 1.0, }; -// todo!(): Add updated status colors to theme +// TODO: Add updated status colors to theme pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement { match status { GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)), From ec741d61ed3c82e864d7703965fe35d5d1cea843 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:42:09 -0300 Subject: [PATCH 15/32] assistant2: Adjust thread history list item visuals (#21998) Most notably, adding the `outlined` property in the `ListItem` component. Screenshot 2024-12-13 at 20 35 39 Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 9 +++++---- crates/assistant2/src/thread_history.rs | 14 +++++++++++--- crates/ui/src/components/list/list_item.rs | 15 ++++++++++++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 2ab28077f111b3..07902b5c835abe 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -323,10 +323,11 @@ impl AssistantPanel { .when(!recent_threads.is_empty(), |parent| { parent .child( - h_flex() - .w_full() - .justify_center() - .child(Label::new("Recent Threads:").size(LabelSize::Small)), + h_flex().w_full().justify_center().child( + Label::new("Recent Threads:") + .size(LabelSize::Small) + .color(Color::Muted), + ), ) .child( v_flex().gap_2().children( diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs index f183276f7b575b..3eb333688a1544 100644 --- a/crates/assistant2/src/thread_history.rs +++ b/crates/assistant2/src/thread_history.rs @@ -2,7 +2,7 @@ use gpui::{ uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView, }; use time::{OffsetDateTime, UtcOffset}; -use ui::{prelude::*, IconButtonShape, ListItem}; +use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip}; use crate::thread::Thread; use crate::thread_store::ThreadStore; @@ -117,17 +117,25 @@ impl RenderOnce for PastThread { .unwrap_or(UtcOffset::UTC), time_format::TimestampFormat::EnhancedAbsolute, ); + ListItem::new(("past-thread", self.thread.entity_id())) + .outlined() .start_slot(Icon::new(IconName::MessageBubbles)) - .child(Label::new(summary)) + .spacing(ListItemSpacing::Sparse) + .child(Label::new(summary).size(LabelSize::Small)) .end_slot( h_flex() .gap_2() - .child(Label::new(thread_timestamp).color(Color::Disabled)) + .child( + Label::new(thread_timestamp) + .color(Color::Disabled) + .size(LabelSize::Small), + ) .child( IconButton::new("delete", IconName::TrashAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::text("Delete Thread", cx)) .on_click({ let assistant_panel = self.assistant_panel.clone(); let id = id.clone(); diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 48d1e9b03d3799..908c873f9d740d 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -38,6 +38,7 @@ pub struct ListItem { on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, selectable: bool, + outlined: bool, overflow_x: bool, focused: Option, } @@ -62,6 +63,7 @@ impl ListItem { tooltip: None, children: SmallVec::new(), selectable: true, + outlined: false, overflow_x: false, focused: None, } @@ -138,6 +140,11 @@ impl ListItem { self } + pub fn outlined(mut self) -> Self { + self.outlined = true; + self + } + pub fn overflow_x(mut self) -> Self { self.overflow_x = true; self @@ -203,6 +210,7 @@ impl RenderOnce for ListItem { .child( h_flex() .id("inner_list_item") + .group("list_item") .w_full() .relative() .items_center() @@ -212,7 +220,6 @@ impl RenderOnce for ListItem { ListItemSpacing::Dense => this, ListItemSpacing::Sparse => this.py_1(), }) - .group("list_item") .when(self.inset && !self.disabled, |this| { this // TODO: Add focus state @@ -238,6 +245,12 @@ impl RenderOnce for ListItem { .when_some(self.on_click, |this, on_click| { this.cursor_pointer().on_click(on_click) }) + .when(self.outlined, |this| { + this.border_1() + .border_color(cx.theme().colors().border) + .rounded_md() + .overflow_hidden() + }) .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { this.on_mouse_down(MouseButton::Right, move |event, cx| { (on_mouse_down)(event, cx) From 082469e173595b031a08c42f941a693a5028fc29 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:40:02 -0500 Subject: [PATCH 16/32] docs: Use `rev` instead of `commit` for extension grammars (#22105) `rev` is the preferred key --- docs/src/extensions/languages.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index fc2c42c74aee5b..061496caf75812 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -51,10 +51,10 @@ Zed uses the [Tree-sitter](https://tree-sitter.github.io) parsing library to pro ```toml [grammars.gleam] repository = "https://github.com/gleam-lang/tree-sitter-gleam" -commit = "58b7cac8fc14c92b0677c542610d8738c373fa81" +rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" ``` -The `repository` field must specify a repository where the Tree-sitter grammar should be loaded from, and the `commit` field must contain the SHA of the Git commit to use. An extension can provide multiple grammars by referencing multiple tree-sitter repositories. +The `repository` field must specify a repository where the Tree-sitter grammar should be loaded from, and the `rev` field must contain a Git revision to use, such as the SHA of a Git commit. An extension can provide multiple grammars by referencing multiple tree-sitter repositories. ## Tree-sitter Queries From 4bf005ef528243ac9f1127821c3014e04abe7fd0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 15:46:28 -0500 Subject: [PATCH 17/32] assistant2: Wire up context picker with inline assist (#22106) This PR wire up the context picker with the inline assist. UI is not finalized. Release Notes: - N/A --------- Co-authored-by: Richard Co-authored-by: Agus --- crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 4 + crates/assistant2/src/context.rs | 49 ++++ crates/assistant2/src/context_picker.rs | 83 +++--- .../context_picker/fetch_context_picker.rs | 20 +- .../src/context_picker/file_context_picker.rs | 18 +- .../context_picker/thread_context_picker.rs | 18 +- crates/assistant2/src/context_store.rs | 47 ++++ crates/assistant2/src/context_strip.rs | 63 ++--- crates/assistant2/src/inline_assistant.rs | 264 ++++++++++++------ crates/assistant2/src/message_editor.rs | 17 +- crates/assistant2/src/thread.rs | 48 +--- 12 files changed, 391 insertions(+), 241 deletions(-) create mode 100644 crates/assistant2/src/context_store.rs diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index d7d37fc78bb328..64f1416fa44bef 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -3,6 +3,7 @@ mod assistant_panel; mod assistant_settings; mod context; mod context_picker; +mod context_store; mod context_strip; mod inline_assistant; mod message_editor; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 07902b5c835abe..1d21413583e7e4 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -110,6 +110,10 @@ impl AssistantPanel { self.local_timezone } + pub(crate) fn thread_store(&self) -> &Model { + &self.thread_store + } + fn new_thread(&mut self, cx: &mut ViewContext) { let thread = self .thread_store diff --git a/crates/assistant2/src/context.rs b/crates/assistant2/src/context.rs index 414093dc3136ac..b83e3bad12cad8 100644 --- a/crates/assistant2/src/context.rs +++ b/crates/assistant2/src/context.rs @@ -1,4 +1,5 @@ use gpui::SharedString; +use language_model::{LanguageModelRequestMessage, MessageContent}; use serde::{Deserialize, Serialize}; use util::post_inc; @@ -26,3 +27,51 @@ pub enum ContextKind { FetchedUrl, Thread, } + +pub fn attach_context_to_message( + message: &mut LanguageModelRequestMessage, + context: impl IntoIterator, +) { + let mut file_context = String::new(); + let mut fetch_context = String::new(); + let mut thread_context = String::new(); + + for context in context.into_iter() { + match context.kind { + ContextKind::File => { + file_context.push_str(&context.text); + file_context.push('\n'); + } + ContextKind::FetchedUrl => { + fetch_context.push_str(&context.name); + fetch_context.push('\n'); + fetch_context.push_str(&context.text); + fetch_context.push('\n'); + } + ContextKind::Thread => { + thread_context.push_str(&context.name); + thread_context.push('\n'); + thread_context.push_str(&context.text); + thread_context.push('\n'); + } + } + } + + let mut context_text = String::new(); + if !file_context.is_empty() { + context_text.push_str("The following files are available:\n"); + context_text.push_str(&file_context); + } + + if !fetch_context.is_empty() { + context_text.push_str("The following fetched results are available\n"); + context_text.push_str(&fetch_context); + } + + if !thread_context.is_empty() { + context_text.push_str("The following previous conversation threads are available\n"); + context_text.push_str(&thread_context); + } + + message.content.push(MessageContent::Text(context_text)); +} diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs index 5766801bb2dc2b..9e6086f86adf66 100644 --- a/crates/assistant2/src/context_picker.rs +++ b/crates/assistant2/src/context_picker.rs @@ -16,7 +16,7 @@ use workspace::Workspace; use crate::context_picker::fetch_context_picker::FetchContextPicker; use crate::context_picker::file_context_picker::FileContextPicker; use crate::context_picker::thread_context_picker::ThreadContextPicker; -use crate::context_strip::ContextStrip; +use crate::context_store::ContextStore; use crate::thread_store::ThreadStore; #[derive(Debug, Clone)] @@ -35,37 +35,42 @@ pub(super) struct ContextPicker { impl ContextPicker { pub fn new( workspace: WeakView, - thread_store: WeakModel, - context_strip: WeakView, + thread_store: Option>, + context_store: WeakModel, cx: &mut ViewContext, ) -> Self { + let mut entries = vec![ + ContextPickerEntry { + name: "directory".into(), + description: "Insert any directory".into(), + icon: IconName::Folder, + }, + ContextPickerEntry { + name: "file".into(), + description: "Insert any file".into(), + icon: IconName::File, + }, + ContextPickerEntry { + name: "fetch".into(), + description: "Fetch content from URL".into(), + icon: IconName::Globe, + }, + ]; + + if thread_store.is_some() { + entries.push(ContextPickerEntry { + name: "thread".into(), + description: "Insert any thread".into(), + icon: IconName::MessageBubbles, + }); + } + let delegate = ContextPickerDelegate { context_picker: cx.view().downgrade(), workspace, thread_store, - context_strip, - entries: vec![ - ContextPickerEntry { - name: "directory".into(), - description: "Insert any directory".into(), - icon: IconName::Folder, - }, - ContextPickerEntry { - name: "file".into(), - description: "Insert any file".into(), - icon: IconName::File, - }, - ContextPickerEntry { - name: "fetch".into(), - description: "Fetch content from URL".into(), - icon: IconName::Globe, - }, - ContextPickerEntry { - name: "thread".into(), - description: "Insert any thread".into(), - icon: IconName::MessageBubbles, - }, - ], + context_store, + entries, selected_ix: 0, }; @@ -121,8 +126,8 @@ struct ContextPickerEntry { pub(crate) struct ContextPickerDelegate { context_picker: WeakView, workspace: WeakView, - thread_store: WeakModel, - context_strip: WeakView, + thread_store: Option>, + context_store: WeakModel, entries: Vec, selected_ix: usize, } @@ -161,7 +166,7 @@ impl PickerDelegate for ContextPickerDelegate { FileContextPicker::new( self.context_picker.clone(), self.workspace.clone(), - self.context_strip.clone(), + self.context_store.clone(), cx, ) })); @@ -171,20 +176,22 @@ impl PickerDelegate for ContextPickerDelegate { FetchContextPicker::new( self.context_picker.clone(), self.workspace.clone(), - self.context_strip.clone(), + self.context_store.clone(), cx, ) })); } "thread" => { - this.mode = ContextPickerMode::Thread(cx.new_view(|cx| { - ThreadContextPicker::new( - self.thread_store.clone(), - self.context_picker.clone(), - self.context_strip.clone(), - cx, - ) - })); + if let Some(thread_store) = self.thread_store.as_ref() { + this.mode = ContextPickerMode::Thread(cx.new_view(|cx| { + ThreadContextPicker::new( + thread_store.clone(), + self.context_picker.clone(), + self.context_store.clone(), + cx, + ) + })); + } } _ => {} } diff --git a/crates/assistant2/src/context_picker/fetch_context_picker.rs b/crates/assistant2/src/context_picker/fetch_context_picker.rs index dd0a9d3d5a2f26..070c72eec5311b 100644 --- a/crates/assistant2/src/context_picker/fetch_context_picker.rs +++ b/crates/assistant2/src/context_picker/fetch_context_picker.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{bail, Context as _, Result}; use futures::AsyncReadExt as _; -use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView}; +use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler}; use http_client::{AsyncBody, HttpClientWithUrl}; use picker::{Picker, PickerDelegate}; @@ -13,7 +13,7 @@ use workspace::Workspace; use crate::context::ContextKind; use crate::context_picker::ContextPicker; -use crate::context_strip::ContextStrip; +use crate::context_store::ContextStore; pub struct FetchContextPicker { picker: View>, @@ -23,10 +23,10 @@ impl FetchContextPicker { pub fn new( context_picker: WeakView, workspace: WeakView, - context_strip: WeakView, + context_store: WeakModel, cx: &mut ViewContext, ) -> Self { - let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_strip); + let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } @@ -55,7 +55,7 @@ enum ContentType { pub struct FetchContextPickerDelegate { context_picker: WeakView, workspace: WeakView, - context_strip: WeakView, + context_store: WeakModel, url: String, } @@ -63,12 +63,12 @@ impl FetchContextPickerDelegate { pub fn new( context_picker: WeakView, workspace: WeakView, - context_strip: WeakView, + context_store: WeakModel, ) -> Self { FetchContextPickerDelegate { context_picker, workspace, - context_strip, + context_store, url: String::new(), } } @@ -189,9 +189,9 @@ impl PickerDelegate for FetchContextPickerDelegate { this.update(&mut cx, |this, cx| { this.delegate - .context_strip - .update(cx, |context_strip, _cx| { - context_strip.insert_context(ContextKind::FetchedUrl, url, text); + .context_store + .update(cx, |context_store, _cx| { + context_store.insert_context(ContextKind::FetchedUrl, url, text); }) })??; diff --git a/crates/assistant2/src/context_picker/file_context_picker.rs b/crates/assistant2/src/context_picker/file_context_picker.rs index a0e89aa7000c20..be34283826e1f9 100644 --- a/crates/assistant2/src/context_picker/file_context_picker.rs +++ b/crates/assistant2/src/context_picker/file_context_picker.rs @@ -5,7 +5,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use fuzzy::PathMatch; -use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView}; +use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView}; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, WorktreeId}; use ui::{prelude::*, ListItem}; @@ -14,7 +14,7 @@ use workspace::Workspace; use crate::context::ContextKind; use crate::context_picker::ContextPicker; -use crate::context_strip::ContextStrip; +use crate::context_store::ContextStore; pub struct FileContextPicker { picker: View>, @@ -24,10 +24,10 @@ impl FileContextPicker { pub fn new( context_picker: WeakView, workspace: WeakView, - context_strip: WeakView, + context_store: WeakModel, cx: &mut ViewContext, ) -> Self { - let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_strip); + let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } @@ -49,7 +49,7 @@ impl Render for FileContextPicker { pub struct FileContextPickerDelegate { context_picker: WeakView, workspace: WeakView, - context_strip: WeakView, + context_store: WeakModel, matches: Vec, selected_index: usize, } @@ -58,12 +58,12 @@ impl FileContextPickerDelegate { pub fn new( context_picker: WeakView, workspace: WeakView, - context_strip: WeakView, + context_store: WeakModel, ) -> Self { Self { context_picker, workspace, - context_strip, + context_store, matches: Vec::new(), selected_index: 0, } @@ -214,7 +214,7 @@ impl PickerDelegate for FileContextPickerDelegate { let buffer = open_buffer_task.await?; this.update(&mut cx, |this, cx| { - this.delegate.context_strip.update(cx, |context_strip, cx| { + this.delegate.context_store.update(cx, |context_store, cx| { let mut text = String::new(); text.push_str(&codeblock_fence_for_path(Some(&path), None)); text.push_str(&buffer.read(cx).text()); @@ -224,7 +224,7 @@ impl PickerDelegate for FileContextPickerDelegate { text.push_str("```\n"); - context_strip.insert_context( + context_store.insert_context( ContextKind::File, path.to_string_lossy().to_string(), text, diff --git a/crates/assistant2/src/context_picker/thread_context_picker.rs b/crates/assistant2/src/context_picker/thread_context_picker.rs index 57b2ee9ce06c5c..61395c4224d1ff 100644 --- a/crates/assistant2/src/context_picker/thread_context_picker.rs +++ b/crates/assistant2/src/context_picker/thread_context_picker.rs @@ -7,7 +7,7 @@ use ui::{prelude::*, ListItem}; use crate::context::ContextKind; use crate::context_picker::ContextPicker; -use crate::context_strip::ContextStrip; +use crate::context_store; use crate::thread::ThreadId; use crate::thread_store::ThreadStore; @@ -19,11 +19,11 @@ impl ThreadContextPicker { pub fn new( thread_store: WeakModel, context_picker: WeakView, - context_strip: WeakView, + context_store: WeakModel, cx: &mut ViewContext, ) -> Self { let delegate = - ThreadContextPickerDelegate::new(thread_store, context_picker, context_strip); + ThreadContextPickerDelegate::new(thread_store, context_picker, context_store); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); ThreadContextPicker { picker } @@ -51,7 +51,7 @@ struct ThreadContextEntry { pub struct ThreadContextPickerDelegate { thread_store: WeakModel, context_picker: WeakView, - context_strip: WeakView, + context_store: WeakModel, matches: Vec, selected_index: usize, } @@ -60,12 +60,12 @@ impl ThreadContextPickerDelegate { pub fn new( thread_store: WeakModel, context_picker: WeakView, - context_strip: WeakView, + context_store: WeakModel, ) -> Self { ThreadContextPickerDelegate { thread_store, context_picker, - context_strip, + context_store, matches: Vec::new(), selected_index: 0, } @@ -157,8 +157,8 @@ impl PickerDelegate for ThreadContextPickerDelegate { return; }; - self.context_strip - .update(cx, |context_strip, cx| { + self.context_store + .update(cx, |context_store, cx| { let text = thread.update(cx, |thread, _cx| { let mut text = String::new(); @@ -177,7 +177,7 @@ impl PickerDelegate for ThreadContextPickerDelegate { text }); - context_strip.insert_context(ContextKind::Thread, entry.summary.clone(), text); + context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text); }) .ok(); } diff --git a/crates/assistant2/src/context_store.rs b/crates/assistant2/src/context_store.rs new file mode 100644 index 00000000000000..febd1f597dbfad --- /dev/null +++ b/crates/assistant2/src/context_store.rs @@ -0,0 +1,47 @@ +use gpui::SharedString; + +use crate::context::{Context, ContextId, ContextKind}; + +pub struct ContextStore { + context: Vec, + next_context_id: ContextId, +} + +impl ContextStore { + pub fn new() -> Self { + Self { + context: Vec::new(), + next_context_id: ContextId(0), + } + } + + pub fn context(&self) -> &Vec { + &self.context + } + + pub fn drain(&mut self) -> Vec { + self.context.drain(..).collect() + } + + pub fn clear(&mut self) { + self.context.clear(); + } + + pub fn insert_context( + &mut self, + kind: ContextKind, + name: impl Into, + text: impl Into, + ) { + self.context.push(Context { + id: self.next_context_id.post_inc(), + name: name.into(), + kind, + text: text.into(), + }); + } + + pub fn remove_context(&mut self, id: &ContextId) { + self.context.retain(|context| context.id != *id); + } +} diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs index fd5b23a16aeba5..c5b6164b4a6dd3 100644 --- a/crates/assistant2/src/context_strip.rs +++ b/crates/assistant2/src/context_strip.rs @@ -1,60 +1,45 @@ use std::rc::Rc; -use gpui::{View, WeakModel, WeakView}; +use gpui::{Model, View, WeakModel, WeakView}; use ui::{prelude::*, IconButtonShape, PopoverMenu, PopoverMenuHandle, Tooltip}; use workspace::Workspace; -use crate::context::{Context, ContextId, ContextKind}; use crate::context_picker::ContextPicker; +use crate::context_store::ContextStore; use crate::thread_store::ThreadStore; use crate::ui::ContextPill; pub struct ContextStrip { - context: Vec, - next_context_id: ContextId, + context_store: Model, context_picker: View, pub(crate) context_picker_handle: PopoverMenuHandle, } impl ContextStrip { pub fn new( + context_store: Model, workspace: WeakView, - thread_store: WeakModel, + thread_store: Option>, cx: &mut ViewContext, ) -> Self { - let weak_self = cx.view().downgrade(); - Self { - context: Vec::new(), - next_context_id: ContextId(0), + context_store: context_store.clone(), context_picker: cx.new_view(|cx| { - ContextPicker::new(workspace.clone(), thread_store.clone(), weak_self, cx) + ContextPicker::new( + workspace.clone(), + thread_store.clone(), + context_store.downgrade(), + cx, + ) }), context_picker_handle: PopoverMenuHandle::default(), } } - - pub fn drain(&mut self) -> Vec { - self.context.drain(..).collect() - } - - pub fn insert_context( - &mut self, - kind: ContextKind, - name: impl Into, - text: impl Into, - ) { - self.context.push(Context { - id: self.next_context_id.post_inc(), - name: name.into(), - kind, - text: text.into(), - }); - } } impl Render for ContextStrip { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let context = self.context_store.read(cx).context(); let context_picker = self.context_picker.clone(); h_flex() @@ -76,25 +61,31 @@ impl Render for ContextStrip { }) .with_handle(self.context_picker_handle.clone()), ) - .children(self.context.iter().map(|context| { + .children(context.iter().map(|context| { ContextPill::new(context.clone()).on_remove({ let context = context.clone(); - Rc::new(cx.listener(move |this, _event, cx| { - this.context.retain(|other| other.id != context.id); + let context_store = self.context_store.clone(); + Rc::new(cx.listener(move |_this, _event, cx| { + context_store.update(cx, |this, _cx| { + this.remove_context(&context.id); + }); cx.notify(); })) }) })) - .when(!self.context.is_empty(), |parent| { + .when(!context.is_empty(), |parent| { parent.child( IconButton::new("remove-all-context", IconName::Eraser) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .tooltip(move |cx| Tooltip::text("Remove All Context", cx)) - .on_click(cx.listener(|this, _event, cx| { - this.context.clear(); - cx.notify(); - })), + .on_click({ + let context_store = self.context_store.clone(); + cx.listener(move |_this, _event, cx| { + context_store.update(cx, |this, _cx| this.clear()); + cx.notify(); + }) + }), ) }) } diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index 020e20291375c3..61d415a2a82dfa 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -1,3 +1,8 @@ +use crate::context::attach_context_to_message; +use crate::context_store::ContextStore; +use crate::context_strip::ContextStrip; +use crate::thread_store::ThreadStore; +use crate::AssistantPanel; use crate::{ assistant_settings::AssistantSettings, prompts::PromptBuilder, @@ -24,7 +29,8 @@ use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, Stre use gpui::{ anchored, deferred, point, AnyElement, AppContext, ClickEvent, CursorStyle, EventEmitter, FocusHandle, FocusableView, FontWeight, Global, HighlightStyle, Model, ModelContext, - Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView, WindowContext, + Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakModel, WeakView, + WindowContext, }; use language::{Buffer, IndentKind, Point, Selection, TransactionId}; use language_model::{ @@ -178,10 +184,16 @@ impl InlineAssistant { ) { if let Some(editor) = item.act_as::(cx) { editor.update(cx, |editor, cx| { + let thread_store = workspace + .read(cx) + .panel::(cx) + .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade()); + editor.push_code_action_provider( Rc::new(AssistantCodeActionProvider { editor: cx.view().downgrade(), workspace: workspace.downgrade(), + thread_store, }), cx, ); @@ -212,7 +224,11 @@ impl InlineAssistant { let handle_assist = |cx: &mut ViewContext| match inline_assist_target { InlineAssistTarget::Editor(active_editor) => { InlineAssistant::update_global(cx, |assistant, cx| { - assistant.assist(&active_editor, cx.view().downgrade(), cx) + let thread_store = workspace + .panel::(cx) + .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade()); + + assistant.assist(&active_editor, cx.view().downgrade(), thread_store, cx) }) } InlineAssistTarget::Terminal(active_terminal) => { @@ -265,6 +281,7 @@ impl InlineAssistant { &mut self, editor: &View, workspace: WeakView, + thread_store: Option>, cx: &mut WindowContext, ) { let (snapshot, initial_selections) = editor.update(cx, |editor, cx| { @@ -343,11 +360,13 @@ impl InlineAssistant { let mut assist_to_focus = None; for range in codegen_ranges { let assist_id = self.next_assist_id.post_inc(); + let context_store = cx.new_model(|_cx| ContextStore::new()); let codegen = cx.new_model(|cx| { Codegen::new( editor.read(cx).buffer().clone(), range.clone(), None, + context_store.clone(), self.telemetry.clone(), self.prompt_builder.clone(), cx, @@ -363,6 +382,9 @@ impl InlineAssistant { prompt_buffer.clone(), codegen.clone(), self.fs.clone(), + context_store, + workspace.clone(), + thread_store.clone(), cx, ) }); @@ -430,6 +452,7 @@ impl InlineAssistant { initial_transaction_id: Option, focus: bool, workspace: WeakView, + thread_store: Option>, cx: &mut WindowContext, ) -> InlineAssistId { let assist_group_id = self.next_assist_group_id.post_inc(); @@ -445,11 +468,14 @@ impl InlineAssistant { range.end = range.end.bias_right(&snapshot); } + let context_store = cx.new_model(|_cx| ContextStore::new()); + let codegen = cx.new_model(|cx| { Codegen::new( editor.read(cx).buffer().clone(), range.clone(), initial_transaction_id, + context_store.clone(), self.telemetry.clone(), self.prompt_builder.clone(), cx, @@ -465,6 +491,9 @@ impl InlineAssistant { prompt_buffer.clone(), codegen.clone(), self.fs.clone(), + context_store, + workspace.clone(), + thread_store, cx, ) }); @@ -1456,6 +1485,7 @@ enum PromptEditorEvent { struct PromptEditor { id: InlineAssistId, editor: View, + context_strip: View, language_model_selector: View, edited_since_done: bool, gutter_dimensions: Arc>, @@ -1473,11 +1503,7 @@ impl EventEmitter for PromptEditor {} impl Render for PromptEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let gutter_dimensions = *self.gutter_dimensions.lock(); - let mut buttons = vec![Button::new("add-context", "Add Context") - .style(ButtonStyle::Filled) - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .into_any_element()]; + let mut buttons = Vec::new(); let codegen = self.codegen.read(cx); if codegen.alternative_count(cx) > 1 { buttons.push(self.render_cycle_controls(cx)); @@ -1570,91 +1596,114 @@ impl Render for PromptEditor { } }); - h_flex() - .key_context("PromptEditor") - .bg(cx.theme().colors().editor_background) - .block_mouse_down() - .cursor(CursorStyle::Arrow) + v_flex() .border_y_1() .border_color(cx.theme().status().info_border) .size_full() .py(cx.line_height() / 2.5) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::move_up)) - .on_action(cx.listener(Self::move_down)) - .capture_action(cx.listener(Self::cycle_prev)) - .capture_action(cx.listener(Self::cycle_next)) .child( h_flex() - .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) - .justify_center() - .gap_2() - .child(LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - IconButton::new("context", IconName::SettingsAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(move |cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - cx, - ) - }), - )) - .map(|el| { - let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else { - return el; - }; - - let error_message = SharedString::from(error.to_string()); - if error.error_code() == proto::ErrorCode::RateLimitExceeded - && cx.has_flag::() - { - el.child( - v_flex() - .child( - IconButton::new("rate-limit-error", IconName::XCircle) - .toggle_state(self.show_rate_limit_notice) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .on_click(cx.listener(Self::toggle_rate_limit_notice)), - ) - .children(self.show_rate_limit_notice.then(|| { - deferred( - anchored() - .position_mode(gpui::AnchoredPositionMode::Local) - .position(point(px(0.), px(24.))) - .anchor(gpui::AnchorCorner::TopLeft) - .child(self.render_rate_limit_notice(cx)), + .key_context("PromptEditor") + .bg(cx.theme().colors().editor_background) + .block_mouse_down() + .cursor(CursorStyle::Arrow) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::move_up)) + .on_action(cx.listener(Self::move_down)) + .capture_action(cx.listener(Self::cycle_prev)) + .capture_action(cx.listener(Self::cycle_next)) + .child( + h_flex() + .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) + .justify_center() + .gap_2() + .child(LanguageModelSelectorPopoverMenu::new( + self.language_model_selector.clone(), + IconButton::new("context", IconName::SettingsAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(move |cx| { + Tooltip::with_meta( + format!( + "Using {}", + LanguageModelRegistry::read_global(cx) + .active_model() + .map(|model| model.name().0) + .unwrap_or_else(|| "No model selected".into()), + ), + None, + "Change Model", + cx, ) - })), - ) - } else { - el.child( - div() - .id("error") - .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) - .child( - Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error), - ), - ) - } - }), + }), + )) + .map(|el| { + let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) + else { + return el; + }; + + let error_message = SharedString::from(error.to_string()); + if error.error_code() == proto::ErrorCode::RateLimitExceeded + && cx.has_flag::() + { + el.child( + v_flex() + .child( + IconButton::new( + "rate-limit-error", + IconName::XCircle, + ) + .toggle_state(self.show_rate_limit_notice) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .on_click( + cx.listener(Self::toggle_rate_limit_notice), + ), + ) + .children(self.show_rate_limit_notice.then(|| { + deferred( + anchored() + .position_mode( + gpui::AnchoredPositionMode::Local, + ) + .position(point(px(0.), px(24.))) + .anchor(gpui::AnchorCorner::TopLeft) + .child(self.render_rate_limit_notice(cx)), + ) + })), + ) + } else { + el.child( + div() + .id("error") + .tooltip(move |cx| { + Tooltip::text(error_message.clone(), cx) + }) + .child( + Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error), + ), + ) + } + }), + ) + .child(div().flex_1().child(self.render_editor(cx))) + .child(h_flex().gap_2().pr_6().children(buttons)), + ) + .child( + h_flex() + .child( + h_flex() + .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) + .justify_center() + .gap_2(), + ) + .child(self.context_strip.clone()), ) - .child(div().flex_1().child(self.render_editor(cx))) - .child(h_flex().gap_2().pr_6().children(buttons)) } } @@ -1675,6 +1724,9 @@ impl PromptEditor { prompt_buffer: Model, codegen: Model, fs: Arc, + context_store: Model, + workspace: WeakView, + thread_store: Option>, cx: &mut ViewContext, ) -> Self { let prompt_editor = cx.new_view(|cx| { @@ -1699,6 +1751,9 @@ impl PromptEditor { let mut this = Self { id, editor: prompt_editor, + context_strip: cx.new_view(|cx| { + ContextStrip::new(context_store, workspace.clone(), thread_store.clone(), cx) + }), language_model_selector: cx.new_view(|cx| { let fs = fs.clone(); LanguageModelSelector::new( @@ -2293,6 +2348,7 @@ pub struct Codegen { buffer: Model, range: Range, initial_transaction_id: Option, + context_store: Model, telemetry: Arc, builder: Arc, is_insertion: bool, @@ -2303,6 +2359,7 @@ impl Codegen { buffer: Model, range: Range, initial_transaction_id: Option, + context_store: Model, telemetry: Arc, builder: Arc, cx: &mut ModelContext, @@ -2312,6 +2369,7 @@ impl Codegen { buffer.clone(), range.clone(), false, + Some(context_store.clone()), Some(telemetry.clone()), builder.clone(), cx, @@ -2326,6 +2384,7 @@ impl Codegen { buffer, range, initial_transaction_id, + context_store, telemetry, builder, }; @@ -2398,6 +2457,7 @@ impl Codegen { self.buffer.clone(), self.range.clone(), false, + Some(self.context_store.clone()), Some(self.telemetry.clone()), self.builder.clone(), cx, @@ -2477,6 +2537,7 @@ pub struct CodegenAlternative { status: CodegenStatus, generation: Task<()>, diff: Diff, + context_store: Option>, telemetry: Option>, _subscription: gpui::Subscription, builder: Arc, @@ -2515,6 +2576,7 @@ impl CodegenAlternative { buffer: Model, range: Range, active: bool, + context_store: Option>, telemetry: Option>, builder: Arc, cx: &mut ModelContext, @@ -2552,6 +2614,7 @@ impl CodegenAlternative { status: CodegenStatus::Idle, generation: Task::ready(()), diff: Diff::default(), + context_store, telemetry, _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), builder, @@ -2637,7 +2700,11 @@ impl CodegenAlternative { Ok(()) } - fn build_request(&self, user_prompt: String, cx: &AppContext) -> Result { + fn build_request( + &self, + user_prompt: String, + cx: &mut AppContext, + ) -> Result { let buffer = self.buffer.read(cx).snapshot(cx); let language = buffer.language_at(self.range.start); let language_name = if let Some(language) = language.as_ref() { @@ -2670,15 +2737,24 @@ impl CodegenAlternative { .generate_inline_transformation_prompt(user_prompt, language_name, buffer, range) .map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?; + let mut request_message = LanguageModelRequestMessage { + role: Role::User, + content: Vec::new(), + cache: false, + }; + + if let Some(context_store) = &self.context_store { + let context = context_store.update(cx, |this, _cx| this.context().clone()); + attach_context_to_message(&mut request_message, context); + } + + request_message.content.push(prompt.into()); + Ok(LanguageModelRequest { tools: Vec::new(), stop: Vec::new(), temperature: None, - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![prompt.into()], - cache: false, - }], + messages: vec![request_message], }) } @@ -3273,6 +3349,7 @@ where struct AssistantCodeActionProvider { editor: WeakView, workspace: WeakView, + thread_store: Option>, } impl CodeActionProvider for AssistantCodeActionProvider { @@ -3337,6 +3414,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { ) -> Task> { let editor = self.editor.clone(); let workspace = self.workspace.clone(); + let thread_store = self.thread_store.clone(); cx.spawn(|mut cx| async move { let editor = editor.upgrade().context("editor was released")?; let range = editor @@ -3384,6 +3462,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { None, true, workspace, + thread_store, cx, ); assistant.start_assist(assist_id, cx); @@ -3469,6 +3548,7 @@ mod tests { range.clone(), true, None, + None, prompt_builder, cx, ) @@ -3533,6 +3613,7 @@ mod tests { range.clone(), true, None, + None, prompt_builder, cx, ) @@ -3600,6 +3681,7 @@ mod tests { range.clone(), true, None, + None, prompt_builder, cx, ) @@ -3666,6 +3748,7 @@ mod tests { range.clone(), true, None, + None, prompt_builder, cx, ) @@ -3721,6 +3804,7 @@ mod tests { range.clone(), false, None, + None, prompt_builder, cx, ) diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index f27d0789bb77b1..b8d7b4d1be486c 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -7,6 +7,7 @@ use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, Tooltip}; use workspace::Workspace; +use crate::context_store::ContextStore; use crate::context_strip::ContextStrip; use crate::thread::{RequestKind, Thread}; use crate::thread_store::ThreadStore; @@ -15,6 +16,7 @@ use crate::{Chat, ToggleModelSelector}; pub struct MessageEditor { thread: Model, editor: View, + context_store: Model, context_strip: View, language_model_selector: View, use_tools: bool, @@ -27,6 +29,8 @@ impl MessageEditor { thread: Model, cx: &mut ViewContext, ) -> Self { + let context_store = cx.new_model(|_cx| ContextStore::new()); + Self { thread, editor: cx.new_view(|cx| { @@ -35,8 +39,15 @@ impl MessageEditor { editor }), - context_strip: cx - .new_view(|cx| ContextStrip::new(workspace.clone(), thread_store.clone(), cx)), + context_store: context_store.clone(), + context_strip: cx.new_view(|cx| { + ContextStrip::new( + context_store, + workspace.clone(), + Some(thread_store.clone()), + cx, + ) + }), language_model_selector: cx.new_view(|cx| { LanguageModelSelector::new( |model, _cx| { @@ -75,7 +86,7 @@ impl MessageEditor { editor.clear(cx); text }); - let context = self.context_strip.update(cx, |this, _cx| this.drain()); + let context = self.context_store.update(cx, |this, _cx| this.drain()); self.thread.update(cx, |thread, cx| { thread.insert_user_message(user_message, context, cx); diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 73d022c664d7e6..ae5a564c1ad1dc 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; use util::{post_inc, TryFutureExt as _}; use uuid::Uuid; -use crate::context::{Context, ContextKind}; +use crate::context::{attach_context_to_message, Context}; #[derive(Debug, Clone, Copy)] pub enum RequestKind { @@ -192,51 +192,7 @@ impl Thread { } if let Some(context) = self.context_for_message(message.id) { - let mut file_context = String::new(); - let mut fetch_context = String::new(); - let mut thread_context = String::new(); - - for context in context.iter() { - match context.kind { - ContextKind::File => { - file_context.push_str(&context.text); - file_context.push('\n'); - } - ContextKind::FetchedUrl => { - fetch_context.push_str(&context.name); - fetch_context.push('\n'); - fetch_context.push_str(&context.text); - fetch_context.push('\n'); - } - ContextKind::Thread => { - thread_context.push_str(&context.name); - thread_context.push('\n'); - thread_context.push_str(&context.text); - thread_context.push('\n'); - } - } - } - - let mut context_text = String::new(); - if !file_context.is_empty() { - context_text.push_str("The following files are available:\n"); - context_text.push_str(&file_context); - } - - if !fetch_context.is_empty() { - context_text.push_str("The following fetched results are available\n"); - context_text.push_str(&fetch_context); - } - - if !thread_context.is_empty() { - context_text - .push_str("The following previous conversation threads are available\n"); - context_text.push_str(&thread_context); - } - - request_message - .content - .push(MessageContent::Text(context_text)) + attach_context_to_message(&mut request_message, context.clone()); } if !message.text.is_empty() { From 8127decd2dfb61f558a0f26ad31f9a572c983dbc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 16 Dec 2024 12:51:17 -0800 Subject: [PATCH 18/32] Extract logic around custom text highlights out of InlayChunks iterator (#22104) This is a pure refactor, extracted from https://github.com/zed-industries/zed/tree/new-diff-map Release Notes: - N/A Co-authored-by: Conrad Co-authored-by: Agus --- crates/editor/src/display_map.rs | 1 + .../src/display_map/custom_highlights.rs | 174 +++++++++++++++++ crates/editor/src/display_map/inlay_map.rs | 177 ++---------------- 3 files changed, 191 insertions(+), 161 deletions(-) create mode 100644 crates/editor/src/display_map/custom_highlights.rs diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 76b508079d1980..7ec189395673dd 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -19,6 +19,7 @@ mod block_map; mod crease_map; +mod custom_highlights; mod fold_map; mod inlay_map; pub(crate) mod invisibles; diff --git a/crates/editor/src/display_map/custom_highlights.rs b/crates/editor/src/display_map/custom_highlights.rs new file mode 100644 index 00000000000000..11356586ebf3c5 --- /dev/null +++ b/crates/editor/src/display_map/custom_highlights.rs @@ -0,0 +1,174 @@ +use collections::BTreeMap; +use gpui::HighlightStyle; +use language::Chunk; +use multi_buffer::{Anchor, MultiBufferChunks, MultiBufferSnapshot, ToOffset as _}; +use std::{ + any::TypeId, + cmp, + iter::{self, Peekable}, + ops::Range, + sync::Arc, + vec, +}; +use sum_tree::TreeMap; + +pub struct CustomHighlightsChunks<'a> { + buffer_chunks: MultiBufferChunks<'a>, + buffer_chunk: Option>, + offset: usize, + multibuffer_snapshot: &'a MultiBufferSnapshot, + + highlight_endpoints: Peekable>, + active_highlights: BTreeMap, + text_highlights: Option<&'a TreeMap>)>>>, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct HighlightEndpoint { + offset: usize, + is_start: bool, + tag: TypeId, + style: HighlightStyle, +} + +impl<'a> CustomHighlightsChunks<'a> { + pub fn new( + range: Range, + language_aware: bool, + text_highlights: Option<&'a TreeMap>)>>>, + multibuffer_snapshot: &'a MultiBufferSnapshot, + ) -> Self { + Self { + buffer_chunks: multibuffer_snapshot.chunks(range.clone(), language_aware), + buffer_chunk: None, + offset: range.start, + + text_highlights, + highlight_endpoints: create_highlight_endpoints( + &range, + text_highlights, + multibuffer_snapshot, + ), + active_highlights: Default::default(), + multibuffer_snapshot, + } + } + + pub fn seek(&mut self, new_range: Range) { + self.highlight_endpoints = + create_highlight_endpoints(&new_range, self.text_highlights, self.multibuffer_snapshot); + self.offset = new_range.start; + self.buffer_chunks.seek(new_range); + self.buffer_chunk.take(); + self.active_highlights.clear() + } +} + +fn create_highlight_endpoints( + range: &Range, + text_highlights: Option<&TreeMap>)>>>, + buffer: &MultiBufferSnapshot, +) -> iter::Peekable> { + let mut highlight_endpoints = Vec::new(); + if let Some(text_highlights) = text_highlights { + let start = buffer.anchor_after(range.start); + let end = buffer.anchor_after(range.end); + for (&tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&start, &buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + + for range in &ranges[start_ix..] { + if range.start.cmp(&end, &buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.start.to_offset(&buffer), + is_start: true, + tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.end.to_offset(&buffer), + is_start: false, + tag, + style, + }); + } + } + highlight_endpoints.sort(); + } + highlight_endpoints.into_iter().peekable() +} + +impl<'a> Iterator for CustomHighlightsChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + let mut next_highlight_endpoint = usize::MAX; + while let Some(endpoint) = self.highlight_endpoints.peek().copied() { + if endpoint.offset <= self.offset { + if endpoint.is_start { + self.active_highlights.insert(endpoint.tag, endpoint.style); + } else { + self.active_highlights.remove(&endpoint.tag); + } + self.highlight_endpoints.next(); + } else { + next_highlight_endpoint = endpoint.offset; + break; + } + } + + let chunk = self + .buffer_chunk + .get_or_insert_with(|| self.buffer_chunks.next().unwrap()); + if chunk.text.is_empty() { + *chunk = self.buffer_chunks.next().unwrap(); + } + + let (prefix, suffix) = chunk + .text + .split_at(chunk.text.len().min(next_highlight_endpoint - self.offset)); + + chunk.text = suffix; + self.offset += prefix.len(); + let mut prefix = Chunk { + text: prefix, + ..chunk.clone() + }; + if !self.active_highlights.is_empty() { + let mut highlight_style = HighlightStyle::default(); + for active_highlight in self.active_highlights.values() { + highlight_style.highlight(*active_highlight); + } + prefix.highlight_style = Some(highlight_style); + } + Some(prefix) + } +} + +impl PartialOrd for HighlightEndpoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for HighlightEndpoint { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.offset + .cmp(&other.offset) + .then_with(|| other.is_start.cmp(&self.is_start)) + } +} diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index c03fa37df06587..e288f5a76c90de 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,22 +1,15 @@ use crate::{HighlightStyles, InlayId}; -use collections::{BTreeMap, BTreeSet}; -use gpui::HighlightStyle; +use collections::BTreeSet; use language::{Chunk, Edit, Point, TextSummary}; -use multi_buffer::{ - Anchor, MultiBufferChunks, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset, -}; +use multi_buffer::{Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset}; use std::{ - any::TypeId, cmp, - iter::Peekable, ops::{Add, AddAssign, Range, Sub, SubAssign}, - sync::Arc, - vec, }; -use sum_tree::{Bias, Cursor, SumTree, TreeMap}; +use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::Highlights; +use super::{custom_highlights::CustomHighlightsChunks, Highlights}; /// Decides where the [`Inlay`]s should be displayed. /// @@ -207,39 +200,15 @@ pub struct InlayBufferRows<'a> { max_buffer_row: MultiBufferRow, } -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -struct HighlightEndpoint { - offset: InlayOffset, - is_start: bool, - tag: TypeId, - style: HighlightStyle, -} - -impl PartialOrd for HighlightEndpoint { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for HighlightEndpoint { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.offset - .cmp(&other.offset) - .then_with(|| other.is_start.cmp(&self.is_start)) - } -} - pub struct InlayChunks<'a> { transforms: Cursor<'a, Transform, (InlayOffset, usize)>, - buffer_chunks: MultiBufferChunks<'a>, + buffer_chunks: CustomHighlightsChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, inlay_chunk: Option<&'a str>, output_offset: InlayOffset, max_output_offset: InlayOffset, highlight_styles: HighlightStyles, - highlight_endpoints: Peekable>, - active_highlights: BTreeMap, highlights: Highlights<'a>, snapshot: &'a InlaySnapshot, } @@ -255,22 +224,6 @@ impl<'a> InlayChunks<'a> { self.buffer_chunk = None; self.output_offset = new_range.start; self.max_output_offset = new_range.end; - - let mut highlight_endpoints = Vec::new(); - if let Some(text_highlights) = self.highlights.text_highlights { - if !text_highlights.is_empty() { - self.snapshot.apply_text_highlights( - &mut self.transforms, - &new_range, - text_highlights, - &mut highlight_endpoints, - ); - self.transforms.seek(&new_range.start, Bias::Right, &()); - highlight_endpoints.sort(); - } - } - self.highlight_endpoints = highlight_endpoints.into_iter().peekable(); - self.active_highlights.clear(); } pub fn offset(&self) -> InlayOffset { @@ -286,21 +239,6 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } - let mut next_highlight_endpoint = InlayOffset(usize::MAX); - while let Some(endpoint) = self.highlight_endpoints.peek().copied() { - if endpoint.offset <= self.output_offset { - if endpoint.is_start { - self.active_highlights.insert(endpoint.tag, endpoint.style); - } else { - self.active_highlights.remove(&endpoint.tag); - } - self.highlight_endpoints.next(); - } else { - next_highlight_endpoint = endpoint.offset; - break; - } - } - let chunk = match self.transforms.item()? { Transform::Isomorphic(_) => { let chunk = self @@ -314,24 +252,15 @@ impl<'a> Iterator for InlayChunks<'a> { chunk .text .len() - .min(self.transforms.end(&()).0 .0 - self.output_offset.0) - .min(next_highlight_endpoint.0 - self.output_offset.0), + .min(self.transforms.end(&()).0 .0 - self.output_offset.0), ); chunk.text = suffix; self.output_offset.0 += prefix.len(); - let mut prefix = Chunk { + Chunk { text: prefix, ..chunk.clone() - }; - if !self.active_highlights.is_empty() { - let mut highlight_style = HighlightStyle::default(); - for active_highlight in self.active_highlights.values() { - highlight_style.highlight(*active_highlight); - } - prefix.highlight_style = Some(highlight_style); } - prefix } Transform::Inlay(inlay) => { let mut inlay_style_and_highlight = None; @@ -393,13 +322,6 @@ impl<'a> Iterator for InlayChunks<'a> { self.output_offset.0 += chunk.len(); - if !self.active_highlights.is_empty() { - for active_highlight in self.active_highlights.values() { - highlight_style - .get_or_insert(Default::default()) - .highlight(*active_highlight); - } - } Chunk { text: chunk, highlight_style, @@ -1068,21 +990,13 @@ impl InlaySnapshot { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); cursor.seek(&range.start, Bias::Right, &()); - let mut highlight_endpoints = Vec::new(); - if let Some(text_highlights) = highlights.text_highlights { - if !text_highlights.is_empty() { - self.apply_text_highlights( - &mut cursor, - &range, - text_highlights, - &mut highlight_endpoints, - ); - cursor.seek(&range.start, Bias::Right, &()); - } - } - highlight_endpoints.sort(); let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); - let buffer_chunks = self.buffer.chunks(buffer_range, language_aware); + let buffer_chunks = CustomHighlightsChunks::new( + buffer_range, + language_aware, + highlights.text_highlights, + &self.buffer, + ); InlayChunks { transforms: cursor, @@ -1093,71 +1007,11 @@ impl InlaySnapshot { output_offset: range.start, max_output_offset: range.end, highlight_styles: highlights.styles, - highlight_endpoints: highlight_endpoints.into_iter().peekable(), - active_highlights: Default::default(), highlights, snapshot: self, } } - fn apply_text_highlights( - &self, - cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>, - range: &Range, - text_highlights: &TreeMap>)>>, - highlight_endpoints: &mut Vec, - ) { - while cursor.start().0 < range.end { - let transform_start = self - .buffer - .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); - let transform_end = - { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - - for (&tag, text_highlights) in text_highlights.iter() { - let style = text_highlights.0; - let ranges = &text_highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag, - style, - }); - } - } - - cursor.next(&()); - } - } - #[cfg(test)] pub fn text(&self) -> String { self.chunks(Default::default()..self.len(), false, Highlights::default()) @@ -1213,11 +1067,12 @@ mod tests { hover_links::InlayHighlight, InlayId, MultiBuffer, }; - use gpui::AppContext; + use gpui::{AppContext, HighlightStyle}; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; - use std::{cmp::Reverse, env, sync::Arc}; + use std::{any::TypeId, cmp::Reverse, env, sync::Arc}; + use sum_tree::TreeMap; use text::Patch; use util::post_inc; From 91fdb5d2a9b72abbff30eb01ab2b5d556585d287 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 16 Dec 2024 22:53:14 +0200 Subject: [PATCH 19/32] Return back the logic for indent guides check (#22095) Follow-up of https://github.com/zed-industries/zed/pull/22046 Release Notes: - N/A Co-authored-by: Bennet --- crates/editor/src/editor_tests.rs | 1 + crates/editor/src/indent_guides.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 8ff7c1cacd33aa..84f811645ef87c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -13078,6 +13078,7 @@ fn assert_indent_guides( let indent_guides = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx).display_snapshot; let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range( + editor, MultiBufferRow(range.start)..MultiBufferRow(range.end), true, &snapshot, diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index 1e04fe4b73d439..1284ba81560d88 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -56,6 +56,7 @@ impl Editor { } Some(indent_guides_in_range( + self, visible_buffer_range, self.should_show_indent_guides() == Some(true), snapshot, @@ -152,6 +153,7 @@ impl Editor { } pub fn indent_guides_in_range( + editor: &Editor, visible_buffer_range: Range, ignore_disabled_for_language: bool, snapshot: &DisplaySnapshot, @@ -169,10 +171,20 @@ pub fn indent_guides_in_range( .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx) .into_iter() .filter(|indent_guide| { + if editor.buffer_folded(indent_guide.buffer_id, cx) { + return false; + } + let start = MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1)); // Filter out indent guides that are inside a fold - !snapshot.is_line_folded(start) + // All indent guides that are starting "offscreen" have a start value of the first visible row minus one + // Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well + let is_folded = snapshot.is_line_folded(start); + let line_indent = snapshot.line_indent_for_buffer_row(start); + let contained_in_fold = + line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level(); + !(is_folded && contained_in_fold) }) .collect() } From 84392fbc2f70d4ab648887b4f713e59b2e5aaffe Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:11:32 -0500 Subject: [PATCH 20/32] docs: Use preferred `languages` instead of deprecated `language` field (#22107) Ref https://github.com/zed-industries/zed/issues/21994#issuecomment-2545988779, `language` is already deprecated but was still suggested in the docs here. Release Notes: - N/A --- docs/src/extensions/languages.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index 061496caf75812..9b414ab4fe8589 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -363,12 +363,12 @@ TBD: `#set! tag` Zed uses the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) to provide advanced language support. -An extension may provide any number of language servers. To provide a language server from your extension, add an entry to your `extension.toml` with the name of your language server and the language it applies to: +An extension may provide any number of language servers. To provide a language server from your extension, add an entry to your `extension.toml` with the name of your language server and the language(s) it applies to: ```toml [language_servers.my-language] name = "My Language LSP" -language = "My Language" +languages = ["My Language"] ``` Then, in the Rust code for your extension, implement the `language_server_command` method on your extension: From 92fb38acb6d5b03f1eb2386b753c5119a2f4bf6a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 16:22:16 -0500 Subject: [PATCH 21/32] assistant2: Wire up context for terminal inline assist (#22108) This PR updates up the context picker for the terminal's inline assist. Release Notes: - N/A Co-authored-by: Richard Co-authored-by: Agus --- crates/assistant2/src/inline_assistant.rs | 10 +- .../src/terminal_inline_assistant.rs | 156 +++++++++++------- 2 files changed, 98 insertions(+), 68 deletions(-) diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index 61d415a2a82dfa..4f260518ae44e6 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -221,19 +221,19 @@ impl InlineAssistant { .map_or(false, |provider| provider.is_authenticated(cx)) }; + let thread_store = workspace + .panel::(cx) + .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade()); + let handle_assist = |cx: &mut ViewContext| match inline_assist_target { InlineAssistTarget::Editor(active_editor) => { InlineAssistant::update_global(cx, |assistant, cx| { - let thread_store = workspace - .panel::(cx) - .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade()); - assistant.assist(&active_editor, cx.view().downgrade(), thread_store, cx) }) } InlineAssistTarget::Terminal(active_terminal) => { TerminalInlineAssistant::update_global(cx, |assistant, cx| { - assistant.assist(&active_terminal, cx.view().downgrade(), cx) + assistant.assist(&active_terminal, cx.view().downgrade(), thread_store, cx) }) } }; diff --git a/crates/assistant2/src/terminal_inline_assistant.rs b/crates/assistant2/src/terminal_inline_assistant.rs index e3b81979a86ebf..48851bf82c6119 100644 --- a/crates/assistant2/src/terminal_inline_assistant.rs +++ b/crates/assistant2/src/terminal_inline_assistant.rs @@ -1,5 +1,9 @@ use crate::assistant_settings::AssistantSettings; +use crate::context::attach_context_to_message; +use crate::context_store::ContextStore; +use crate::context_strip::ContextStrip; use crate::prompts::PromptBuilder; +use crate::thread_store::ThreadStore; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; use collections::{HashMap, VecDeque}; @@ -11,7 +15,7 @@ use fs::Fs; use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext, - Subscription, Task, TextStyle, UpdateGlobal, View, WeakView, + Subscription, Task, TextStyle, UpdateGlobal, View, WeakModel, WeakView, }; use language::Buffer; use language_model::{ @@ -83,6 +87,7 @@ impl TerminalInlineAssistant { &mut self, terminal_view: &View, workspace: WeakView, + thread_store: Option>, cx: &mut WindowContext, ) { let terminal = terminal_view.read(cx).terminal().clone(); @@ -90,6 +95,7 @@ impl TerminalInlineAssistant { let prompt_buffer = cx.new_model(|cx| { MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx) }); + let context_store = cx.new_model(|_cx| ContextStore::new()); let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone())); let prompt_editor = cx.new_view(|cx| { @@ -99,6 +105,9 @@ impl TerminalInlineAssistant { prompt_buffer.clone(), codegen, self.fs.clone(), + context_store.clone(), + workspace.clone(), + thread_store.clone(), cx, ) }); @@ -116,6 +125,7 @@ impl TerminalInlineAssistant { terminal_view, prompt_editor, workspace.clone(), + context_store, cx, ); @@ -246,12 +256,21 @@ impl TerminalInlineAssistant { &latest_output, )?; + let mut request_message = LanguageModelRequestMessage { + role: Role::User, + content: vec![], + cache: false, + }; + + let context = assist + .context_store + .update(cx, |this, _cx| this.context().clone()); + attach_context_to_message(&mut request_message, context); + + request_message.content.push(prompt.into()); + Ok(LanguageModelRequest { - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![prompt.into()], - cache: false, - }], + messages: vec![request_message], tools: Vec::new(), stop: Vec::new(), temperature: None, @@ -362,6 +381,7 @@ struct TerminalInlineAssist { prompt_editor: Option>, codegen: Model, workspace: WeakView, + context_store: Model, _subscriptions: Vec, } @@ -371,6 +391,7 @@ impl TerminalInlineAssist { terminal: &View, prompt_editor: View, workspace: WeakView, + context_store: Model, cx: &mut WindowContext, ) -> Self { let codegen = prompt_editor.read(cx).codegen.clone(); @@ -379,6 +400,7 @@ impl TerminalInlineAssist { prompt_editor: Some(prompt_editor.clone()), codegen: codegen.clone(), workspace: workspace.clone(), + context_store, _subscriptions: vec![ cx.subscribe(&prompt_editor, |prompt_editor, event, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| { @@ -437,6 +459,7 @@ struct PromptEditor { id: TerminalInlineAssistId, height_in_lines: u8, editor: View, + context_strip: View, language_model_selector: View, edited_since_done: bool, prompt_history: VecDeque, @@ -452,11 +475,7 @@ impl EventEmitter for PromptEditor {} impl Render for PromptEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let status = &self.codegen.read(cx).status; - let mut buttons = vec![Button::new("add-context", "Add Context") - .style(ButtonStyle::Filled) - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .into_any_element()]; + let mut buttons = Vec::new(); buttons.extend(match status { CodegenStatus::Idle => vec![ @@ -554,64 +573,69 @@ impl Render for PromptEditor { } }); - h_flex() - .bg(cx.theme().colors().editor_background) + v_flex() .border_y_1() .border_color(cx.theme().status().info_border) .py_2() - .h_full() - .w_full() - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::secondary_confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::move_up)) - .on_action(cx.listener(Self::move_down)) + .size_full() .child( h_flex() - .w_12() - .justify_center() - .gap_2() - .child(LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - IconButton::new("context", IconName::SettingsAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(move |cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - cx, - ) - }), - )) - .children( - if let CodegenStatus::Error(error) = &self.codegen.read(cx).status { - let error_message = SharedString::from(error.to_string()); - Some( - div() - .id("error") - .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) - .child( - Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error), - ), - ) - } else { - None - }, - ), + .bg(cx.theme().colors().editor_background) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::secondary_confirm)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::move_up)) + .on_action(cx.listener(Self::move_down)) + .child( + h_flex() + .w_12() + .justify_center() + .gap_2() + .child(LanguageModelSelectorPopoverMenu::new( + self.language_model_selector.clone(), + IconButton::new("context", IconName::SettingsAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(move |cx| { + Tooltip::with_meta( + format!( + "Using {}", + LanguageModelRegistry::read_global(cx) + .active_model() + .map(|model| model.name().0) + .unwrap_or_else(|| "No model selected".into()), + ), + None, + "Change Model", + cx, + ) + }), + )) + .children( + if let CodegenStatus::Error(error) = &self.codegen.read(cx).status { + let error_message = SharedString::from(error.to_string()); + Some( + div() + .id("error") + .tooltip(move |cx| { + Tooltip::text(error_message.clone(), cx) + }) + .child( + Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error), + ), + ) + } else { + None + }, + ), + ) + .child(div().flex_1().child(self.render_prompt_editor(cx))) + .child(h_flex().gap_1().pr_4().children(buttons)), ) - .child(div().flex_1().child(self.render_prompt_editor(cx))) - .child(h_flex().gap_1().pr_4().children(buttons)) + .child(h_flex().child(self.context_strip.clone())) } } @@ -631,6 +655,9 @@ impl PromptEditor { prompt_buffer: Model, codegen: Model, fs: Arc, + context_store: Model, + workspace: WeakView, + thread_store: Option>, cx: &mut ViewContext, ) -> Self { let prompt_editor = cx.new_view(|cx| { @@ -652,6 +679,9 @@ impl PromptEditor { id, height_in_lines: 1, editor: prompt_editor, + context_strip: cx.new_view(|cx| { + ContextStrip::new(context_store, workspace.clone(), thread_store.clone(), cx) + }), language_model_selector: cx.new_view(|cx| { let fs = fs.clone(); LanguageModelSelector::new( From 53c8b48647bcd3685e6b4bc338aedc3c909587f4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:23:42 -0300 Subject: [PATCH 22/32] assistant2: Add stray visual adjustments (#22111) Mostly minor tweaks to make it closer to the prototype. More to come. | With message | Empty state | |--------|--------| | Screenshot 2024-12-16 at 18 59 40 | Screenshot 2024-12-16 at 18 59 33 | Release Notes: - N/A --- assets/icons/message_circle.svg | 1 + assets/icons/person.svg | 5 ++- crates/assistant2/src/active_thread.rs | 23 +++++++---- crates/assistant2/src/assistant_panel.rs | 21 +++++----- crates/assistant2/src/context_picker.rs | 52 ++++++------------------ crates/assistant2/src/context_strip.rs | 9 ++-- crates/assistant2/src/message_editor.rs | 38 ++++++++--------- crates/assistant2/src/thread_history.rs | 6 ++- crates/assistant2/src/ui/context_pill.rs | 7 +++- crates/ui/src/components/icon.rs | 1 + 10 files changed, 74 insertions(+), 89 deletions(-) create mode 100644 assets/icons/message_circle.svg diff --git a/assets/icons/message_circle.svg b/assets/icons/message_circle.svg new file mode 100644 index 00000000000000..e44c8607ea4fcc --- /dev/null +++ b/assets/icons/message_circle.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/person.svg b/assets/icons/person.svg index f6133478d1e092..93bee97a5fc345 100644 --- a/assets/icons/person.svg +++ b/assets/icons/person.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index bf0086ee117d2d..7f491cce3334d7 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -213,26 +213,33 @@ impl ActiveThread { div() .id(("message-container", ix)) - .p_2() + .py_1() + .px_2() .child( v_flex() .border_1() - .border_color(cx.theme().colors().border_variant) + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) .rounded_md() .child( h_flex() .justify_between() - .p_1p5() + .py_1() + .px_2() .border_b_1() .border_color(cx.theme().colors().border_variant) .child( h_flex() - .gap_2() - .child(Icon::new(role_icon).size(IconSize::Small)) - .child(Label::new(role_name).size(LabelSize::Small)), + .gap_1p5() + .child( + Icon::new(role_icon) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child(Label::new(role_name).size(LabelSize::XSmall)), ), ) - .child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())) + .child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone())) .when_some(context, |parent, context| { parent.child( h_flex().flex_wrap().gap_2().p_1p5().children( @@ -249,6 +256,6 @@ impl ActiveThread { impl Render for ActiveThread { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - list(self.list_state.clone()).flex_1() + list(self.list_state.clone()).flex_1().py_1() } } diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 1d21413583e7e4..8d6cc50d81026b 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -10,7 +10,7 @@ use gpui::{ }; use language::LanguageRegistry; use time::UtcOffset; -use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip}; +use ui::{prelude::*, KeyBinding, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; @@ -202,7 +202,7 @@ impl Panel for AssistantPanel { fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {} fn size(&self, _cx: &WindowContext) -> Pixels { - px(640.) + px(550.) } fn set_size(&mut self, _size: Option, _cx: &mut ViewContext) {} @@ -238,15 +238,17 @@ impl AssistantPanel { .px(DynamicSpacing::Base08.rems(cx)) .bg(cx.theme().colors().tab_bar_background) .border_b_1() - .border_color(cx.theme().colors().border_variant) + .border_color(cx.theme().colors().border) .child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new))) .child( h_flex() - .gap(DynamicSpacing::Base08.rems(cx)) - .child(Divider::vertical()) + .h_full() + .pl_1() + .border_l_1() + .border_color(cx.theme().colors().border) + .gap(DynamicSpacing::Base02.rems(cx)) .child( IconButton::new("new-thread", IconName::Plus) - .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip({ @@ -266,7 +268,6 @@ impl AssistantPanel { ) .child( IconButton::new("open-history", IconName::HistoryRerun) - .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip({ @@ -286,7 +287,6 @@ impl AssistantPanel { ) .child( IconButton::new("configure-assistant", IconName::Settings) - .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Configure Assistant", cx)) @@ -312,7 +312,6 @@ impl AssistantPanel { v_flex() .gap_2() - .mx_auto() .child( v_flex().w_full().child( svg() @@ -334,7 +333,7 @@ impl AssistantPanel { ), ) .child( - v_flex().gap_2().children( + v_flex().mx_auto().w_4_5().gap_2().children( recent_threads .into_iter() .map(|thread| PastThread::new(thread, cx.view().downgrade())), @@ -541,7 +540,7 @@ impl Render for AssistantPanel { .child( h_flex() .border_t_1() - .border_color(cx.theme().colors().border_variant) + .border_color(cx.theme().colors().border) .child(self.message_editor.clone()), ) .children(self.render_last_error(cx)), diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs index 9e6086f86adf66..48bb30a5f5272a 100644 --- a/crates/assistant2/src/context_picker.rs +++ b/crates/assistant2/src/context_picker.rs @@ -9,7 +9,7 @@ use gpui::{ WeakModel, WeakView, }; use picker::{Picker, PickerDelegate}; -use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip}; +use ui::{prelude::*, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::Workspace; @@ -41,27 +41,23 @@ impl ContextPicker { ) -> Self { let mut entries = vec![ ContextPickerEntry { - name: "directory".into(), - description: "Insert any directory".into(), + name: "Directory".into(), icon: IconName::Folder, }, ContextPickerEntry { - name: "file".into(), - description: "Insert any file".into(), + name: "File".into(), icon: IconName::File, }, ContextPickerEntry { - name: "fetch".into(), - description: "Fetch content from URL".into(), + name: "Fetch".into(), icon: IconName::Globe, }, ]; if thread_store.is_some() { entries.push(ContextPickerEntry { - name: "thread".into(), - description: "Insert any thread".into(), - icon: IconName::MessageBubbles, + name: "Thread".into(), + icon: IconName::MessageCircle, }); } @@ -119,7 +115,6 @@ impl Render for ContextPicker { #[derive(Clone)] struct ContextPickerEntry { name: SharedString, - description: SharedString, icon: IconName, } @@ -161,7 +156,7 @@ impl PickerDelegate for ContextPickerDelegate { self.context_picker .update(cx, |this, cx| { match entry.name.to_string().as_str() { - "file" => { + "File" => { this.mode = ContextPickerMode::File(cx.new_view(|cx| { FileContextPicker::new( self.context_picker.clone(), @@ -171,7 +166,7 @@ impl PickerDelegate for ContextPickerDelegate { ) })); } - "fetch" => { + "Fetch" => { this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| { FetchContextPicker::new( self.context_picker.clone(), @@ -181,7 +176,7 @@ impl PickerDelegate for ContextPickerDelegate { ) })); } - "thread" => { + "Thread" => { if let Some(thread_store) = self.thread_store.as_ref() { this.mode = ContextPickerMode::Thread(cx.new_view(|cx| { ThreadContextPicker::new( @@ -226,34 +221,13 @@ impl PickerDelegate for ContextPickerDelegate { .inset(true) .spacing(ListItemSpacing::Dense) .toggle_state(selected) - .tooltip({ - let description = entry.description.clone(); - move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into() - }) .child( - v_flex() - .group(format!("context-entry-label-{ix}")) - .w_full() - .py_0p5() + h_flex() .min_w(px(250.)) .max_w(px(400.)) - .child( - h_flex() - .gap_1p5() - .child(Icon::new(entry.icon).size(IconSize::XSmall)) - .child( - Label::new(entry.name.clone()) - .single_line() - .size(LabelSize::Small), - ), - ) - .child( - div().overflow_hidden().text_ellipsis().child( - Label::new(entry.description.clone()) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ), + .gap_2() + .child(Icon::new(entry.icon).size(IconSize::Small)) + .child(Label::new(entry.name.clone()).single_line()), ), ) } diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs index c5b6164b4a6dd3..fa0f7cab8a1871 100644 --- a/crates/assistant2/src/context_strip.rs +++ b/crates/assistant2/src/context_strip.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use gpui::{Model, View, WeakModel, WeakView}; -use ui::{prelude::*, IconButtonShape, PopoverMenu, PopoverMenuHandle, Tooltip}; +use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip}; use workspace::Workspace; use crate::context_picker::ContextPicker; @@ -44,14 +44,14 @@ impl Render for ContextStrip { h_flex() .flex_wrap() - .gap_2() + .gap_1() .child( PopoverMenu::new("context-picker") .menu(move |_cx| Some(context_picker.clone())) .trigger( IconButton::new("add-context", IconName::Plus) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small), + .icon_size(IconSize::Small) + .style(ui::ButtonStyle::Filled), ) .attach(gpui::AnchorCorner::TopLeft) .anchor(gpui::AnchorCorner::BottomLeft) @@ -76,7 +76,6 @@ impl Render for ContextStrip { .when(!context.is_empty(), |parent| { parent.child( IconButton::new("remove-all-context", IconName::Eraser) - .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .tooltip(move |cx| Tooltip::text("Remove All Context", cx)) .on_click({ diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index b8d7b4d1be486c..179909d91928f8 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -35,7 +35,8 @@ impl MessageEditor { thread, editor: cx.new_view(|cx| { let mut editor = Editor::auto_height(80, cx); - editor.set_placeholder_text("Ask anything or type @ to add context", cx); + editor.set_placeholder_text("Ask anything, @ to add context", cx); + editor.set_show_indent_guides(false, cx); editor }), @@ -112,8 +113,8 @@ impl MessageEditor { } fn render_language_model_selector(&self, cx: &mut ViewContext) -> impl IntoElement { - let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); let active_model = LanguageModelRegistry::read_global(cx).active_model(); + let focus_handle = self.language_model_selector.focus_handle(cx).clone(); LanguageModelSelectorPopoverMenu::new( self.language_model_selector.clone(), @@ -128,16 +129,8 @@ impl MessageEditor { .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(match active_model { + Some(model) => h_flex() .child( Label::new(model.name().0) .size(LabelSize::Small) @@ -156,7 +149,9 @@ impl MessageEditor { .size(IconSize::XSmall), ), ) - .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)), + .tooltip(move |cx| { + Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) + }), ) } } @@ -170,8 +165,9 @@ impl FocusableView for MessageEditor { impl Render for MessageEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let font_size = TextSize::Default.rems(cx); - let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; + let line_height = font_size.to_pixels(cx.rem_size()) * 1.5; let focus_handle = self.editor.focus_handle(cx); + let bg_color = cx.theme().colors().editor_background; v_flex() .key_context("MessageEditor") @@ -179,9 +175,9 @@ impl Render for MessageEditor { .size_full() .gap_2() .p_2() - .bg(cx.theme().colors().editor_background) + .bg(bg_color) .child(self.context_strip.clone()) - .child({ + .child(div().id("thread_editor").overflow_y_scroll().h_12().child({ let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: cx.theme().colors().editor_foreground, @@ -196,17 +192,17 @@ impl Render for MessageEditor { EditorElement::new( &self.editor, EditorStyle { - background: cx.theme().colors().editor_background, + background: bg_color, local_player: cx.theme().players().local(), text: text_style, ..Default::default() }, ) - }) + })) .child( h_flex() .justify_between() - .child(h_flex().gap_2().child(CheckboxWithLabel::new( + .child(CheckboxWithLabel::new( "use-tools", Label::new("Tools"), self.use_tools.into(), @@ -216,10 +212,10 @@ impl Render for MessageEditor { ToggleState::Unselected | ToggleState::Indeterminate => false, }; }), - ))) + )) .child( h_flex() - .gap_2() + .gap_1() .child(self.render_language_model_selector(cx)) .child( ButtonLike::new("chat") diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs index 3eb333688a1544..b7e792e15b7c3e 100644 --- a/crates/assistant2/src/thread_history.rs +++ b/crates/assistant2/src/thread_history.rs @@ -120,7 +120,11 @@ impl RenderOnce for PastThread { ListItem::new(("past-thread", self.thread.entity_id())) .outlined() - .start_slot(Icon::new(IconName::MessageBubbles)) + .start_slot( + Icon::new(IconName::MessageCircle) + .size(IconSize::Small) + .color(Color::Muted), + ) .spacing(ListItemSpacing::Sparse) .child(Label::new(summary).size(LabelSize::Small)) .end_slot( diff --git a/crates/assistant2/src/ui/context_pill.rs b/crates/assistant2/src/ui/context_pill.rs index dd74465ad0adb2..aa27e82b273355 100644 --- a/crates/assistant2/src/ui/context_pill.rs +++ b/crates/assistant2/src/ui/context_pill.rs @@ -29,9 +29,12 @@ impl RenderOnce for ContextPill { fn render(self, cx: &mut WindowContext) -> impl IntoElement { h_flex() .gap_1() - .px_1() + .pl_1p5() + .pr_0p5() + .pb(px(1.)) .border_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border.opacity(0.5)) + .bg(cx.theme().colors().element_background) .rounded_md() .child(Label::new(self.context.name.clone()).size(LabelSize::Small)) .when_some(self.on_remove, |parent, on_remove| { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 66601a853cac24..6a01ca0b739748 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -218,6 +218,7 @@ pub enum IconName { Maximize, Menu, MessageBubbles, + MessageCircle, Mic, MicMute, Microscope, From 97d9567188d5bbe27ad1e529d2e293381b45d14d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Dec 2024 00:33:43 +0200 Subject: [PATCH 23/32] Show a brighter border around folded blocks with selections (#22114) Follow-up of https://github.com/zed-industries/zed/pull/22046 Properly [un]fold blocks based on the selections image Release Notes: - N/A --- crates/editor/src/editor.rs | 91 +++++++++++++----------------------- crates/editor/src/element.rs | 27 +++++++++-- 2 files changed, 57 insertions(+), 61 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2bda8878754510..39259c5eed05bc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10329,31 +10329,20 @@ impl Editor { self.fold(&Default::default(), cx) } } else { - let (display_snapshot, selections) = self.selections.all_adjusted_display(cx); + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); let mut toggled_buffers = HashSet::default(); - for selection in selections { - if let Some(buffer_id) = display_snapshot - .display_point_to_anchor(selection.head(), Bias::Right) - .buffer_id - { - if toggled_buffers.insert(buffer_id) { - if self.buffer_folded(buffer_id, cx) { - self.unfold_buffer(buffer_id, cx); - } else { - self.fold_buffer(buffer_id, cx); - } - } - } - if let Some(buffer_id) = display_snapshot - .display_point_to_anchor(selection.tail(), Bias::Left) - .buffer_id - { - if toggled_buffers.insert(buffer_id) { - if self.buffer_folded(buffer_id, cx) { - self.unfold_buffer(buffer_id, cx); - } else { - self.fold_buffer(buffer_id, cx); - } + for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges( + self.selections + .disjoint_anchors() + .into_iter() + .map(|selection| selection.range()), + ) { + let buffer_id = buffer_snapshot.remote_id(); + if toggled_buffers.insert(buffer_id) { + if self.buffer_folded(buffer_id, cx) { + self.unfold_buffer(buffer_id, cx); + } else { + self.fold_buffer(buffer_id, cx); } } } @@ -10426,24 +10415,17 @@ impl Editor { self.fold_creases(to_fold, true, cx); } else { - let (display_snapshot, selections) = self.selections.all_adjusted_display(cx); + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); let mut folded_buffers = HashSet::default(); - for selection in selections { - if let Some(buffer_id) = display_snapshot - .display_point_to_anchor(selection.head(), Bias::Right) - .buffer_id - { - if folded_buffers.insert(buffer_id) { - self.fold_buffer(buffer_id, cx); - } - } - if let Some(buffer_id) = display_snapshot - .display_point_to_anchor(selection.tail(), Bias::Left) - .buffer_id - { - if folded_buffers.insert(buffer_id) { - self.fold_buffer(buffer_id, cx); - } + for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges( + self.selections + .disjoint_anchors() + .into_iter() + .map(|selection| selection.range()), + ) { + let buffer_id = buffer_snapshot.remote_id(); + if folded_buffers.insert(buffer_id) { + self.fold_buffer(buffer_id, cx); } } } @@ -10599,24 +10581,17 @@ impl Editor { self.unfold_ranges(&ranges, true, true, cx); } else { - let (display_snapshot, selections) = self.selections.all_adjusted_display(cx); + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); let mut unfolded_buffers = HashSet::default(); - for selection in selections { - if let Some(buffer_id) = display_snapshot - .display_point_to_anchor(selection.head(), Bias::Right) - .buffer_id - { - if unfolded_buffers.insert(buffer_id) { - self.unfold_buffer(buffer_id, cx); - } - } - if let Some(buffer_id) = display_snapshot - .display_point_to_anchor(selection.tail(), Bias::Left) - .buffer_id - { - if unfolded_buffers.insert(buffer_id) { - self.unfold_buffer(buffer_id, cx); - } + for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges( + self.selections + .disjoint_anchors() + .into_iter() + .map(|selection| selection.range()), + ) { + let buffer_id = buffer_snapshot.remote_id(); + if unfolded_buffers.insert(buffer_id) { + self.unfold_buffer(buffer_id, cx); } } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d0ed31dc7d7dbf..37b69f1d7721c0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2151,8 +2151,20 @@ impl EditorElement { prev_excerpt, show_excerpt_controls, height, - .. } => { + let block_start = DisplayPoint::new(block_row_start, 0).to_point(snapshot); + let block_end = DisplayPoint::new(block_row_start + *height, 0).to_point(snapshot); + let selected = selections + .binary_search_by(|selection| { + if selection.end <= block_start { + Ordering::Less + } else if selection.start >= block_end { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .is_ok(); let icon_offset = gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin); @@ -2181,6 +2193,7 @@ impl EditorElement { first_excerpt, header_padding, true, + selected, jump_data, cx, )) @@ -2192,7 +2205,6 @@ impl EditorElement { show_excerpt_controls, height, starts_new_buffer, - .. } => { let icon_offset = gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin); @@ -2223,6 +2235,7 @@ impl EditorElement { next_excerpt, header_padding, false, + false, jump_data, cx, )); @@ -2380,6 +2393,7 @@ impl EditorElement { for_excerpt: &ExcerptInfo, header_padding: Pixels, is_folded: bool, + is_selected: bool, jump_data: JumpData, cx: &mut WindowContext, ) -> Div { @@ -2415,7 +2429,14 @@ impl EditorElement { .rounded_md() .shadow_md() .border_1() - .border_color(cx.theme().colors().border) + .map(|div| { + let border_color = if is_selected { + cx.theme().colors().text_accent + } else { + cx.theme().colors().border + }; + div.border_color(border_color) + }) .bg(cx.theme().colors().editor_subheader_background) .hover(|style| style.bg(cx.theme().colors().element_hover)) .map(|header| { From 1932c04b84a10c96c09dd821ad6895be3b7edc7c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 17:35:56 -0500 Subject: [PATCH 24/32] assistant2: Add ability to resize the panel (#22113) This PR adds the ability to resize the Assistant2 panel. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 43 +++++++++++++++++++-- crates/assistant2/src/assistant_settings.rs | 16 ++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 8d6cc50d81026b..63d52d6d01d486 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -3,18 +3,21 @@ use std::sync::Arc; use anyhow::Result; use assistant_tool::ToolWorkingSet; use client::zed_urls; +use fs::Fs; use gpui::{ prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; use language::LanguageRegistry; +use settings::Settings; use time::UtcOffset; use ui::{prelude::*, KeyBinding, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::active_thread::ActiveThread; +use crate::assistant_settings::{AssistantDockPosition, AssistantSettings}; use crate::message_editor::MessageEditor; use crate::thread::{ThreadError, ThreadId}; use crate::thread_history::{PastThread, ThreadHistory}; @@ -39,6 +42,7 @@ enum ActiveView { pub struct AssistantPanel { workspace: WeakView, + fs: Arc, language_registry: Arc, thread_store: Model, thread: View, @@ -47,6 +51,8 @@ pub struct AssistantPanel { local_timezone: UtcOffset, active_view: ActiveView, history: View, + width: Option, + height: Option, } impl AssistantPanel { @@ -76,6 +82,7 @@ impl AssistantPanel { cx: &mut ViewContext, ) -> Self { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); + let fs = workspace.app_state().fs.clone(); let language_registry = workspace.project().read(cx).languages().clone(); let workspace = workspace.weak_handle(); let weak_self = cx.view().downgrade(); @@ -83,6 +90,7 @@ impl AssistantPanel { Self { active_view: ActiveView::Thread, workspace: workspace.clone(), + fs, language_registry: language_registry.clone(), thread_store: thread_store.clone(), thread: cx.new_view(|cx| { @@ -103,6 +111,8 @@ impl AssistantPanel { ) .unwrap(), history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)), + width: None, + height: None, } } @@ -199,13 +209,38 @@ impl Panel for AssistantPanel { true } - fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {} + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings, _| { + let dock = match position { + DockPosition::Left => AssistantDockPosition::Left, + DockPosition::Bottom => AssistantDockPosition::Bottom, + DockPosition::Right => AssistantDockPosition::Right, + }; + settings.set_dock(dock); + }, + ); + } - fn size(&self, _cx: &WindowContext) -> Pixels { - px(550.) + fn size(&self, cx: &WindowContext) -> Pixels { + let settings = AssistantSettings::get_global(cx); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => { + self.width.unwrap_or(settings.default_width) + } + DockPosition::Bottom => self.height.unwrap_or(settings.default_height), + } } - fn set_size(&mut self, _size: Option, _cx: &mut ViewContext) {} + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + match self.position(cx) { + DockPosition::Left | DockPosition::Right => self.width = size, + DockPosition::Bottom => self.height = size, + } + cx.notify(); + } fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} diff --git a/crates/assistant2/src/assistant_settings.rs b/crates/assistant2/src/assistant_settings.rs index 2c7886faeaa6c7..1d6d1cd850d2e6 100644 --- a/crates/assistant2/src/assistant_settings.rs +++ b/crates/assistant2/src/assistant_settings.rs @@ -157,6 +157,22 @@ impl AssistantSettingsContent { } } + pub fn set_dock(&mut self, dock: AssistantDockPosition) { + match self { + AssistantSettingsContent::Versioned(settings) => match settings { + VersionedAssistantSettingsContent::V1(settings) => { + settings.dock = Some(dock); + } + VersionedAssistantSettingsContent::V2(settings) => { + settings.dock = Some(dock); + } + }, + AssistantSettingsContent::Legacy(settings) => { + settings.dock = Some(dock); + } + } + } + pub fn set_model(&mut self, language_model: Arc) { let model = language_model.id().0.to_string(); let provider = language_model.provider_id().0.to_string(); From eb74332e967d32d722bd2fc2d34ba491efff9feb Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Mon, 16 Dec 2024 22:54:55 +0000 Subject: [PATCH 25/32] extensions_ui: Add Cython as a suggested extension (#22053) This suggest the [Cython extension](https://github.com/lgeiger/zed-cython) for syntax highlighting of Cython files. Release Notes: - Suggest Cython extension for syntax highlighting of `.pyx`, `.pxd` and `.pxi` files --- crates/extensions_ui/src/extension_suggest.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index dd1aae9cd665c4..9bd2dec03e8afa 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -19,6 +19,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("clojure", &["bb", "clj", "cljc", "cljs", "edn"]), ("neocmake", &["CMakeLists.txt", "cmake"]), ("csharp", &["cs"]), + ("cython", &["pyx", "pxd", "pxi"]), ("dart", &["dart"]), ("dockerfile", &["Dockerfile"]), ("elisp", &["el"]), From 373854be4614b92745e72014ff66a0d1ea431087 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 17:57:05 -0500 Subject: [PATCH 26/32] assistant2: Uniquely identify remove buttons on `ContextPill`s (#22115) This PR ensures that the remove buttons on the `ContextPill`s are uniquely identified. Release Notes: - N/A --- crates/assistant2/src/ui/context_pill.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/assistant2/src/ui/context_pill.rs b/crates/assistant2/src/ui/context_pill.rs index aa27e82b273355..d2f020a582e31d 100644 --- a/crates/assistant2/src/ui/context_pill.rs +++ b/crates/assistant2/src/ui/context_pill.rs @@ -39,7 +39,7 @@ impl RenderOnce for ContextPill { .child(Label::new(self.context.name.clone()).size(LabelSize::Small)) .when_some(self.on_remove, |parent, on_remove| { parent.child( - IconButton::new("remove", IconName::Close) + IconButton::new(("remove", self.context.id.0), IconName::Close) .shape(IconButtonShape::Square) .icon_size(IconSize::XSmall) .on_click({ From db2aa0bca542e6b1f6cee990442aed73b398be85 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Dec 2024 01:00:11 +0200 Subject: [PATCH 27/32] Use a proper color for the folded buffer border selection --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 37b69f1d7721c0..59c990b69da6ff 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2431,7 +2431,7 @@ impl EditorElement { .border_1() .map(|div| { let border_color = if is_selected { - cx.theme().colors().text_accent + cx.theme().colors().border_focused } else { cx.theme().colors().border }; From ccf2a60039182226edff69b1b6e60abc157fe5db Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 18:00:40 -0500 Subject: [PATCH 28/32] assistant2: Persist model selector changes (#22116) This PR makes the language model selector in the Assistant2 panel persist the model changes to the settings. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 12 ++++++++++-- crates/assistant2/src/message_editor.rs | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 63d52d6d01d486..3a756d9d044f47 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -90,7 +90,7 @@ impl AssistantPanel { Self { active_view: ActiveView::Thread, workspace: workspace.clone(), - fs, + fs: fs.clone(), language_registry: language_registry.clone(), thread_store: thread_store.clone(), thread: cx.new_view(|cx| { @@ -103,7 +103,13 @@ impl AssistantPanel { ) }), message_editor: cx.new_view(|cx| { - MessageEditor::new(workspace, thread_store.downgrade(), thread.clone(), cx) + MessageEditor::new( + fs.clone(), + workspace, + thread_store.downgrade(), + thread.clone(), + cx, + ) }), tools, local_timezone: UtcOffset::from_whole_seconds( @@ -141,6 +147,7 @@ impl AssistantPanel { }); self.message_editor = cx.new_view(|cx| { MessageEditor::new( + self.fs.clone(), self.workspace.clone(), self.thread_store.downgrade(), thread, @@ -170,6 +177,7 @@ impl AssistantPanel { }); self.message_editor = cx.new_view(|cx| { MessageEditor::new( + self.fs.clone(), self.workspace.clone(), self.thread_store.downgrade(), thread, diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 179909d91928f8..50715d91a469f1 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,12 +1,16 @@ +use std::sync::Arc; + use editor::{Editor, EditorElement, EditorStyle}; +use fs::Fs; use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView}; use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; -use settings::Settings; +use settings::{update_settings_file, Settings}; use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, Tooltip}; use workspace::Workspace; +use crate::assistant_settings::AssistantSettings; use crate::context_store::ContextStore; use crate::context_strip::ContextStrip; use crate::thread::{RequestKind, Thread}; @@ -24,6 +28,7 @@ pub struct MessageEditor { impl MessageEditor { pub fn new( + fs: Arc, workspace: WeakView, thread_store: WeakModel, thread: Model, @@ -50,9 +55,14 @@ impl MessageEditor { ) }), language_model_selector: cx.new_view(|cx| { + let fs = fs.clone(); LanguageModelSelector::new( - |model, _cx| { - println!("Selected {:?}", model.name()); + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _cx| settings.set_model(model.clone()), + ); }, cx, ) From ac24f074df4920bc02059e9787459657a100edd7 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 16 Dec 2024 16:24:38 -0700 Subject: [PATCH 29/32] Use Popover and ListItem in code actions menu (#22112) This is good for code sharing but also sets up #22102 for making assumptions about popover y padding. Release Notes: - N/A --- crates/editor/src/code_context_menus.rs | 83 ++++++++++++------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index bf19f2a00e86a4..9ed52a252e5412 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -4,7 +4,7 @@ use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior, - Model, MouseButton, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, + Model, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle, ViewContext, WeakView, }; use language::Buffer; @@ -17,7 +17,7 @@ use task::ResolvedTask; use ui::{ h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement, Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, - StatefulInteractiveElement as _, Styled, StyledExt as _, Toggleable as _, + StatefulInteractiveElement as _, Styled, Toggleable as _, }; use util::ResultExt as _; use workspace::Workspace; @@ -788,7 +788,7 @@ impl CodeActionsMenu { ) -> (ContextMenuOrigin, AnyElement) { let actions = self.actions.clone(); let selected_item = self.selected_item; - let element = uniform_list( + let list = uniform_list( cx.view().clone(), "code_actions_menu", self.actions.len(), @@ -800,27 +800,14 @@ impl CodeActionsMenu { .enumerate() .map(|(ix, action)| { let item_ix = range.start + ix; - let selected = selected_item == item_ix; + let selected = item_ix == selected_item; let colors = cx.theme().colors(); - div() - .px_1() - .rounded_md() - .text_color(colors.text) - .when(selected, |style| { - style - .bg(colors.element_active) - .text_color(colors.text_accent) - }) - .hover(|style| { - style - .bg(colors.element_hover) - .text_color(colors.text_accent) - }) - .whitespace_nowrap() - .when_some(action.as_code_action(), |this, action| { - this.on_mouse_down( - MouseButton::Left, - cx.listener(move |editor, _, cx| { + div().min_w(px(220.)).max_w(px(540.)).child( + ListItem::new(item_ix) + .inset(true) + .toggle_state(selected) + .when_some(action.as_code_action(), |this, action| { + this.on_click(cx.listener(move |editor, _, cx| { cx.stop_propagation(); if let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { @@ -830,17 +817,21 @@ impl CodeActionsMenu { ) { task.detach_and_log_err(cx) } - }), - ) - // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. - .child(SharedString::from( - action.lsp_action.title.replace("\n", ""), - )) - }) - .when_some(action.as_task(), |this, task| { - this.on_mouse_down( - MouseButton::Left, - cx.listener(move |editor, _, cx| { + })) + .child( + h_flex() + .overflow_hidden() + .child( + // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. + action.lsp_action.title.replace("\n", ""), + ) + .when(selected, |this| { + this.text_color(colors.text_accent) + }), + ) + }) + .when_some(action.as_task(), |this, task| { + this.on_click(cx.listener(move |editor, _, cx| { cx.stop_propagation(); if let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { @@ -850,18 +841,23 @@ impl CodeActionsMenu { ) { task.detach_and_log_err(cx) } - }), - ) - .child(SharedString::from(task.resolved_label.replace("\n", ""))) - }) + })) + .child( + h_flex() + .overflow_hidden() + .child(task.resolved_label.replace("\n", "")) + .when(selected, |this| { + this.text_color(colors.text_accent) + }), + ) + }), + ) }) .collect() }, ) - .elevation_1(cx) - .p_1() - .max_h(max_height) .occlude() + .max_h(max_height) .track_scroll(self.scroll_handle.clone()) .with_width_from_item( self.actions @@ -875,8 +871,9 @@ impl CodeActionsMenu { }) .map(|(ix, _)| ix), ) - .with_sizing_behavior(ListSizingBehavior::Infer) - .into_any_element(); + .with_sizing_behavior(ListSizingBehavior::Infer); + + let element = Popover::new().child(list).into_any_element(); let cursor_position = if let Some(row) = self.deployed_from_indicator { ContextMenuOrigin::GutterIndicator(row) From 8e71e468673126051b7c1ba9071015924c707c92 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 18:43:01 -0500 Subject: [PATCH 30/32] ui: Add `text_ellipsis` method to `Label`s (#22118) This PR adds a `text_ellipsis` method to `Label`s. This can be used to truncate the text with an ellipsis without needing to wrap the `Label` in another element. Release Notes: - N/A --- crates/assistant/src/slash_command_picker.rs | 9 ++- crates/extensions_ui/src/extensions_ui.rs | 64 +++++++++---------- .../src/components/label/highlighted_label.rs | 5 ++ crates/ui/src/components/label/label.rs | 5 ++ crates/ui/src/components/label/label_like.rs | 13 ++++ 5 files changed, 57 insertions(+), 39 deletions(-) diff --git a/crates/assistant/src/slash_command_picker.rs b/crates/assistant/src/slash_command_picker.rs index 612eb5d4250cda..310e11322ebdb8 100644 --- a/crates/assistant/src/slash_command_picker.rs +++ b/crates/assistant/src/slash_command_picker.rs @@ -217,11 +217,10 @@ impl PickerDelegate for SlashCommandDelegate { )), ) .child( - div().overflow_hidden().text_ellipsis().child( - Label::new(info.description.clone()) - .size(LabelSize::Small) - .color(Color::Muted), - ), + Label::new(info.description.clone()) + .size(LabelSize::Small) + .color(Color::Muted) + .text_ellipsis(), ), ), ), diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 248e3117369f45..edbe188ee602ca 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -449,18 +449,17 @@ impl ExtensionsPage { .gap_2() .justify_between() .child( - div().overflow_x_hidden().text_ellipsis().child( - Label::new(format!( - "{}: {}", - if extension.authors.len() > 1 { - "Authors" - } else { - "Author" - }, - extension.authors.join(", ") - )) - .size(LabelSize::Small), - ), + Label::new(format!( + "{}: {}", + if extension.authors.len() > 1 { + "Authors" + } else { + "Author" + }, + extension.authors.join(", ") + )) + .size(LabelSize::Small) + .text_ellipsis(), ) .child(Label::new("<>").size(LabelSize::Small)), ) @@ -469,11 +468,10 @@ impl ExtensionsPage { .gap_2() .justify_between() .children(extension.description.as_ref().map(|description| { - div().overflow_x_hidden().text_ellipsis().child( - Label::new(description.clone()) - .size(LabelSize::Small) - .color(Color::Default), - ) + Label::new(description.clone()) + .size(LabelSize::Small) + .color(Color::Default) + .text_ellipsis() })) .children(repository_url.map(|repository_url| { IconButton::new( @@ -550,18 +548,17 @@ impl ExtensionsPage { .gap_2() .justify_between() .child( - div().overflow_x_hidden().text_ellipsis().child( - Label::new(format!( - "{}: {}", - if extension.manifest.authors.len() > 1 { - "Authors" - } else { - "Author" - }, - extension.manifest.authors.join(", ") - )) - .size(LabelSize::Small), - ), + Label::new(format!( + "{}: {}", + if extension.manifest.authors.len() > 1 { + "Authors" + } else { + "Author" + }, + extension.manifest.authors.join(", ") + )) + .size(LabelSize::Small) + .text_ellipsis(), ) .child( Label::new(format!( @@ -576,11 +573,10 @@ impl ExtensionsPage { .gap_2() .justify_between() .children(extension.manifest.description.as_ref().map(|description| { - div().overflow_x_hidden().text_ellipsis().child( - Label::new(description.clone()) - .size(LabelSize::Small) - .color(Color::Default), - ) + Label::new(description.clone()) + .size(LabelSize::Small) + .color(Color::Default) + .text_ellipsis() })) .child( h_flex() diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index 0e6cc26b182729..c6e5b444cc5ed0 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -66,6 +66,11 @@ impl LabelCommon for HighlightedLabel { self } + fn text_ellipsis(mut self) -> Self { + self.base = self.base.text_ellipsis(); + self + } + fn single_line(mut self) -> Self { self.base = self.base.single_line(); self diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 1df33d27403be2..5ec557d7e69fa0 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -164,6 +164,11 @@ impl LabelCommon for Label { self } + fn text_ellipsis(mut self) -> Self { + self.base = self.base.text_ellipsis(); + self + } + fn single_line(mut self) -> Self { self.single_line = true; self.base = self.base.single_line(); diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index b1c3240f5a7287..683db552d6b252 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -50,6 +50,9 @@ pub trait LabelCommon { /// Sets the alpha property of the label, overwriting the alpha value of the color. fn alpha(self, alpha: f32) -> Self; + /// Truncates overflowing text with an ellipsis (`…`) if needed. + fn text_ellipsis(self) -> Self; + /// Sets the label to render as a single line. fn single_line(self) -> Self; } @@ -67,6 +70,7 @@ pub struct LabelLike { alpha: Option, underline: bool, single_line: bool, + text_ellipsis: bool, } impl Default for LabelLike { @@ -89,6 +93,7 @@ impl LabelLike { alpha: None, underline: false, single_line: false, + text_ellipsis: false, } } } @@ -145,6 +150,11 @@ impl LabelCommon for LabelLike { self } + fn text_ellipsis(mut self) -> Self { + self.text_ellipsis = true; + self + } + fn single_line(mut self) -> Self { self.single_line = true; self @@ -189,6 +199,9 @@ impl RenderOnce for LabelLike { }) .when(self.strikethrough, |this| this.line_through()) .when(self.single_line, |this| this.whitespace_nowrap()) + .when(self.text_ellipsis, |this| { + this.overflow_x_hidden().text_ellipsis() + }) .text_color(color) .font_weight(self.weight.unwrap_or(settings.ui_font.weight)) .children(self.children) From d11deff3c2c66ff3a131f0dba6f4ef2de069cf6b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 18:43:16 -0500 Subject: [PATCH 31/32] ui: Round hover styles for `ListItem`s with `outlined` set (#22120) This PR makes `ListItem`s with `outlined` set use the same rounding for their hover state to ensure that the hover background doesn't bleed outside of the outline. Release Notes: - N/A --- crates/ui/src/components/list/list_item.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 908c873f9d740d..a33916380f0c3f 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -202,6 +202,7 @@ impl RenderOnce for ListItem { .when(self.selectable, |this| { this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.outlined, |this| this.rounded_md()) .when(self.selected, |this| { this.bg(cx.theme().colors().ghost_element_selected) }) From 0ca0433912ad486b676944949e69e82b0d77031b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 16 Dec 2024 19:19:24 -0500 Subject: [PATCH 32/32] assistant2: Add keybinding to toggle `LanguageModelSelector` (#22122) This PR adds a keybinding to toggle the `LanguageModelSelector` in Assistant2. Release Notes: - N/A --- assets/keymaps/default-macos.json | 3 ++- crates/assistant2/src/message_editor.rs | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 321aa283690c07..5bbc93c390e86a 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -224,7 +224,8 @@ "use_key_equivalents": true, "bindings": { "cmd-n": "assistant2::NewThread", - "cmd-shift-h": "assistant2::OpenHistory" + "cmd-shift-h": "assistant2::OpenHistory", + "cmd-shift-m": "assistant2::ToggleModelSelector" } }, { diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 50715d91a469f1..64a40f5b9570c8 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -7,7 +7,10 @@ use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use settings::{update_settings_file, Settings}; use theme::ThemeSettings; -use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, Tooltip}; +use ui::{ + prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle, + Tooltip, +}; use workspace::Workspace; use crate::assistant_settings::AssistantSettings; @@ -23,6 +26,7 @@ pub struct MessageEditor { context_store: Model, context_strip: View, language_model_selector: View, + language_model_selector_menu_handle: PopoverMenuHandle, use_tools: bool, } @@ -67,10 +71,15 @@ impl MessageEditor { cx, ) }), + language_model_selector_menu_handle: PopoverMenuHandle::default(), use_tools: false, } } + fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext) { + self.language_model_selector_menu_handle.toggle(cx); + } + fn chat(&mut self, _: &Chat, cx: &mut ViewContext) { self.send_to_model(RequestKind::Chat, cx); } @@ -163,6 +172,7 @@ impl MessageEditor { Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) }), ) + .with_handle(self.language_model_selector_menu_handle.clone()) } } @@ -182,6 +192,7 @@ impl Render for MessageEditor { v_flex() .key_context("MessageEditor") .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(Self::toggle_model_selector)) .size_full() .gap_2() .p_2()