diff --git a/Cargo.lock b/Cargo.lock
index ee1b95a986..339b741d84 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -373,6 +373,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "similar",
"smol",
"strsim 0.11.1",
"strum",
diff --git a/Cargo.toml b/Cargo.toml
index 4090567600..48b22cc18a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
diff --git a/assets/icons/context.svg b/assets/icons/context.svg
new file mode 100644
index 0000000000..837b3aadd9
--- /dev/null
+++ b/assets/icons/context.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/rotate_cw.svg b/assets/icons/rotate_cw.svg
new file mode 100644
index 0000000000..019367745f
--- /dev/null
+++ b/assets/icons/rotate_cw.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/stop.svg b/assets/icons/stop.svg
new file mode 100644
index 0000000000..3beabd5394
--- /dev/null
+++ b/assets/icons/stop.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml
index 06df24d69a..eaa6fc5b73 100644
--- a/crates/assistant/Cargo.toml
+++ b/crates/assistant/Cargo.toml
@@ -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
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index e7fadd70a7..a6efad6049 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -1222,6 +1222,10 @@ impl Context {
}
}
+ pub(crate) fn token_count(&self) -> Option {
+ self.token_count
+ }
+
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext) {
let request = self.to_completion_request(cx);
self.pending_token_count = cx.spawn(|this, mut cx| {
diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs
index a40c4d1338..c45de4a1fd 100644
--- a/crates/assistant/src/inline_assistant.rs
+++ b/crates/assistant/src/inline_assistant.rs
@@ -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, 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, 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::(cx);
+ enum GutterPendingRange {}
+ if gutter_pending_ranges.is_empty() {
+ editor.clear_gutter_highlights::(cx);
} else {
- editor.highlight_background::(
- &background_ranges,
- |theme| theme.editor_active_line_background, // TODO use the appropriate color
+ editor.highlight_gutter::(
+ &gutter_pending_ranges,
+ |cx| cx.theme().status().info_background,
cx,
- );
+ )
+ }
+
+ enum GutterTransformedRange {}
+ if gutter_transformed_ranges.is_empty() {
+ editor.clear_gutter_highlights::(cx);
+ } else {
+ editor.highlight_gutter::(
+ &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::();
+ for row_range in inserted_row_ranges {
+ editor.highlight_rows::(
+ row_range,
+ Some(cx.theme().status().info_background),
+ false,
+ cx,
+ );
+ }
});
}
+
+ fn update_editor_blocks(
+ &mut self,
+ editor: &View,
+ 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::(
+ 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,
- confirmed: bool,
+ edited_since_done: bool,
gutter_dimensions: Arc>,
prompt_history: VecDeque,
prompt_history_ix: Option,
pending_prompt: String,
codegen: Model,
+ workspace: Option>,
_subscriptions: Vec,
}
@@ -584,39 +778,170 @@ impl EventEmitter for InlineAssistEditor {}
impl Render for InlineAssistEditor {
fn render(&mut self, cx: &mut ViewContext) -> 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::(cx);
+ })
+ .ok();
+ })
+ })
+ .tooltip(move |cx| {
+ let token_count = workspace.upgrade().and_then(|workspace| {
+ let panel =
+ workspace.read(cx).panel::(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>,
prompt_history: VecDeque,
codegen: Model,
+ workspace: Option>,
cx: &mut ViewContext,
) -> 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) {
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::(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, cx: &mut ViewContext) {
- 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) {
- 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) {
- 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,
- inline_assist_editor: Option<(BlockId, View)>,
+ editor_decorations: Option,
codegen: Model,
_subscriptions: Vec,
workspace: Option>,
include_context: bool,
}
+struct PendingInlineAssistDecorations {
+ prompt_block_id: BlockId,
+ prompt_editor: View,
+ removed_line_block_ids: HashSet,
+ 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 {
+ match self {
+ CodegenKind::Transform { range } => range.clone(),
+ CodegenKind::Generate { position } => position.bias_left(snapshot)..*position,
+ }
+ }
+}
+
pub struct Codegen {
buffer: Model,
+ old_buffer: Model,
snapshot: MultiBufferSnapshot,
kind: CodegenKind,
+ edit_position: Anchor,
last_equal_ranges: Vec>,
transaction_id: Option,
- error: Option,
+ status: CodegenStatus,
generation: Task<()>,
- idle: bool,
+ diff: Diff,
telemetry: Option>,
_subscription: gpui::Subscription,
}
+enum CodegenStatus {
+ Idle,
+ Pending,
+ Done,
+ Error(anyhow::Error),
+}
+
+#[derive(Default)]
+struct Diff {
+ task: Option>,
+ should_update: bool,
+ deleted_row_ranges: Vec<(Anchor, RangeInclusive)>,
+ inserted_row_ranges: Vec>,
+}
+
impl EventEmitter for Codegen {}
impl Codegen {
@@ -856,15 +1243,38 @@ impl Codegen {
cx: &mut ModelContext,
) -> 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),
}
@@ -886,28 +1296,13 @@ impl Codegen {
}
pub fn range(&self) -> Range {
- match &self.kind {
- CodegenKind::Transform { range } => range.clone(),
- CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
- }
- }
-
- pub fn kind(&self) -> &CodegenKind {
- &self.kind
+ self.kind.range(&self.snapshot)
}
pub fn last_equal_ranges(&self) -> &[Range] {
&self.last_equal_ranges
}
- pub fn idle(&self) -> bool {
- self.idle
- }
-
- pub fn error(&self) -> Option<&anyhow::Error> {
- self.error.as_ref()
- }
-
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext) {
let range = self.range();
let snapshot = self.snapshot.clone();
@@ -925,6 +1320,9 @@ impl Codegen {
let model_telemetry_id = prompt.model.telemetry_id();
let response = CompletionProvider::global(cx).complete(prompt);
let telemetry = self.telemetry.clone();
+ self.edit_position = range.start;
+ self.diff = Diff::default();
+ self.status = CodegenStatus::Pending;
self.generation = cx.spawn(|this, mut cx| {
async move {
let generate = async {
@@ -1058,6 +1456,7 @@ impl Codegen {
None,
cx,
);
+ this.edit_position = snapshot.anchor_after(edit_start);
buffer.end_transaction(cx)
});
@@ -1080,6 +1479,7 @@ impl Codegen {
}
}
+ this.update_diff(cx);
cx.notify();
})?;
}
@@ -1092,9 +1492,10 @@ impl Codegen {
let result = generate.await;
this.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
- this.idle = true;
if let Err(error) = result {
- this.error = Some(error);
+ this.status = CodegenStatus::Error(error);
+ } else {
+ this.status = CodegenStatus::Done;
}
cx.emit(CodegenEvent::Finished);
cx.notify();
@@ -1102,17 +1503,122 @@ impl Codegen {
.ok();
}
});
- self.error.take();
- self.idle = false;
+ cx.notify();
+ }
+
+ pub fn stop(&mut self, cx: &mut ModelContext) {
+ self.last_equal_ranges.clear();
+ self.status = CodegenStatus::Done;
+ self.generation = Task::ready(());
+ cx.emit(CodegenEvent::Finished);
cx.notify();
}
pub fn undo(&mut self, cx: &mut ModelContext) {
- if let Some(transaction_id) = self.transaction_id {
+ if let Some(transaction_id) = self.transaction_id.take() {
self.buffer
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
}
}
+
+ fn update_diff(&mut self, cx: &mut ModelContext) {
+ if self.diff.task.is_some() {
+ self.diff.should_update = true;
+ } else {
+ self.diff.should_update = false;
+
+ let old_snapshot = self.snapshot.clone();
+ let old_range = self.range().to_point(&old_snapshot);
+ let new_snapshot = self.buffer.read(cx).snapshot(cx);
+ let new_range = self.range().to_point(&new_snapshot);
+
+ self.diff.task = Some(cx.spawn(|this, mut cx| async move {
+ let (deleted_row_ranges, inserted_row_ranges) = cx
+ .background_executor()
+ .spawn(async move {
+ let old_text = old_snapshot
+ .text_for_range(
+ Point::new(old_range.start.row, 0)
+ ..Point::new(
+ old_range.end.row,
+ old_snapshot.line_len(MultiBufferRow(old_range.end.row)),
+ ),
+ )
+ .collect::();
+ let new_text = new_snapshot
+ .text_for_range(
+ Point::new(new_range.start.row, 0)
+ ..Point::new(
+ new_range.end.row,
+ new_snapshot.line_len(MultiBufferRow(new_range.end.row)),
+ ),
+ )
+ .collect::();
+
+ let mut old_row = old_range.start.row;
+ let mut new_row = new_range.start.row;
+ let diff = TextDiff::from_lines(old_text.as_str(), new_text.as_str());
+
+ let mut deleted_row_ranges: Vec<(Anchor, RangeInclusive)> = Vec::new();
+ let mut inserted_row_ranges = Vec::new();
+ for change in diff.iter_all_changes() {
+ let line_count = change.value().lines().count() as u32;
+ match change.tag() {
+ similar::ChangeTag::Equal => {
+ old_row += line_count;
+ new_row += line_count;
+ }
+ similar::ChangeTag::Delete => {
+ let old_end_row = old_row + line_count - 1;
+ let new_row =
+ new_snapshot.anchor_before(Point::new(new_row, 0));
+
+ if let Some((_, last_deleted_row_range)) =
+ deleted_row_ranges.last_mut()
+ {
+ if *last_deleted_row_range.end() + 1 == old_row {
+ *last_deleted_row_range =
+ *last_deleted_row_range.start()..=old_end_row;
+ } else {
+ deleted_row_ranges
+ .push((new_row, old_row..=old_end_row));
+ }
+ } else {
+ deleted_row_ranges.push((new_row, old_row..=old_end_row));
+ }
+
+ old_row += line_count;
+ }
+ similar::ChangeTag::Insert => {
+ let new_end_row = new_row + line_count - 1;
+ let start = new_snapshot.anchor_before(Point::new(new_row, 0));
+ let end = new_snapshot.anchor_before(Point::new(
+ new_end_row,
+ new_snapshot.line_len(MultiBufferRow(new_end_row)),
+ ));
+ inserted_row_ranges.push(start..=end);
+ new_row += line_count;
+ }
+ }
+ }
+
+ (deleted_row_ranges, inserted_row_ranges)
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.diff.deleted_row_ranges = deleted_row_ranges;
+ this.diff.inserted_row_ranges = inserted_row_ranges;
+ this.diff.task = None;
+ if this.diff.should_update {
+ this.update_diff(cx);
+ }
+ cx.notify();
+ })
+ .ok();
+ }));
+ }
+ }
}
fn strip_invalid_spans_from_codeblock(
diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs
index 1694b83231..5bed94a7ec 100644
--- a/crates/editor/src/display_map.rs
+++ b/crates/editor/src/display_map.rs
@@ -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)))
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 16402795ee..1e02312ecb 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -376,6 +376,7 @@ type CompletionId = usize;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option;
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>);
+type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range]>);
struct ScrollbarMarkerState {
scrollbar_size: Size,
@@ -464,6 +465,7 @@ pub struct Editor {
highlight_order: usize,
highlighted_rows: HashMap>,
background_highlights: TreeMap,
+ gutter_highlights: TreeMap,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option,
@@ -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(
+ &mut self,
+ ranges: &[Range],
+ color_fetcher: fn(&AppContext) -> Hsla,
+ cx: &mut ViewContext,
+ ) {
+ self.gutter_highlights
+ .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges)));
+ cx.notify();
+ }
+
+ pub fn clear_gutter_highlights(
+ &mut self,
+ cx: &mut ViewContext,
+ ) -> Option {
+ cx.notify();
+ self.gutter_highlights.remove(&TypeId::of::())
+ }
+
#[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,
+ display_snapshot: &DisplaySnapshot,
+ cx: &AppContext,
+ ) -> Vec<(Range, 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,
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 4df0022855..7eca7685af 100644
--- a/crates/editor/src/element.rs
+++ b/crates/editor/src/element.rs
@@ -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,
blocks: Vec,
highlighted_ranges: Vec<(Range, Hsla)>,
+ highlighted_gutter_ranges: Vec<(Range, Hsla)>,
redacted_ranges: Vec>,
cursors: Vec<(DisplayPoint, Hsla)>,
visible_cursors: Vec,
diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml
index 2fcae1b602..af2d90421f 100644
--- a/crates/language/Cargo.toml
+++ b/crates/language/Cargo.toml
@@ -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
diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs
index 36f905c961..31b6cb573e 100644
--- a/crates/language/src/buffer.rs
+++ b/crates/language/src/buffer.rs
@@ -797,6 +797,10 @@ impl Buffer {
.set_language_registry(language_registry);
}
+ pub fn language_registry(&self) -> Option> {
+ self.syntax_map.lock().language_registry()
+ }
+
/// Assign the buffer a new [Capability].
pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext) {
self.capability = capability;
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index cbda8cb12f..8103390eac 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -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",