Add slash commands for adding context into the assistant (#12102)

Tasks

* [x] remove old flaps and output when editing a slash command
* [x] the completing a command name that takes args, insert a space to
prepare for typing an arg
* [x] always trigger completions when  typing in a slash command
* [x] don't show line numbers
* [x] implement `prompt` command
* [x] `current-file` command
* [x] state gets corrupted on `duplicate line up` on a slash command
* [x] exclude slash command source from completion request

Next steps:
* show output token count in flap trailer
* add `/project` command that matches project ambient context
* delete ambient context

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Max Brunsfeld 2024-05-22 14:06:28 -07:00 committed by GitHub
parent d6e59bfae1
commit a73a3ef243
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1649 additions and 147 deletions

View file

@ -449,6 +449,9 @@ pub struct Editor {
mode: EditorMode,
show_breadcrumbs: bool,
show_gutter: bool,
show_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
show_wrap_guides: Option<bool>,
placeholder_text: Option<Arc<str>>,
highlight_order: usize,
@ -517,6 +520,9 @@ pub struct Editor {
pub struct EditorSnapshot {
pub mode: EditorMode,
show_gutter: bool,
show_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
render_git_blame_gutter: bool,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
@ -1646,6 +1652,9 @@ impl Editor {
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
show_line_numbers: None,
show_git_diff_gutter: None,
show_code_actions: None,
show_wrap_guides: None,
placeholder_text: None,
highlight_order: 0,
@ -1881,6 +1890,9 @@ impl Editor {
EditorSnapshot {
mode: self.mode,
show_gutter: self.show_gutter,
show_line_numbers: self.show_line_numbers,
show_git_diff_gutter: self.show_git_diff_gutter,
show_code_actions: self.show_code_actions,
render_git_blame_gutter: self.render_git_blame_gutter(cx),
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
scroll_anchor: self.scroll_manager.anchor(),
@ -1933,8 +1945,8 @@ impl Editor {
self.custom_context_menu = Some(Box::new(f))
}
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
self.completion_provider = Some(hub);
pub fn set_completion_provider(&mut self, provider: Box<dyn CompletionProvider>) {
self.completion_provider = Some(provider);
}
pub fn set_inline_completion_provider<T>(
@ -3280,22 +3292,41 @@ impl Editor {
trigger_in_words: bool,
cx: &mut ViewContext<Self>,
) {
if !EditorSettings::get_global(cx).show_completions_on_input {
return;
}
let selection = self.selections.newest_anchor();
if self
.buffer
.read(cx)
.is_completion_trigger(selection.head(), text, trigger_in_words, cx)
{
if self.is_completion_trigger(text, trigger_in_words, cx) {
self.show_completions(&ShowCompletions, cx);
} else {
self.hide_context_menu(cx);
}
}
fn is_completion_trigger(
&self,
text: &str,
trigger_in_words: bool,
cx: &mut ViewContext<Self>,
) -> bool {
let position = self.selections.newest_anchor().head();
let multibuffer = self.buffer.read(cx);
let Some(buffer) = position
.buffer_id
.and_then(|buffer_id| multibuffer.buffer(buffer_id).clone())
else {
return false;
};
if let Some(completion_provider) = &self.completion_provider {
completion_provider.is_completion_trigger(
&buffer,
position.text_anchor,
text,
trigger_in_words,
cx,
)
} else {
false
}
}
/// If any empty selections is touching the start of its innermost containing autoclose
/// region, expand it to select the brackets.
fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
@ -9613,8 +9644,27 @@ impl Editor {
cx.notify();
}
pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
self.show_wrap_guides = Some(show_gutter);
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
self.show_line_numbers = Some(show_line_numbers);
cx.notify();
}
pub fn set_show_git_diff_gutter(
&mut self,
show_git_diff_gutter: bool,
cx: &mut ViewContext<Self>,
) {
self.show_git_diff_gutter = Some(show_git_diff_gutter);
cx.notify();
}
pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut ViewContext<Self>) {
self.show_code_actions = Some(show_code_actions);
cx.notify();
}
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
self.show_wrap_guides = Some(show_wrap_guides);
cx.notify();
}
@ -10888,6 +10938,15 @@ pub trait CompletionProvider {
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>>;
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
text: &str,
trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool;
}
impl CompletionProvider for Model<Project> {
@ -10925,6 +10984,40 @@ impl CompletionProvider for Model<Project> {
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
})
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
text: &str,
trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool {
if !EditorSettings::get_global(cx).show_completions_on_input {
return false;
}
let mut chars = text.chars();
let char = if let Some(char) = chars.next() {
char
} else {
return false;
};
if chars.next().is_some() {
return false;
}
let buffer = buffer.read(cx);
let scope = buffer.snapshot().language_scope_at(position);
if trigger_in_words && char_kind(&scope, char) == CharKind::Word {
return true;
}
buffer
.completion_triggers()
.iter()
.any(|string| string == text)
}
}
fn inlay_hint_settings(
@ -11030,13 +11123,17 @@ impl EditorSnapshot {
}
let descent = cx.text_system().descent(font_id, font_size);
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {
matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
)
});
let gutter_settings = EditorSettings::get_global(cx).gutter;
let gutter_lines_enabled = gutter_settings.line_numbers;
let line_gutter_width = if gutter_lines_enabled {
let show_line_numbers = self
.show_line_numbers
.unwrap_or_else(|| gutter_settings.line_numbers);
let line_gutter_width = if show_line_numbers {
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
let min_width_for_number_on_gutter = em_width * 4.0;
max_line_number_width.max(min_width_for_number_on_gutter)
@ -11044,26 +11141,30 @@ impl EditorSnapshot {
0.0.into()
};
let show_code_actions = self
.show_code_actions
.unwrap_or_else(|| gutter_settings.code_actions);
let git_blame_entries_width = self
.render_git_blame_gutter
.then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
left_padding += if gutter_settings.code_actions {
left_padding += if show_code_actions {
em_width * 3.0
} else if show_git_gutter && gutter_lines_enabled {
} else if show_git_gutter && show_line_numbers {
em_width * 2.0
} else if show_git_gutter || gutter_lines_enabled {
} else if show_git_gutter || show_line_numbers {
em_width
} else {
px(0.)
};
let right_padding = if gutter_settings.folds && gutter_lines_enabled {
let right_padding = if gutter_settings.folds && show_line_numbers {
em_width * 4.0
} else if gutter_settings.folds {
em_width * 3.0
} else if gutter_lines_enabled {
} else if show_line_numbers {
em_width
} else {
px(0.)

View file

@ -1623,6 +1623,13 @@ impl EditorElement {
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<Option<ShapedLine>> {
let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
});
if !include_line_numbers {
return Vec::new();
}
let editor = self.editor.read(cx);
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
let newest = editor.selections.newest::<Point>(cx);
@ -1638,54 +1645,47 @@ impl EditorElement {
.head
});
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
let include_line_numbers =
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
let mut line_number = String::new();
let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
let relative_to = if is_relative {
Some(newest_selection_head.row())
} else {
None
};
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
for (ix, row) in buffer_rows.into_iter().enumerate() {
let display_row = DisplayRow(rows.start.0 + ix as u32);
let color = if active_rows.contains_key(&display_row) {
cx.theme().colors().editor_active_line_number
} else {
cx.theme().colors().editor_line_number
};
if let Some(multibuffer_row) = row {
if include_line_numbers {
line_number.clear();
let default_number = multibuffer_row.0 + 1;
let number = relative_rows
.get(&DisplayRow(ix as u32 + rows.start.0))
.unwrap_or(&default_number);
write!(&mut line_number, "{number}").unwrap();
let run = TextRun {
len: line_number.len(),
font: self.style.text.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_line = cx
.text_system()
.shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
shaped_line_numbers.push(Some(shaped_line));
}
} else {
shaped_line_numbers.push(None);
}
}
shaped_line_numbers
let mut line_number = String::new();
buffer_rows
.into_iter()
.enumerate()
.map(|(ix, multibuffer_row)| {
let multibuffer_row = multibuffer_row?;
let display_row = DisplayRow(rows.start.0 + ix as u32);
let color = if active_rows.contains_key(&display_row) {
cx.theme().colors().editor_active_line_number
} else {
cx.theme().colors().editor_line_number
};
line_number.clear();
let default_number = multibuffer_row.0 + 1;
let number = relative_rows
.get(&DisplayRow(ix as u32 + rows.start.0))
.unwrap_or(&default_number);
write!(&mut line_number, "{number}").unwrap();
let run = TextRun {
len: line_number.len(),
font: self.style.text.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_line = cx
.text_system()
.shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
Some(shaped_line)
})
.collect()
}
fn layout_gutter_fold_toggles(
@ -2513,10 +2513,16 @@ impl EditorElement {
}
}
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
let show_git_gutter = layout
.position_map
.snapshot
.show_git_diff_gutter
.unwrap_or_else(|| {
matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
)
});
if show_git_gutter {
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
}
@ -4281,7 +4287,11 @@ impl Element for EditorElement {
gutter_dimensions.width - gutter_dimensions.left_padding,
cx,
);
if gutter_settings.code_actions {
let show_code_actions = snapshot
.show_code_actions
.unwrap_or_else(|| gutter_settings.code_actions);
if show_code_actions {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(