Cargo.lock π
@@ -373,6 +373,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "similar",
"smol",
"strsim 0.11.1",
"strum",
Antonio Scandurra created
This pull request introduces a new diff mechanism that helps users
understand exactly which lines were changed by the LLM.
Release Notes:
- N/A
Cargo.lock | 1
Cargo.toml | 1
assets/icons/context.svg | 6
assets/icons/rotate_cw.svg | 1
assets/icons/stop.svg | 3
crates/assistant/Cargo.toml | 1
crates/assistant/src/assistant_panel.rs | 4
crates/assistant/src/inline_assistant.rs | 714 ++++++++++++++++++++-----
crates/editor/src/display_map.rs | 18
crates/editor/src/editor.rs | 60 ++
crates/editor/src/element.rs | 41 +
crates/language/Cargo.toml | 2
crates/language/src/buffer.rs | 4
crates/ui/src/components/icon.rs | 6
14 files changed, 707 insertions(+), 155 deletions(-)
@@ -373,6 +373,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "similar",
"smol",
"strsim 0.11.1",
"strum",
@@ -340,6 +340,7 @@ serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
+similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.88889 1H2.11111C1.49746 1 1 1.49746 1 2.11111V9.88889C1 10.5025 1.49746 11 2.11111 11H9.88889C10.5025 11 11 10.5025 11 9.88889V2.11111C11 1.49746 10.5025 1 9.88889 1Z" stroke="#C56757" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -47,6 +47,7 @@ semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
+similar.workspace = true
smol.workspace = true
strsim = "0.11"
strum.workspace = true
@@ -1222,6 +1222,10 @@ impl Context {
}
}
+ pub(crate) fn token_count(&self) -> Option<usize> {
+ self.token_count
+ }
+
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
let request = self.to_completion_request(cx);
self.pending_token_count = cx.spawn(|this, mut cx| {
@@ -11,8 +11,8 @@ use editor::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
},
scroll::{Autoscroll, AutoscrollStrategy},
- Anchor, Editor, EditorElement, EditorEvent, EditorStyle, GutterDimensions, MultiBuffer,
- MultiBufferSnapshot, ToOffset, ToPoint,
+ Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorStyle, ExcerptRange,
+ GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{
@@ -20,12 +20,18 @@ use gpui::{
Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View,
ViewContext, WeakView, WhiteSpace, WindowContext,
};
-use language::{Point, TransactionId};
+use language::{Buffer, Point, TransactionId};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use rope::Rope;
use settings::Settings;
-use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
+use similar::TextDiff;
+use std::{
+ cmp, future, mem,
+ ops::{Range, RangeInclusive},
+ sync::Arc,
+ time::Instant,
+};
use theme::ThemeSettings;
use ui::{prelude::*, Tooltip};
use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -108,37 +114,53 @@ impl InlineAssistant {
});
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
- let inline_assist_editor = cx.new_view(|cx| {
+ let prompt_editor = cx.new_view(|cx| {
InlineAssistEditor::new(
inline_assist_id,
gutter_dimensions.clone(),
self.prompt_history.clone(),
codegen.clone(),
+ workspace.clone(),
cx,
)
});
- let block_id = editor.update(cx, |editor, cx| {
- editor.change_selections(None, cx, |selections| {
- selections.select_anchor_ranges([selection.head()..selection.head()])
+ let (prompt_block_id, end_block_id) = editor.update(cx, |editor, cx| {
+ let start_anchor = snapshot.anchor_before(point_selection.start);
+ let end_anchor = snapshot.anchor_after(point_selection.end);
+ editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
+ selections.select_anchor_ranges([start_anchor..start_anchor])
});
- editor.insert_blocks(
- [BlockProperties {
- style: BlockStyle::Sticky,
- position: snapshot.anchor_before(Point::new(point_selection.head().row, 0)),
- height: inline_assist_editor.read(cx).height_in_lines,
- render: build_inline_assist_editor_renderer(
- &inline_assist_editor,
- gutter_dimensions,
- ),
- disposition: if selection.reversed {
- BlockDisposition::Above
- } else {
- BlockDisposition::Below
+ let block_ids = editor.insert_blocks(
+ [
+ BlockProperties {
+ style: BlockStyle::Sticky,
+ position: start_anchor,
+ height: prompt_editor.read(cx).height_in_lines,
+ render: build_inline_assist_editor_renderer(
+ &prompt_editor,
+ gutter_dimensions,
+ ),
+ disposition: BlockDisposition::Above,
+ },
+ BlockProperties {
+ style: BlockStyle::Sticky,
+ position: end_anchor,
+ height: 1,
+ render: Box::new(|cx| {
+ v_flex()
+ .h_full()
+ .w_full()
+ .border_t_1()
+ .border_color(cx.theme().status().info_border)
+ .into_any_element()
+ }),
+ disposition: BlockDisposition::Below,
},
- }],
+ ],
Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
cx,
- )[0]
+ );
+ (block_ids[0], block_ids[1])
});
self.pending_assists.insert(
@@ -146,17 +168,22 @@ impl InlineAssistant {
PendingInlineAssist {
include_context,
editor: editor.downgrade(),
- inline_assist_editor: Some((block_id, inline_assist_editor.clone())),
+ editor_decorations: Some(PendingInlineAssistDecorations {
+ prompt_block_id,
+ prompt_editor: prompt_editor.clone(),
+ removed_line_block_ids: HashSet::default(),
+ end_block_id,
+ }),
codegen: codegen.clone(),
workspace,
_subscriptions: vec![
- cx.subscribe(&inline_assist_editor, |inline_assist_editor, event, cx| {
+ cx.subscribe(&prompt_editor, |inline_assist_editor, event, cx| {
InlineAssistant::update_global(cx, |this, cx| {
this.handle_inline_assistant_event(inline_assist_editor, event, cx)
})
}),
cx.subscribe(editor, {
- let inline_assist_editor = inline_assist_editor.downgrade();
+ let inline_assist_editor = prompt_editor.downgrade();
move |editor, event, cx| {
if let Some(inline_assist_editor) = inline_assist_editor.upgrade() {
if let EditorEvent::SelectionsChanged { local } = event {
@@ -176,7 +203,8 @@ impl InlineAssistant {
move |_, cx| {
if let Some(editor) = editor.upgrade() {
InlineAssistant::update_global(cx, |this, cx| {
- this.update_highlights_for_editor(&editor, cx);
+ this.update_editor_highlights(&editor, cx);
+ this.update_editor_blocks(&editor, inline_assist_id, cx);
})
}
}
@@ -195,17 +223,15 @@ impl InlineAssistant {
return;
};
- let error = codegen
- .read(cx)
- .error()
- .map(|error| format!("Inline assistant error: {}", error));
- if let Some(error) = error {
- if pending_assist.inline_assist_editor.is_none() {
+ if let CodegenStatus::Error(error) = &codegen.read(cx).status {
+ if pending_assist.editor_decorations.is_none() {
if let Some(workspace) = pending_assist
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade())
{
+ let error =
+ format!("Inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| {
struct InlineAssistantError;
@@ -218,10 +244,10 @@ impl InlineAssistant {
workspace.show_toast(Toast::new(id, error), cx);
})
}
-
- this.finish_inline_assist(inline_assist_id, false, cx);
}
- } else {
+ }
+
+ if pending_assist.editor_decorations.is_none() {
this.finish_inline_assist(inline_assist_id, false, cx);
}
}
@@ -239,7 +265,7 @@ impl InlineAssistant {
})
.assist_ids
.push(inline_assist_id);
- self.update_highlights_for_editor(editor, cx);
+ self.update_editor_highlights(editor, cx);
}
fn handle_inline_assistant_event(
@@ -250,14 +276,20 @@ impl InlineAssistant {
) {
let assist_id = inline_assist_editor.read(cx).id;
match event {
- InlineAssistEditorEvent::Confirmed { prompt } => {
- self.confirm_inline_assist(assist_id, prompt, cx);
+ InlineAssistEditorEvent::Started => {
+ self.start_inline_assist(assist_id, cx);
+ }
+ InlineAssistEditorEvent::Stopped => {
+ self.stop_inline_assist(assist_id, cx);
+ }
+ InlineAssistEditorEvent::Confirmed => {
+ self.finish_inline_assist(assist_id, false, cx);
}
InlineAssistEditorEvent::Canceled => {
self.finish_inline_assist(assist_id, true, cx);
}
InlineAssistEditorEvent::Dismissed => {
- self.hide_inline_assist(assist_id, cx);
+ self.hide_inline_assist_decorations(assist_id, cx);
}
InlineAssistEditorEvent::Resized { height_in_lines } => {
self.resize_inline_assist(assist_id, *height_in_lines, cx);
@@ -287,7 +319,7 @@ impl InlineAssistant {
undo: bool,
cx: &mut WindowContext,
) {
- self.hide_inline_assist(assist_id, cx);
+ self.hide_inline_assist_decorations(assist_id, cx);
if let Some(pending_assist) = self.pending_assists.remove(&assist_id) {
if let hash_map::Entry::Occupied(mut entry) = self
@@ -301,7 +333,7 @@ impl InlineAssistant {
}
if let Some(editor) = pending_assist.editor.upgrade() {
- self.update_highlights_for_editor(&editor, cx);
+ self.update_editor_highlights(&editor, cx);
if undo {
pending_assist
@@ -312,21 +344,37 @@ impl InlineAssistant {
}
}
- fn hide_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
- if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) {
- if let Some(editor) = pending_assist.editor.upgrade() {
- if let Some((block_id, inline_assist_editor)) =
- pending_assist.inline_assist_editor.take()
- {
- editor.update(cx, |editor, cx| {
- editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
- if inline_assist_editor.focus_handle(cx).contains_focused(cx) {
- editor.focus(cx);
- }
- });
- }
+ fn hide_inline_assist_decorations(
+ &mut self,
+ assist_id: InlineAssistId,
+ cx: &mut WindowContext,
+ ) -> bool {
+ let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
+ return false;
+ };
+ let Some(editor) = pending_assist.editor.upgrade() else {
+ return false;
+ };
+ let Some(decorations) = pending_assist.editor_decorations.take() else {
+ return false;
+ };
+
+ editor.update(cx, |editor, cx| {
+ let mut to_remove = decorations.removed_line_block_ids;
+ to_remove.insert(decorations.prompt_block_id);
+ to_remove.insert(decorations.end_block_id);
+ editor.remove_blocks(to_remove, None, cx);
+ if decorations
+ .prompt_editor
+ .focus_handle(cx)
+ .contains_focused(cx)
+ {
+ editor.focus(cx);
}
- }
+ });
+
+ self.update_editor_highlights(&editor, cx);
+ true
}
fn resize_inline_assist(
@@ -337,17 +385,16 @@ impl InlineAssistant {
) {
if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) {
if let Some(editor) = pending_assist.editor.upgrade() {
- if let Some((block_id, inline_assist_editor)) =
- pending_assist.inline_assist_editor.as_ref()
- {
- let gutter_dimensions = inline_assist_editor.read(cx).gutter_dimensions.clone();
+ if let Some(decorations) = pending_assist.editor_decorations.as_ref() {
+ let gutter_dimensions =
+ decorations.prompt_editor.read(cx).gutter_dimensions.clone();
let mut new_blocks = HashMap::default();
new_blocks.insert(
- *block_id,
+ decorations.prompt_block_id,
(
Some(height_in_lines),
build_inline_assist_editor_renderer(
- inline_assist_editor,
+ &decorations.prompt_editor,
gutter_dimensions,
),
),
@@ -362,12 +409,7 @@ impl InlineAssistant {
}
}
- fn confirm_inline_assist(
- &mut self,
- assist_id: InlineAssistId,
- user_prompt: &str,
- cx: &mut WindowContext,
- ) {
+ fn start_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
let pending_assist = if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id)
{
pending_assist
@@ -375,6 +417,18 @@ impl InlineAssistant {
return;
};
+ pending_assist
+ .codegen
+ .update(cx, |codegen, cx| codegen.undo(cx));
+
+ let Some(user_prompt) = pending_assist
+ .editor_decorations
+ .as_ref()
+ .map(|decorations| decorations.prompt_editor.read(cx).prompt(cx))
+ else {
+ return;
+ };
+
let context = if pending_assist.include_context {
pending_assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?.read(cx);
@@ -404,8 +458,8 @@ impl InlineAssistant {
)
});
- self.prompt_history.retain(|prompt| prompt != user_prompt);
- self.prompt_history.push_back(user_prompt.into());
+ self.prompt_history.retain(|prompt| *prompt != user_prompt);
+ self.prompt_history.push_back(user_prompt.clone());
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
self.prompt_history.pop_front();
}
@@ -453,8 +507,6 @@ impl InlineAssistant {
1.0
};
- let user_prompt = user_prompt.to_string();
-
let prompt = cx.background_executor().spawn(async move {
let language_name = language_name.as_deref();
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
@@ -488,9 +540,24 @@ impl InlineAssistant {
.detach_and_log_err(cx);
}
- fn update_highlights_for_editor(&self, editor: &View<Editor>, cx: &mut WindowContext) {
- let mut background_ranges = Vec::new();
+ fn stop_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+ let pending_assist = if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id)
+ {
+ pending_assist
+ } else {
+ return;
+ };
+
+ pending_assist
+ .codegen
+ .update(cx, |codegen, cx| codegen.stop(cx));
+ }
+
+ fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
+ let mut gutter_pending_ranges = Vec::new();
+ let mut gutter_transformed_ranges = Vec::new();
let mut foreground_ranges = Vec::new();
+ let mut inserted_row_ranges = Vec::new();
let empty_inline_assist_ids = Vec::new();
let inline_assist_ids = self
.pending_assist_ids_by_editor
@@ -502,23 +569,47 @@ impl InlineAssistant {
for inline_assist_id in inline_assist_ids {
if let Some(pending_assist) = self.pending_assists.get(inline_assist_id) {
let codegen = pending_assist.codegen.read(cx);
- background_ranges.push(codegen.range());
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
+
+ if codegen.edit_position != codegen.range().end {
+ gutter_pending_ranges.push(codegen.edit_position..codegen.range().end);
+ }
+
+ if codegen.range().start != codegen.edit_position {
+ gutter_transformed_ranges.push(codegen.range().start..codegen.edit_position);
+ }
+
+ if pending_assist.editor_decorations.is_some() {
+ inserted_row_ranges.extend(codegen.diff.inserted_row_ranges.iter().cloned());
+ }
}
}
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
- merge_ranges(&mut background_ranges, &snapshot);
merge_ranges(&mut foreground_ranges, &snapshot);
+ merge_ranges(&mut gutter_pending_ranges, &snapshot);
+ merge_ranges(&mut gutter_transformed_ranges, &snapshot);
editor.update(cx, |editor, cx| {
- if background_ranges.is_empty() {
- editor.clear_background_highlights::<PendingInlineAssist>(cx);
+ enum GutterPendingRange {}
+ if gutter_pending_ranges.is_empty() {
+ editor.clear_gutter_highlights::<GutterPendingRange>(cx);
} else {
- editor.highlight_background::<PendingInlineAssist>(
- &background_ranges,
- |theme| theme.editor_active_line_background, // TODO use the appropriate color
+ editor.highlight_gutter::<GutterPendingRange>(
+ &gutter_pending_ranges,
+ |cx| cx.theme().status().info_background,
cx,
- );
+ )
+ }
+
+ enum GutterTransformedRange {}
+ if gutter_transformed_ranges.is_empty() {
+ editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
+ } else {
+ editor.highlight_gutter::<GutterTransformedRange>(
+ &gutter_transformed_ranges,
+ |cx| cx.theme().status().info,
+ cx,
+ )
}
if foreground_ranges.is_empty() {
@@ -533,8 +624,108 @@ impl InlineAssistant {
cx,
);
}
+
+ editor.clear_row_highlights::<PendingInlineAssist>();
+ for row_range in inserted_row_ranges {
+ editor.highlight_rows::<PendingInlineAssist>(
+ row_range,
+ Some(cx.theme().status().info_background),
+ false,
+ cx,
+ );
+ }
});
}
+
+ fn update_editor_blocks(
+ &mut self,
+ editor: &View<Editor>,
+ assist_id: InlineAssistId,
+ cx: &mut WindowContext,
+ ) {
+ let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
+ return;
+ };
+ let Some(decorations) = pending_assist.editor_decorations.as_mut() else {
+ return;
+ };
+
+ let codegen = pending_assist.codegen.read(cx);
+ let old_snapshot = codegen.snapshot.clone();
+ let old_buffer = codegen.old_buffer.clone();
+ let deleted_row_ranges = codegen.diff.deleted_row_ranges.clone();
+
+ editor.update(cx, |editor, cx| {
+ let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
+ editor.remove_blocks(old_blocks, None, cx);
+
+ let mut new_blocks = Vec::new();
+ for (new_row, old_row_range) in deleted_row_ranges {
+ let (_, buffer_start) = old_snapshot
+ .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
+ .unwrap();
+ let (_, buffer_end) = old_snapshot
+ .point_to_buffer_offset(Point::new(
+ *old_row_range.end(),
+ old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
+ ))
+ .unwrap();
+
+ let deleted_lines_editor = cx.new_view(|cx| {
+ let multi_buffer = cx.new_model(|_| {
+ MultiBuffer::without_headers(0, language::Capability::ReadOnly)
+ });
+ multi_buffer.update(cx, |multi_buffer, cx| {
+ multi_buffer.push_excerpts(
+ old_buffer.clone(),
+ Some(ExcerptRange {
+ context: buffer_start..buffer_end,
+ primary: None,
+ }),
+ cx,
+ );
+ });
+
+ enum DeletedLines {}
+ let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
+ editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
+ editor.set_show_wrap_guides(false, cx);
+ editor.set_show_gutter(false, cx);
+ editor.scroll_manager.set_forbid_vertical_scroll(true);
+ editor.set_read_only(true);
+ editor.highlight_rows::<DeletedLines>(
+ Anchor::min()..=Anchor::max(),
+ Some(cx.theme().status().deleted_background),
+ false,
+ cx,
+ );
+ editor
+ });
+
+ let height = deleted_lines_editor
+ .update(cx, |editor, cx| editor.max_point(cx).row().0 as u8 + 1);
+ new_blocks.push(BlockProperties {
+ position: new_row,
+ height,
+ style: BlockStyle::Flex,
+ render: Box::new(move |cx| {
+ div()
+ .bg(cx.theme().status().deleted_background)
+ .size_full()
+ .pl(cx.gutter_dimensions.full_width())
+ .child(deleted_lines_editor.clone())
+ .into_any_element()
+ }),
+ disposition: BlockDisposition::Above,
+ });
+ }
+
+ decorations.removed_line_block_ids = editor
+ .insert_blocks(new_blocks, None, cx)
+ .into_iter()
+ .collect();
+ })
+ }
}
fn build_inline_assist_editor_renderer(
@@ -560,7 +751,9 @@ impl InlineAssistId {
}
enum InlineAssistEditorEvent {
- Confirmed { prompt: String },
+ Started,
+ Stopped,
+ Confirmed,
Canceled,
Dismissed,
Resized { height_in_lines: u8 },
@@ -570,12 +763,13 @@ struct InlineAssistEditor {
id: InlineAssistId,
height_in_lines: u8,
prompt_editor: View<Editor>,
- confirmed: bool,
+ edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
pending_prompt: String,
codegen: Model<Codegen>,
+ workspace: Option<WeakView<Workspace>>,
_subscriptions: Vec<Subscription>,
}
@@ -584,39 +778,170 @@ impl EventEmitter<InlineAssistEditorEvent> for InlineAssistEditor {}
impl Render for InlineAssistEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let gutter_dimensions = *self.gutter_dimensions.lock();
- let icon_size = IconSize::default();
- h_flex()
- .w_full()
- .py_1p5()
- .border_y_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().editor_background)
- .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))
- .child(
- h_flex()
- .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
- .pr(gutter_dimensions.fold_area_width())
- .justify_end()
- .children(if let Some(error) = self.codegen.read(cx).error() {
- 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(icon_size)
- .color(Color::Error),
- ),
- )
+
+ let buttons = match &self.codegen.read(cx).status {
+ CodegenStatus::Idle => {
+ vec![
+ IconButton::new("start", IconName::Sparkle)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::None)
+ .icon_size(IconSize::XSmall)
+ .tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx))
+ .on_click(
+ cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Started)),
+ ),
+ IconButton::new("cancel", IconName::Close)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::None)
+ .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
+ .on_click(
+ cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)),
+ ),
+ ]
+ }
+ CodegenStatus::Pending => {
+ vec![
+ IconButton::new("stop", IconName::Stop)
+ .icon_color(Color::Error)
+ .size(ButtonSize::None)
+ .icon_size(IconSize::XSmall)
+ .tooltip(|cx| {
+ Tooltip::with_meta(
+ "Interrupt Transformation",
+ Some(&menu::Cancel),
+ "Changes won't be discarded",
+ cx,
+ )
+ })
+ .on_click(
+ cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Stopped)),
+ ),
+ IconButton::new("cancel", IconName::Close)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::None)
+ .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
+ .on_click(
+ cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)),
+ ),
+ ]
+ }
+ CodegenStatus::Error(_) | CodegenStatus::Done => {
+ vec![
+ if self.edited_since_done {
+ IconButton::new("restart", IconName::RotateCw)
+ .icon_color(Color::Info)
+ .icon_size(IconSize::XSmall)
+ .size(ButtonSize::None)
+ .tooltip(|cx| {
+ Tooltip::with_meta(
+ "Restart Transformation",
+ Some(&menu::Confirm),
+ "Changes will be discarded",
+ cx,
+ )
+ })
+ .on_click(cx.listener(|_, _, cx| {
+ cx.emit(InlineAssistEditorEvent::Started);
+ }))
} else {
- None
- }),
- )
- .child(div().flex_1().child(self.render_prompt_editor(cx)))
+ IconButton::new("confirm", IconName::Check)
+ .icon_color(Color::Info)
+ .size(ButtonSize::None)
+ .tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx))
+ .on_click(cx.listener(|_, _, cx| {
+ cx.emit(InlineAssistEditorEvent::Confirmed);
+ }))
+ },
+ IconButton::new("cancel", IconName::Close)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::None)
+ .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
+ .on_click(
+ cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)),
+ ),
+ ]
+ }
+ };
+
+ v_flex().h_full().w_full().justify_end().child(
+ h_flex()
+ .bg(cx.theme().colors().editor_background)
+ .border_y_1()
+ .border_color(cx.theme().status().info_border)
+ .py_1p5()
+ .w_full()
+ .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))
+ .child(
+ h_flex()
+ .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
+ // .pr(gutter_dimensions.fold_area_width())
+ .justify_center()
+ .gap_2()
+ .children(self.workspace.clone().map(|workspace| {
+ IconButton::new("context", IconName::Context)
+ .size(ButtonSize::None)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .on_click({
+ let workspace = workspace.clone();
+ cx.listener(move |_, _, cx| {
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.focus_panel::<AssistantPanel>(cx);
+ })
+ .ok();
+ })
+ })
+ .tooltip(move |cx| {
+ let token_count = workspace.upgrade().and_then(|workspace| {
+ let panel =
+ workspace.read(cx).panel::<AssistantPanel>(cx)?;
+ let context = panel.read(cx).active_context(cx)?;
+ context.read(cx).token_count()
+ });
+ if let Some(token_count) = token_count {
+ Tooltip::with_meta(
+ format!(
+ "{} Additional Context Tokens from Assistant",
+ token_count
+ ),
+ Some(&crate::ToggleFocus),
+ "Click to openβ¦",
+ cx,
+ )
+ } else {
+ Tooltip::for_action(
+ "Toggle Assistant Panel",
+ &crate::ToggleFocus,
+ 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_2().pr_4().children(buttons)),
+ )
}
}
@@ -635,16 +960,13 @@ impl InlineAssistEditor {
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
codegen: Model<Codegen>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
- let placeholder = match codegen.read(cx).kind() {
- CodegenKind::Transform { .. } => "Enter transformation promptβ¦",
- CodegenKind::Generate { .. } => "Enter generation promptβ¦",
- };
- editor.set_placeholder_text(placeholder, cx);
+ editor.set_placeholder_text("Add a promptβ¦", cx);
editor
});
cx.focus_view(&prompt_editor);
@@ -659,21 +981,26 @@ impl InlineAssistEditor {
id,
height_in_lines: 1,
prompt_editor,
- confirmed: false,
+ edited_since_done: false,
gutter_dimensions,
prompt_history,
prompt_history_ix: None,
pending_prompt: String::new(),
codegen,
+ workspace,
_subscriptions: subscriptions,
};
this.count_lines(cx);
this
}
+ fn prompt(&self, cx: &AppContext) -> String {
+ self.prompt_editor.read(cx).text(cx)
+ }
+
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
let height_in_lines = cmp::max(
- 2, // Make the editor at least two lines tall, to account for padding.
+ 2, // Make the editor at least two lines tall, to account for padding and buttons.
cmp::min(
self.prompt_editor
.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
@@ -699,12 +1026,25 @@ impl InlineAssistEditor {
) {
match event {
EditorEvent::Edited => {
+ self.edited_since_done = true;
self.pending_prompt = self.prompt_editor.read(cx).text(cx);
cx.notify();
}
EditorEvent::Blurred => {
- if !self.confirmed {
- cx.emit(InlineAssistEditorEvent::Canceled);
+ if let CodegenStatus::Idle = &self.codegen.read(cx).status {
+ let assistant_panel_is_focused = self
+ .workspace
+ .as_ref()
+ .and_then(|workspace| {
+ let panel =
+ workspace.upgrade()?.read(cx).panel::<AssistantPanel>(cx)?;
+ Some(panel.focus_handle(cx).contains_focused(cx))
+ })
+ .unwrap_or(false);
+
+ if !assistant_panel_is_focused {
+ cx.emit(InlineAssistEditorEvent::Canceled);
+ }
}
}
_ => {}
@@ -712,35 +1052,49 @@ impl InlineAssistEditor {
}
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
- let is_read_only = !self.codegen.read(cx).idle();
- self.prompt_editor.update(cx, |editor, cx| {
- let was_read_only = editor.read_only(cx);
- if was_read_only != is_read_only {
- if is_read_only {
- editor.set_read_only(true);
- } else {
- self.confirmed = false;
- editor.set_read_only(false);
- }
+ match &self.codegen.read(cx).status {
+ CodegenStatus::Idle => {
+ self.prompt_editor
+ .update(cx, |editor, _| editor.set_read_only(false));
}
- });
- cx.notify();
+ CodegenStatus::Pending => {
+ self.prompt_editor
+ .update(cx, |editor, _| editor.set_read_only(true));
+ }
+ CodegenStatus::Done | CodegenStatus::Error(_) => {
+ self.edited_since_done = false;
+ self.prompt_editor
+ .update(cx, |editor, _| editor.set_read_only(false));
+ }
+ }
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(InlineAssistEditorEvent::Canceled);
+ match &self.codegen.read(cx).status {
+ CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
+ cx.emit(InlineAssistEditorEvent::Canceled);
+ }
+ CodegenStatus::Pending => {
+ cx.emit(InlineAssistEditorEvent::Stopped);
+ }
+ }
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
- if self.confirmed {
- cx.emit(InlineAssistEditorEvent::Dismissed);
- } else {
- let prompt = self.prompt_editor.read(cx).text(cx);
- self.prompt_editor
- .update(cx, |editor, _cx| editor.set_read_only(true));
- cx.emit(InlineAssistEditorEvent::Confirmed { prompt });
- self.confirmed = true;
- cx.notify();
+ match &self.codegen.read(cx).status {
+ CodegenStatus::Idle => {
+ cx.emit(InlineAssistEditorEvent::Started);
+ }
+ CodegenStatus::Pending => {
+ cx.emit(InlineAssistEditorEvent::Dismissed);
+ }
+ CodegenStatus::Done | CodegenStatus::Error(_) => {
+ if self.edited_since_done {
+ cx.emit(InlineAssistEditorEvent::Started);
+ } else {
+ cx.emit(InlineAssistEditorEvent::Confirmed);
+ }
+ }
}
}
@@ -814,13 +1168,20 @@ impl InlineAssistEditor {
struct PendingInlineAssist {
editor: WeakView<Editor>,
- inline_assist_editor: Option<(BlockId, View<InlineAssistEditor>)>,
+ editor_decorations: Option<PendingInlineAssistDecorations>,
codegen: Model<Codegen>,
_subscriptions: Vec<Subscription>,
workspace: Option<WeakView<Workspace>>,
include_context: bool,
}
+struct PendingInlineAssistDecorations {
+ prompt_block_id: BlockId,
+ prompt_editor: View<InlineAssistEditor>,
+ removed_line_block_ids: HashSet<BlockId>,
+ end_block_id: BlockId,
+}
+
#[derive(Debug)]
pub enum CodegenEvent {
Finished,
@@ -833,19 +1194,45 @@ pub enum CodegenKind {
Generate { position: Anchor },
}
+impl CodegenKind {
+ fn range(&self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
+ match self {
+ CodegenKind::Transform { range } => range.clone(),
+ CodegenKind::Generate { position } => position.bias_left(snapshot)..*position,
+ }
+ }
+}
+
pub struct Codegen {
buffer: Model<MultiBuffer>,
+ old_buffer: Model<Buffer>,
snapshot: MultiBufferSnapshot,
kind: CodegenKind,
+ edit_position: Anchor,
last_equal_ranges: Vec<Range<Anchor>>,
transaction_id: Option<TransactionId>,
- error: Option<anyhow::Error>,
+ status: CodegenStatus,
generation: Task<()>,
- idle: bool,
+ diff: Diff,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
}
+enum CodegenStatus {
+ Idle,
+ Pending,
+ Done,
+ Error(anyhow::Error),
+}
+
+#[derive(Default)]
+struct Diff {
+ task: Option<Task<()>>,
+ should_update: bool,
+ deleted_row_ranges: Vec<(Anchor, RangeInclusive<u32>)>,
+ inserted_row_ranges: Vec<RangeInclusive<Anchor>>,
+}
+
impl EventEmitter<CodegenEvent> for Codegen {}
impl Codegen {
@@ -856,15 +1243,38 @@ impl Codegen {
cx: &mut ModelContext<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
+
+ let (old_buffer, _, _) = buffer
+ .read(cx)
+ .range_to_buffer_ranges(kind.range(&snapshot), cx)
+ .pop()
+ .unwrap();
+ let old_buffer = cx.new_model(|cx| {
+ let old_buffer = old_buffer.read(cx);
+ let text = old_buffer.as_rope().clone();
+ let line_ending = old_buffer.line_ending();
+ let language = old_buffer.language().cloned();
+ let language_registry = old_buffer.language_registry();
+
+ let mut buffer = Buffer::local_normalized(text, line_ending, cx);
+ buffer.set_language(language, cx);
+ if let Some(language_registry) = language_registry {
+ buffer.set_language_registry(language_registry)
+ }
+ buffer
+ });
+
Self {
buffer: buffer.clone(),
+ old_buffer,
+ edit_position: kind.range(&snapshot).start,
snapshot,
kind,
last_equal_ranges: Default::default(),
transaction_id: Default::default(),
- error: Default::default(),
- idle: true,
+ status: CodegenStatus::Idle,
generation: Task::ready(()),
+ diff: Diff::default(),
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
}
@@ -52,8 +52,14 @@ use multi_buffer::{
ToOffset, ToPoint,
};
use serde::Deserialize;
-use std::ops::Add;
-use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+use std::{
+ any::TypeId,
+ borrow::Cow,
+ fmt::Debug,
+ num::NonZeroU32,
+ ops::{Add, Range, Sub},
+ sync::Arc,
+};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
@@ -1027,6 +1033,14 @@ impl Add for DisplayRow {
}
}
+impl Sub for DisplayRow {
+ type Output = Self;
+
+ fn sub(self, other: Self) -> Self::Output {
+ DisplayRow(self.0 - other.0)
+ }
+}
+
impl DisplayPoint {
pub fn new(row: DisplayRow, column: u32) -> Self {
Self(BlockPoint(Point::new(row.0, column)))
@@ -376,6 +376,7 @@ type CompletionId = usize;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
+type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range<Anchor>]>);
struct ScrollbarMarkerState {
scrollbar_size: Size<Pixels>,
@@ -464,6 +465,7 @@ pub struct Editor {
highlight_order: usize,
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
background_highlights: TreeMap<TypeId, BackgroundHighlight>,
+ gutter_highlights: TreeMap<TypeId, GutterHighlight>,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
@@ -1752,6 +1754,7 @@ impl Editor {
highlight_order: 0,
highlighted_rows: HashMap::default(),
background_highlights: Default::default(),
+ gutter_highlights: TreeMap::default(),
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
@@ -10263,6 +10266,25 @@ impl Editor {
Some(text_highlights)
}
+ pub fn highlight_gutter<T: 'static>(
+ &mut self,
+ ranges: &[Range<Anchor>],
+ color_fetcher: fn(&AppContext) -> Hsla,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.gutter_highlights
+ .insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
+ cx.notify();
+ }
+
+ pub fn clear_gutter_highlights<T: 'static>(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<GutterHighlight> {
+ cx.notify();
+ self.gutter_highlights.remove(&TypeId::of::<T>())
+ }
+
#[cfg(feature = "test-support")]
pub fn all_text_background_highlights(
&mut self,
@@ -10452,6 +10474,44 @@ impl Editor {
results
}
+ pub fn gutter_highlights_in_range(
+ &self,
+ search_range: Range<Anchor>,
+ display_snapshot: &DisplaySnapshot,
+ cx: &AppContext,
+ ) -> Vec<(Range<DisplayPoint>, Hsla)> {
+ let mut results = Vec::new();
+ for (color_fetcher, ranges) in self.gutter_highlights.values() {
+ let color = color_fetcher(cx);
+ let start_ix = match ranges.binary_search_by(|probe| {
+ let cmp = probe
+ .end
+ .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
+ if cmp.is_gt() {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ }) {
+ Ok(i) | Err(i) => i,
+ };
+ for range in &ranges[start_ix..] {
+ if range
+ .start
+ .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+ .is_ge()
+ {
+ break;
+ }
+
+ let start = range.start.to_display_point(&display_snapshot);
+ let end = range.end.to_display_point(&display_snapshot);
+ results.push((start..end, color))
+ }
+ }
+ results
+ }
+
/// Get the text ranges corresponding to the redaction query
pub fn redacted_ranges(
&self,
@@ -2837,6 +2837,8 @@ impl EditorElement {
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
}
+ self.paint_gutter_highlights(layout, cx);
+
if layout.blamed_display_rows.is_some() {
self.paint_blamed_display_rows(layout, cx);
}
@@ -3006,6 +3008,37 @@ impl EditorElement {
}
}
+ fn paint_gutter_highlights(&self, layout: &EditorLayout, cx: &mut WindowContext) {
+ let highlight_width = 0.275 * layout.position_map.line_height;
+ let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
+ cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
+ for (range, color) in &layout.highlighted_gutter_ranges {
+ let start_row = if range.start.row() < layout.visible_display_row_range.start {
+ layout.visible_display_row_range.start - DisplayRow(1)
+ } else {
+ range.start.row()
+ };
+ let end_row = if range.end.row() > layout.visible_display_row_range.end {
+ layout.visible_display_row_range.end + DisplayRow(1)
+ } else {
+ range.end.row()
+ };
+
+ let start_y = layout.gutter_hitbox.top()
+ + start_row.0 as f32 * layout.position_map.line_height
+ - layout.position_map.scroll_pixel_position.y;
+ let end_y = layout.gutter_hitbox.top()
+ + (end_row.0 + 1) as f32 * layout.position_map.line_height
+ - layout.position_map.scroll_pixel_position.y;
+ let bounds = Bounds::from_corners(
+ point(layout.gutter_hitbox.left(), start_y),
+ point(layout.gutter_hitbox.left() + highlight_width, end_y),
+ );
+ cx.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
+ }
+ });
+ }
+
fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
return;
@@ -4631,6 +4664,12 @@ impl Element for EditorElement {
&snapshot.display_snapshot,
cx.theme().colors(),
);
+ let highlighted_gutter_ranges =
+ self.editor.read(cx).gutter_highlights_in_range(
+ start_anchor..end_anchor,
+ &snapshot.display_snapshot,
+ cx,
+ );
let redacted_ranges = self.editor.read(cx).redacted_ranges(
start_anchor..end_anchor,
@@ -4991,6 +5030,7 @@ impl Element for EditorElement {
active_rows,
highlighted_rows,
highlighted_ranges,
+ highlighted_gutter_ranges,
redacted_ranges,
line_elements,
line_numbers,
@@ -5121,6 +5161,7 @@ pub struct EditorLayout {
inline_blame: Option<AnyElement>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
+ highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
redacted_ranges: Vec<Range<DisplayPoint>>,
cursors: Vec<(DisplayPoint, Hsla)>,
visible_cursors: Vec<CursorLayout>,
@@ -49,7 +49,7 @@ schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
-similar = "1.3"
+similar.workspace = true
smallvec.workspace = true
smol.workspace = true
sum_tree.workspace = true
@@ -797,6 +797,10 @@ impl Buffer {
.set_language_registry(language_registry);
}
+ pub fn language_registry(&self) -> Option<Arc<LanguageRegistry>> {
+ self.syntax_map.lock().language_registry()
+ }
+
/// Assign the buffer a new [Capability].
pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext<Self>) {
self.capability = capability;
@@ -106,6 +106,7 @@ pub enum IconName {
Code,
Collab,
Command,
+ Context,
Control,
Copilot,
CopilotDisabled,
@@ -170,6 +171,7 @@ pub enum IconName {
Rerun,
Return,
Reveal,
+ RotateCw,
Save,
Screen,
SelectAll,
@@ -186,6 +188,7 @@ pub enum IconName {
Split,
Star,
StarFilled,
+ Stop,
Strikethrough,
Supermaven,
SupermavenDisabled,
@@ -233,6 +236,7 @@ impl IconName {
IconName::Code => "icons/code.svg",
IconName::Collab => "icons/user_group_16.svg",
IconName::Command => "icons/command.svg",
+ IconName::Context => "icons/context.svg",
IconName::Control => "icons/control.svg",
IconName::Copilot => "icons/copilot.svg",
IconName::CopilotDisabled => "icons/copilot_disabled.svg",
@@ -297,6 +301,7 @@ impl IconName {
IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
IconName::Rerun => "icons/rerun.svg",
IconName::Return => "icons/return.svg",
+ IconName::RotateCw => "icons/rotate_cw.svg",
IconName::Save => "icons/save.svg",
IconName::Screen => "icons/desktop.svg",
IconName::SelectAll => "icons/select_all.svg",
@@ -313,6 +318,7 @@ impl IconName {
IconName::Split => "icons/split.svg",
IconName::Star => "icons/star.svg",
IconName::StarFilled => "icons/star_filled.svg",
+ IconName::Stop => "icons/stop.svg",
IconName::Strikethrough => "icons/strikethrough.svg",
IconName::Supermaven => "icons/supermaven.svg",
IconName::SupermavenDisabled => "icons/supermaven_disabled.svg",