Detailed changes
@@ -490,6 +490,9 @@
"version": "2",
// Whether the assistant is enabled.
"enabled": true,
+ // Whether to show inline hints showing the keybindings to use the inline assistant and the
+ // assistant panel.
+ "show_hints": true,
// Whether to show the assistant panel button in the status bar.
"button": true,
// Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'.
@@ -60,6 +60,7 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
+ pub show_hints: bool,
}
impl AssistantSettings {
@@ -202,6 +203,7 @@ impl AssistantSettingsContent {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
+ show_hints: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
@@ -242,6 +244,7 @@ impl AssistantSettingsContent {
},
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
enabled: None,
+ show_hints: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
@@ -354,6 +357,7 @@ impl Default for VersionedAssistantSettingsContent {
fn default() -> Self {
Self::V2(AssistantSettingsContentV2 {
enabled: None,
+ show_hints: None,
button: None,
dock: None,
default_width: None,
@@ -371,6 +375,11 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: true
enabled: Option<bool>,
+ /// Whether to show inline hints that show keybindings for inline assistant
+ /// and assistant panel.
+ ///
+ /// Default: true
+ show_hints: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
@@ -505,6 +514,7 @@ impl Settings for AssistantSettings {
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
+ merge(&mut settings.show_hints, value.show_hints);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
@@ -575,6 +585,7 @@ mod tests {
}),
inline_alternatives: None,
enabled: None,
+ show_hints: None,
button: None,
dock: None,
default_width: None,
@@ -540,6 +540,15 @@ pub enum IsVimMode {
No,
}
+pub trait ActiveLineTrailerProvider {
+ fn render_active_line_trailer(
+ &mut self,
+ style: &EditorStyle,
+ focus_handle: &FocusHandle,
+ cx: &mut WindowContext,
+ ) -> Option<AnyElement>;
+}
+
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@@ -667,6 +676,7 @@ pub struct Editor {
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
_scroll_cursor_center_top_bottom_task: Task<()>,
+ active_line_trailer_provider: Option<Box<dyn ActiveLineTrailerProvider>>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -2200,6 +2210,7 @@ impl Editor {
addons: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
text_style_refinement: None,
+ active_line_trailer_provider: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
this._subscriptions.extend(project_subscriptions);
@@ -2488,6 +2499,16 @@ impl Editor {
self.refresh_inline_completion(false, false, cx);
}
+ pub fn set_active_line_trailer_provider<T>(
+ &mut self,
+ provider: Option<T>,
+ _cx: &mut ViewContext<Self>,
+ ) where
+ T: ActiveLineTrailerProvider + 'static,
+ {
+ self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>);
+ }
+
pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> {
self.placeholder_text.as_deref()
}
@@ -11844,6 +11865,21 @@ impl Editor {
&& self.has_blame_entries(cx)
}
+ pub fn render_active_line_trailer(
+ &mut self,
+ style: &EditorStyle,
+ cx: &mut WindowContext,
+ ) -> Option<AnyElement> {
+ if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) {
+ return None;
+ }
+
+ let focus_handle = self.focus_handle.clone();
+ self.active_line_trailer_provider
+ .as_mut()?
+ .render_active_line_trailer(style, &focus_handle, cx)
+ }
+
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
self.blame()
.map_or(false, |blame| blame.read(cx).has_generated_entries())
@@ -1412,7 +1412,7 @@ impl EditorElement {
}
#[allow(clippy::too_many_arguments)]
- fn layout_inline_blame(
+ fn layout_active_line_trailer(
&self,
display_row: DisplayRow,
display_snapshot: &DisplaySnapshot,
@@ -1424,61 +1424,71 @@ impl EditorElement {
line_height: Pixels,
cx: &mut WindowContext,
) -> Option<AnyElement> {
- if !self
+ let render_inline_blame = self
.editor
- .update(cx, |editor, cx| editor.render_git_blame_inline(cx))
- {
- return None;
- }
+ .update(cx, |editor, cx| editor.render_git_blame_inline(cx));
+ if render_inline_blame {
+ let workspace = self
+ .editor
+ .read(cx)
+ .workspace
+ .as_ref()
+ .map(|(w, _)| w.clone());
- let workspace = self
- .editor
- .read(cx)
- .workspace
- .as_ref()
- .map(|(w, _)| w.clone());
+ let display_point = DisplayPoint::new(display_row, 0);
+ let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
- let display_point = DisplayPoint::new(display_row, 0);
- let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
+ let blame = self.editor.read(cx).blame.clone()?;
+ let blame_entry = blame
+ .update(cx, |blame, cx| {
+ blame.blame_for_rows([Some(buffer_row)], cx).next()
+ })
+ .flatten()?;
- let blame = self.editor.read(cx).blame.clone()?;
- let blame_entry = blame
- .update(cx, |blame, cx| {
- blame.blame_for_rows([Some(buffer_row)], cx).next()
- })
- .flatten()?;
+ let mut element =
+ render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
- let mut element =
- render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
+ let start_y = content_origin.y
+ + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
- let start_y = content_origin.y
- + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
+ let start_x = {
+ const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
- let start_x = {
- const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
+ let line_end = if let Some(crease_trailer) = crease_trailer {
+ crease_trailer.bounds.right()
+ } else {
+ content_origin.x - scroll_pixel_position.x + line_layout.width
+ };
+ let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
- let line_end = if let Some(crease_trailer) = crease_trailer {
- crease_trailer.bounds.right()
- } else {
- content_origin.x - scroll_pixel_position.x + line_layout.width
- };
- let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
+ let min_column_in_pixels = ProjectSettings::get_global(cx)
+ .git
+ .inline_blame
+ .and_then(|settings| settings.min_column)
+ .map(|col| self.column_pixels(col as usize, cx))
+ .unwrap_or(px(0.));
+ let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
- let min_column_in_pixels = ProjectSettings::get_global(cx)
- .git
- .inline_blame
- .and_then(|settings| settings.min_column)
- .map(|col| self.column_pixels(col as usize, cx))
- .unwrap_or(px(0.));
- let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
+ cmp::max(padded_line_end, min_start)
+ };
- cmp::max(padded_line_end, min_start)
- };
+ let absolute_offset = point(start_x, start_y);
+ element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
- let absolute_offset = point(start_x, start_y);
- element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
+ Some(element)
+ } else if let Some(mut element) = self.editor.update(cx, |editor, cx| {
+ editor.render_active_line_trailer(&self.style, cx)
+ }) {
+ let start_y = content_origin.y
+ + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
+ let start_x = content_origin.x - scroll_pixel_position.x + em_width;
+ let absolute_offset = point(start_x, start_y);
+ element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
- Some(element)
+ Some(element)
+ } else {
+ None
+ }
}
#[allow(clippy::too_many_arguments)]
@@ -3454,7 +3464,7 @@ impl EditorElement {
self.paint_lines(&invisible_display_ranges, layout, cx);
self.paint_redactions(layout, cx);
self.paint_cursors(layout, cx);
- self.paint_inline_blame(layout, cx);
+ self.paint_active_line_trailer(layout, cx);
cx.with_element_namespace("crease_trailers", |cx| {
for trailer in layout.crease_trailers.iter_mut().flatten() {
trailer.element.paint(cx);
@@ -3936,10 +3946,10 @@ impl EditorElement {
}
}
- fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
- if let Some(mut inline_blame) = layout.inline_blame.take() {
+ fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
+ if let Some(mut element) = layout.active_line_trailer.take() {
cx.paint_layer(layout.text_hitbox.bounds, |cx| {
- inline_blame.paint(cx);
+ element.paint(cx);
})
}
}
@@ -5331,14 +5341,14 @@ impl Element for EditorElement {
)
});
- let mut inline_blame = None;
+ let mut active_line_trailer = None;
if let Some(newest_selection_head) = newest_selection_head {
let display_row = newest_selection_head.row();
if (start_row..end_row).contains(&display_row) {
let line_ix = display_row.minus(start_row) as usize;
let line_layout = &line_layouts[line_ix];
let crease_trailer_layout = crease_trailers[line_ix].as_ref();
- inline_blame = self.layout_inline_blame(
+ active_line_trailer = self.layout_active_line_trailer(
display_row,
&snapshot.display_snapshot,
line_layout,
@@ -5657,7 +5667,7 @@ impl Element for EditorElement {
line_elements,
line_numbers,
blamed_display_rows,
- inline_blame,
+ active_line_trailer,
blocks,
cursors,
visible_cursors,
@@ -5794,7 +5804,7 @@ pub struct EditorLayout {
line_numbers: Vec<Option<ShapedLine>>,
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
blamed_display_rows: Option<Vec<AnyElement>>,
- inline_blame: Option<AnyElement>,
+ active_line_trailer: Option<AnyElement>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -3050,7 +3050,7 @@ impl<'a> WindowContext<'a> {
}
/// Represent this action as a key binding string, to display in the UI.
- pub fn keystroke_text_for(&self, action: &dyn Action) -> String {
+ pub fn keystroke_text_for_action(&self, action: &dyn Action) -> String {
self.bindings_for_action(action)
.into_iter()
.next()
@@ -3065,6 +3065,26 @@ impl<'a> WindowContext<'a> {
.unwrap_or_else(|| action.name().to_string())
}
+ /// Represent this action as a key binding string, to display in the UI.
+ pub fn keystroke_text_for_action_in(
+ &self,
+ action: &dyn Action,
+ focus_handle: &FocusHandle,
+ ) -> String {
+ self.bindings_for_action_in(action, focus_handle)
+ .into_iter()
+ .next()
+ .map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<_>>()
+ .join(" ")
+ })
+ .unwrap_or_else(|| action.name().to_string())
+ }
+
/// Dispatch a mouse or keyboard event on the window.
#[profiling::function]
pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult {
@@ -3875,13 +3875,13 @@ impl OutlinePanel {
.child({
let keystroke = match self.position(cx) {
DockPosition::Left => {
- cx.keystroke_text_for(&workspace::ToggleLeftDock)
+ cx.keystroke_text_for_action(&workspace::ToggleLeftDock)
}
DockPosition::Bottom => {
- cx.keystroke_text_for(&workspace::ToggleBottomDock)
+ cx.keystroke_text_for_action(&workspace::ToggleBottomDock)
}
DockPosition::Right => {
- cx.keystroke_text_for(&workspace::ToggleRightDock)
+ cx.keystroke_text_for_action(&workspace::ToggleRightDock)
}
};
Label::new(format!("Toggle this panel with {keystroke}"))
@@ -185,13 +185,13 @@ impl PickerDelegate for RecentProjectsDelegate {
fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> {
let (create_window, reuse_window) = if self.create_new_window {
(
- cx.keystroke_text_for(&menu::Confirm),
- cx.keystroke_text_for(&menu::SecondaryConfirm),
+ cx.keystroke_text_for_action(&menu::Confirm),
+ cx.keystroke_text_for_action(&menu::SecondaryConfirm),
)
} else {
(
- cx.keystroke_text_for(&menu::SecondaryConfirm),
- cx.keystroke_text_for(&menu::Confirm),
+ cx.keystroke_text_for_action(&menu::SecondaryConfirm),
+ cx.keystroke_text_for_action(&menu::Confirm),
)
};
Arc::from(format!(
@@ -66,7 +66,7 @@ use zed::{
OpenRequest,
};
-use crate::zed::inline_completion_registry;
+use crate::zed::{assistant_hints, inline_completion_registry};
#[cfg(feature = "mimalloc")]
#[global_allocator]
@@ -401,6 +401,7 @@ fn main() {
stdout_is_a_pty(),
cx,
);
+ assistant_hints::init(cx);
repl::init(
app_state.fs.clone(),
app_state.client.telemetry().clone(),
@@ -1,4 +1,5 @@
mod app_menus;
+pub mod assistant_hints;
pub mod inline_completion_registry;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub(crate) mod linux_prompts;
@@ -0,0 +1,115 @@
+use assistant::assistant_settings::AssistantSettings;
+use collections::HashMap;
+use editor::{ActiveLineTrailerProvider, Editor, EditorMode};
+use gpui::{AnyWindowHandle, AppContext, ViewContext, WeakView, WindowContext};
+use settings::{Settings, SettingsStore};
+use std::{cell::RefCell, rc::Rc};
+use theme::ActiveTheme;
+use ui::prelude::*;
+use workspace::Workspace;
+
+pub fn init(cx: &mut AppContext) {
+ let editors: Rc<RefCell<HashMap<WeakView<Editor>, AnyWindowHandle>>> = Rc::default();
+
+ cx.observe_new_views({
+ let editors = editors.clone();
+ move |_: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+ let workspace_handle = cx.view().clone();
+ cx.subscribe(&workspace_handle, {
+ let editors = editors.clone();
+ move |_, _, event, cx| match event {
+ workspace::Event::ItemAdded { item } => {
+ if let Some(editor) = item.act_as::<Editor>(cx) {
+ if editor.read(cx).mode() != EditorMode::Full {
+ return;
+ }
+
+ cx.on_release({
+ let editor_handle = editor.downgrade();
+ let editors = editors.clone();
+ move |_, _, _| {
+ editors.borrow_mut().remove(&editor_handle);
+ }
+ })
+ .detach();
+ editors
+ .borrow_mut()
+ .insert(editor.downgrade(), cx.window_handle());
+
+ let show_hints = should_show_hints(cx);
+ editor.update(cx, |editor, cx| {
+ assign_active_line_trailer_provider(editor, show_hints, cx)
+ })
+ }
+ }
+ _ => {}
+ }
+ })
+ .detach();
+ }
+ })
+ .detach();
+
+ let mut show_hints = AssistantSettings::get_global(cx).show_hints;
+ cx.observe_global::<SettingsStore>(move |cx| {
+ let new_show_hints = should_show_hints(cx);
+ if new_show_hints != show_hints {
+ show_hints = new_show_hints;
+ for (editor, window) in editors.borrow().iter() {
+ _ = window.update(cx, |_window, cx| {
+ _ = editor.update(cx, |editor, cx| {
+ assign_active_line_trailer_provider(editor, show_hints, cx);
+ })
+ });
+ }
+ }
+ })
+ .detach();
+}
+
+struct AssistantHintsProvider;
+
+impl ActiveLineTrailerProvider for AssistantHintsProvider {
+ fn render_active_line_trailer(
+ &mut self,
+ style: &editor::EditorStyle,
+ focus_handle: &gpui::FocusHandle,
+ cx: &mut WindowContext,
+ ) -> Option<gpui::AnyElement> {
+ if !focus_handle.is_focused(cx) {
+ return None;
+ }
+
+ let chat_keybinding =
+ cx.keystroke_text_for_action_in(&assistant::ToggleFocus, focus_handle);
+ let generate_keybinding =
+ cx.keystroke_text_for_action_in(&zed_actions::InlineAssist::default(), focus_handle);
+
+ Some(
+ h_flex()
+ .id("inline-assistant-instructions")
+ .w_full()
+ .font_family(style.text.font().family)
+ .text_color(cx.theme().status().hint)
+ .line_height(style.text.line_height)
+ .child(format!(
+ "{chat_keybinding} to chat, {generate_keybinding} to generate"
+ ))
+ .into_any(),
+ )
+ }
+}
+
+fn assign_active_line_trailer_provider(
+ editor: &mut Editor,
+ show_hints: bool,
+ cx: &mut ViewContext<Editor>,
+) {
+ let provider = show_hints.then_some(AssistantHintsProvider);
+ editor.set_active_line_trailer_provider(provider, cx);
+}
+
+fn should_show_hints(cx: &AppContext) -> bool {
+ let assistant_settings = AssistantSettings::get_global(cx);
+ assistant_settings.enabled && assistant_settings.show_hints
+}
@@ -200,18 +200,28 @@ You must provide the model's Context Window in the `max_tokens` parameter, this
{
"assistant": {
"enabled": true,
+ "show_hints": true,
+ "button": true,
+ "dock": "right"
+ "default_width": 480,
"default_model": {
"provider": "zed.dev",
"model": "claude-3-5-sonnet"
},
"version": "2",
- "button": true,
- "default_width": 480,
- "dock": "right"
}
}
```
+| key | type | default | description |
+| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- |
+| enabled | boolean | true | Setting this to `false` will completely disable the assistant |
+| show_hints | boolean | true | Whether to to show hints in the editor explaining how to use assistant |
+| button | boolean | true | Show the assistant icon in the status bar |
+| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] |
+| default_height | string | null | The pixel height of the assistant panel when docked to the bottom |
+| default_width | string | null | The pixel width of the assistant panel when docked to the left or right |
+
#### Custom endpoints {#custom-endpoint}
You can use a custom API endpoint for different providers, as long as it's compatible with the providers API structure.
@@ -271,13 +281,3 @@ will generate two outputs for every assist. One with Claude 3.5 Sonnet, and one
}
}
```
-
-#### Common Panel Settings
-
-| key | type | default | description |
-| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- |
-| enabled | boolean | true | Setting this to `false` will completely disable the assistant |
-| button | boolean | true | Show the assistant icon in the status bar |
-| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] |
-| default_height | string | null | The pixel height of the assistant panel when docked to the bottom |
-| default_width | string | null | The pixel width of the assistant panel when docked to the left or right |
@@ -2327,15 +2327,18 @@ Run the `theme selector: toggle` action in the command palette to see a current
- Default:
```json
-"assistant": {
- "enabled": true,
- "button": true,
- "dock": "right",
- "default_width": 640,
- "default_height": 320,
- "provider": "openai",
- "version": "1",
-},
+{
+ "assistant": {
+ "enabled": true,
+ "button": true,
+ "dock": "right",
+ "default_width": 640,
+ "default_height": 320,
+ "provider": "openai",
+ "version": "1",
+ "show_hints": true
+ }
+}
```
## Outline Panel