From 4d177918c119acf6135f55253ab874ed803a56a2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 19 Jul 2024 11:13:15 +0200 Subject: [PATCH] Start on adding support for editing via the assistant panel (#14795) Note that this shouldn't have any visible user-facing behavior yet. The feature is incomplete but we wanna merge early to avoid a long-running branch. Release Notes: - N/A --------- Co-authored-by: Nathan --- Cargo.lock | 24 +- assets/prompts/operations.md | 241 ++++ .../src/activity_indicator.rs | 1 + crates/assistant/Cargo.toml | 3 +- crates/assistant/src/assistant.rs | 5 +- crates/assistant/src/assistant_panel.rs | 580 ++++---- crates/assistant/src/completion_provider.rs | 55 +- .../src/completion_provider/anthropic.rs | 2 +- .../src/completion_provider/cloud.rs | 2 +- .../assistant/src/completion_provider/fake.rs | 17 +- .../src/completion_provider/ollama.rs | 2 +- .../src/completion_provider/open_ai.rs | 2 +- crates/assistant/src/context.rs | 1165 ++++++++++++----- crates/assistant/src/inline_assistant.rs | 328 +++-- crates/assistant/src/prompt_library.rs | 16 +- crates/assistant/src/search.rs | 171 --- .../src/terminal_inline_assistant.rs | 6 +- crates/auto_update/src/auto_update.rs | 7 +- crates/collab/src/tests/following_tests.rs | 8 +- crates/command_palette/src/command_palette.rs | 2 +- crates/diagnostics/src/diagnostics.rs | 4 +- crates/diagnostics/src/grouped_diagnostics.rs | 4 +- crates/editor/src/editor.rs | 50 +- crates/editor/src/editor_tests.rs | 7 +- crates/editor/src/rust_analyzer_ext.rs | 1 + crates/extensions_ui/src/extensions_ui.rs | 4 +- crates/gpui/src/window.rs | 23 + crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 70 +- crates/language/src/buffer_tests.rs | 3 +- crates/language/src/language.rs | 8 + crates/language/src/outline.rs | 1 + crates/language_tools/src/lsp_log.rs | 1 + crates/languages/src/rust/outline.scm | 3 +- crates/multi_buffer/src/multi_buffer.rs | 12 + crates/project/src/project.rs | 31 + crates/project_symbols/src/project_symbols.rs | 3 +- crates/search/src/project_search.rs | 7 +- crates/tasks_ui/src/lib.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/text/src/anchor.rs | 5 + crates/welcome/src/welcome.rs | 2 +- crates/workspace/src/workspace.rs | 79 +- crates/zed/src/zed.rs | 7 +- 44 files changed, 1999 insertions(+), 968 deletions(-) create mode 100644 assets/prompts/operations.md delete mode 100644 crates/assistant/src/search.rs diff --git a/Cargo.lock b/Cargo.lock index 5d5f0bb712..042f6b69d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,6 +372,7 @@ version = "0.1.0" dependencies = [ "anthropic", "anyhow", + "assets", "assistant_slash_command", "async-watch", "breadcrumbs", @@ -408,6 +409,7 @@ dependencies = [ "rand 0.8.5", "regex", "rope", + "roxmltree 0.20.0", "schemars", "search", "semantic_index", @@ -416,7 +418,6 @@ dependencies = [ "settings", "similar", "smol", - "strsim 0.11.1", "strum", "telemetry_events", "terminal", @@ -2244,7 +2245,7 @@ dependencies = [ "bitflags 1.3.2", "clap_lex 0.2.4", "indexmap 1.9.3", - "strsim 0.10.0", + "strsim", "termcolor", "textwrap", ] @@ -2268,7 +2269,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex 0.5.1", - "strsim 0.10.0", + "strsim", ] [[package]] @@ -4272,7 +4273,7 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" dependencies = [ - "roxmltree", + "roxmltree 0.19.0", ] [[package]] @@ -5944,6 +5945,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "async-watch", "clock", "collections", "ctor", @@ -8901,6 +8903,12 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rpc" version = "0.1.0" @@ -10359,12 +10367,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.25.0" @@ -11899,7 +11901,7 @@ dependencies = [ "kurbo", "log", "pico-args", - "roxmltree", + "roxmltree 0.19.0", "simplecss", "siphasher 1.0.1", "strict-num", diff --git a/assets/prompts/operations.md b/assets/prompts/operations.md new file mode 100644 index 0000000000..b77cfedf12 --- /dev/null +++ b/assets/prompts/operations.md @@ -0,0 +1,241 @@ +Your task is to map a step from the conversation above to operations on symbols inside the provided source files. + +Guidelines: +- There's no need to describe *what* to do, just *where* to do it. +- If creating a file, assume any subsequent updates are included at the time of creation. +- Don't create and then update a file. +- We'll create it in one shot. +- Prefer updating symbols lower in the syntax tree if possible. +- Never include operations on a parent symbol and one of its children in the same block. +- Never nest an operation with another operation or include CDATA or other content. All operations are leaf nodes. +- Include a description attribute for each operation with a brief, one-line description of the change to perform. +- Descriptions are required for all operations except delete. +- When generating multiple operations, ensure the descriptions are specific to each individual operation. +- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide. +- Don't generate multiple operations at the same location. Instead, combine them together in a single operation with a succinct combined description. + +The available operation types are: + +1. : Modify an existing symbol in a file. +2. : Create a new file. +3. : Add a new symbol as sibling after an existing symbol in a file. +4. : Add a new symbol as the last child of an existing symbol in a file. +5. : Add a new symbol as the first child of an existing symbol in a file. +6. : Remove an existing symbol from a file. The `description` attribute is invalid for delete, but required for other ops. + +All operations *require* a path. +Operations that *require* a symbol: , , +Operations that don't allow a symbol: +Operations that have an *optional* symbol: , + +Example 1: + +User: + ```rs src/rectangle.rs + struct Rectangle { + width: f64, + height: f64, + } + + impl Rectangle { + fn new(width: f64, height: f64) -> Self { + Rectangle { width, height } + } + } + ``` + + Symbols for src/rectangle.rs: + - struct Rectangle + - impl Rectangle + - impl Rectangle fn new + + Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct + Implement the 'Display' trait for the Rectangle struct + + What are the operations for the step: Add a new method 'calculate_area' to the Rectangle struct + +Assistant (wrong): + + + + + +This demonstrates what NOT to do. NEVER append multiple children at the same location. + +Assistant (corrected): + + + + +User: +What are the operations for the step: Implement the 'Display' trait for the Rectangle struct + +Assistant: + + + + +Example 2: + +User: +```rs src/user.rs +struct User { + pub name: String, + age: u32, + email: String, +} + +impl User { + fn new(name: String, age: u32, email: String) -> Self { + User { name, age, email } + } + + pub fn print_info(&self) { + println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email); + } +} +``` + +Symbols for src/user.rs: +- struct User +- struct User pub name +- struct User age +- struct User email +- impl User +- impl User fn new +- impl User pub fn print_info + +Update the 'print_info' method to use formatted output +Remove the 'email' field from the User struct + +What are the operations for the step: Update the 'print_info' method to use formatted output + +Assistant: + + + + +User: +What are the operations for the step: Remove the 'email' field from the User struct + +Assistant: + + + + +Example 3: + +User: +```rs src/vehicle.rs +struct Vehicle { + make: String, + model: String, + year: u32, +} + +impl Vehicle { + fn new(make: String, model: String, year: u32) -> Self { + Vehicle { make, model, year } + } + + fn print_year(&self) { + println!("Year: {}", self.year); + } +} +``` + +Symbols for src/vehicle.rs: +- struct Vehicle +- struct Vehicle make +- struct Vehicle model +- struct Vehicle year +- impl Vehicle +- impl Vehicle fn new +- impl Vehicle fn print_year + +Add a 'use std::fmt;' statement at the beginning of the file +Add a new method 'start_engine' in the Vehicle impl block + +What are the operations for the step: Add a 'use std::fmt;' statement at the beginning of the file + +Assistant: + + + + +User: +What are the operations for the step: Add a new method 'start_engine' in the Vehicle impl block + +Assistant: + + + + +Example 4: + +User: +```rs src/employee.rs +struct Employee { + name: String, + position: String, + salary: u32, + department: String, +} + +impl Employee { + fn new(name: String, position: String, salary: u32, department: String) -> Self { + Employee { name, position, salary, department } + } + + fn print_details(&self) { + println!("Name: {}, Position: {}, Salary: {}, Department: {}", + self.name, self.position, self.salary, self.department); + } + + fn give_raise(&mut self, amount: u32) { + self.salary += amount; + } +} +``` + +Symbols for src/employee.rs: +- struct Employee +- struct Employee name +- struct Employee position +- struct Employee salary +- struct Employee department +- impl Employee +- impl Employee fn new +- impl Employee fn print_details +- impl Employee fn give_raise + +Make salary an f32 + +What are the operations for the step: Make salary an f32 + +A (wrong): + + + + + +This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`. + +A (corrected): + + + + +User: + What are the correct operations for the step: Remove the 'department' field and update the 'print_details' method + +A: + + + + + +Now generate the operations for the following step. +Output only valid XML containing valid operations with their required attributes. +NEVER output code or any other text inside tags. If you do, you will replaced with another model. +Your response *MUST* begin with and end with : diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 89023c12cf..d39cb4af47 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -107,6 +107,7 @@ impl ActivityIndicator { Editor::for_buffer(buffer, Some(project.clone()), cx) })), None, + true, cx, ); })?; diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index c48da49a0d..e3ddd4e2c7 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -23,6 +23,7 @@ test-support = [ [dependencies] anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true +assets.workspace = true assistant_slash_command.workspace = true async-watch.workspace = true breadcrumbs.workspace = true @@ -63,7 +64,6 @@ serde_json.workspace = true settings.workspace = true similar.workspace = true smol.workspace = true -strsim = "0.11" strum.workspace = true telemetry_events.workspace = true terminal.workspace = true @@ -76,6 +76,7 @@ util.workspace = true uuid.workspace = true workspace.workspace = true picker.workspace = true +roxmltree = "0.20.0" [dev-dependencies] ctor.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 50fec4a575..698465ecfb 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -7,7 +7,6 @@ mod inline_assistant; mod model_selector; mod prompt_library; mod prompts; -mod search; mod slash_command; mod streaming_diff; mod terminal_inline_assistant; @@ -53,9 +52,9 @@ actions!( InsertActivePrompt, DeployHistory, DeployPromptLibrary, - ApplyEdit, ConfirmCommand, - ToggleModelSelector + ToggleModelSelector, + DebugEditSteps ] ); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b6bd583587..d96f1f4bc1 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,19 +1,19 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, - humanize_token_count, parse_next_edit_suggestion, + humanize_token_count, prompt_library::open_prompt_library, - search::*, slash_command::{ default_command::DefaultSlashCommand, docs_command::{DocsSlashCommand, DocsSlashCommandArgs}, SlashCommandCompletionProvider, SlashCommandRegistry, }, terminal_inline_assistant::TerminalInlineAssistant, - ApplyEdit, Assist, CompletionProvider, ConfirmCommand, Context, ContextEvent, ContextId, - ContextStore, CycleMessageRole, DeployHistory, DeployPromptLibrary, EditSuggestion, - InlineAssist, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, - PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, - ResetKey, Role, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, + Assist, CompletionProvider, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, + CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, + EditStepOperations, EditSuggestionGroup, InlineAssist, InlineAssistId, InlineAssistant, + InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, + QuoteSelection, RemoteContextMetadata, ResetKey, Role, SavedContextMetadata, Split, + ToggleFocus, ToggleModelSelector, }; use anyhow::{anyhow, Result}; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; @@ -25,29 +25,36 @@ use editor::{ display_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, RenderBlock, ToDisplayPoint, }, - scroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint, + scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor}, + Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint, }; use editor::{display_map::CreaseId, FoldPlaceholder}; use fs::Fs; use gpui::{ div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext, - AsyncWindowContext, ClipboardItem, DismissEvent, Empty, EventEmitter, FocusHandle, - FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, - SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation, + AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter, + FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels, + Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext, }; use indexed_docs::IndexedDocsStore; use language::{ - language_settings::SoftWrap, AutoindentMode, Buffer, LanguageRegistry, LspAdapterDelegate, - OffsetRangeExt as _, Point, ToOffset, + language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point, + ToOffset, }; use multi_buffer::MultiBufferRow; use picker::{Picker, PickerDelegate}; -use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction}; +use project::{Project, ProjectLspAdapterDelegate}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use settings::Settings; -use std::{cmp, fmt::Write, ops::Range, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + cmp::{self, Ordering}, + fmt::Write, + ops::Range, + path::PathBuf, + sync::Arc, + time::Duration, +}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use theme::ThemeSettings; use ui::{ @@ -60,7 +67,8 @@ use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle}, - pane, + notifications::NotifyTaskExt, + pane::{self, SaveIntent}, searchable::{SearchEvent, SearchableItem}, Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; @@ -591,6 +599,7 @@ impl AssistantPanel { make_lsp_adapter_delegate(workspace.project(), cx).log_err() }); + let assistant_panel = cx.view().downgrade(); let editor = cx.new_view(|cx| { let mut editor = ContextEditor::for_context( context, @@ -598,6 +607,7 @@ impl AssistantPanel { workspace.clone(), self.project.clone(), lsp_adapter_delegate, + assistant_panel, cx, ); editor.insert_default_prompt(cx); @@ -720,6 +730,7 @@ impl AssistantPanel { cx.spawn(|this, mut cx| async move { let context = context.await?; + let assistant_panel = this.clone(); this.update(&mut cx, |this, cx| { let workspace = workspace .upgrade() @@ -731,6 +742,7 @@ impl AssistantPanel { workspace, project, lsp_adapter_delegate, + assistant_panel, cx, ) }); @@ -774,6 +786,7 @@ impl AssistantPanel { cx.spawn(|this, mut cx| async move { let context = context.await?; + let assistant_panel = this.clone(); this.update(&mut cx, |this, cx| { let workspace = workspace .upgrade() @@ -785,6 +798,7 @@ impl AssistantPanel { workspace, this.project.clone(), lsp_adapter_delegate, + assistant_panel, cx, ) }); @@ -956,10 +970,18 @@ struct ScrollPosition { cursor: Anchor, } +struct ActiveEditStep { + start: language::Anchor, + assist_ids: Vec, + editor: Option>, + _open_editor: Task>, +} + pub struct ContextEditor { context: Model, fs: Arc, workspace: WeakView, + project: Model, lsp_adapter_delegate: Option>, editor: View, blocks: HashSet, @@ -968,6 +990,8 @@ pub struct ContextEditor { pending_slash_command_creases: HashMap, CreaseId>, pending_slash_command_blocks: HashMap, BlockId>, _subscriptions: Vec, + active_edit_step: Option, + assistant_panel: WeakView, } impl ContextEditor { @@ -979,6 +1003,7 @@ impl ContextEditor { workspace: View, project: Model, lsp_adapter_delegate: Option>, + assistant_panel: WeakView, cx: &mut ViewContext, ) -> Self { let completion_provider = SlashCommandCompletionProvider::new( @@ -996,7 +1021,7 @@ impl ContextEditor { editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); editor.set_completion_provider(Box::new(completion_provider)); - editor.set_collaboration_hub(Box::new(project)); + editor.set_collaboration_hub(Box::new(project.clone())); editor }); @@ -1017,9 +1042,12 @@ impl ContextEditor { remote_id: None, fs, workspace: workspace.downgrade(), + project, pending_slash_command_creases: HashMap::default(), pending_slash_command_blocks: HashMap::default(), _subscriptions, + active_edit_step: None, + assistant_panel, }; this.update_message_headers(cx); this.insert_slash_command_output_sections(sections, cx); @@ -1052,31 +1080,37 @@ impl ContextEditor { } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { - let cursors = self.cursors(cx); + if !self.apply_edit_step(cx) { + self.send_to_model(cx); + } + } - let user_messages = self.context.update(cx, |context, cx| { - let selected_messages = context - .messages_for_offsets(cursors, cx) - .into_iter() - .map(|message| message.id) - .collect(); - context.assist(selected_messages, cx) - }); - let new_selections = user_messages - .iter() - .map(|message| { - let cursor = message + fn apply_edit_step(&mut self, cx: &mut ViewContext) -> bool { + if let Some(step) = self.active_edit_step.as_ref() { + InlineAssistant::update_global(cx, |assistant, cx| { + for assist_id in &step.assist_ids { + assistant.start_assist(*assist_id, cx); + } + !step.assist_ids.is_empty() + }) + } else { + false + } + } + + fn send_to_model(&mut self, cx: &mut ViewContext) { + if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) { + let new_selection = { + let cursor = user_message .start .to_offset(self.context.read(cx).buffer().read(cx)); cursor..cursor - }) - .collect::>(); - if !new_selections.is_empty() { + }; self.editor.update(cx, |editor, cx| { editor.change_selections( Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), cx, - |selections| selections.select_ranges(new_selections), + |selections| selections.select_ranges([new_selection]), ); }); // Avoid scrolling to the new cursor position so the assistant's output is stable. @@ -1093,6 +1127,53 @@ impl ContextEditor { } } + fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext) { + let mut output = String::new(); + for (i, step) in self.context.read(cx).edit_steps().iter().enumerate() { + output.push_str(&format!("Step {}:\n", i + 1)); + output.push_str(&format!( + "Content: {}\n", + self.context + .read(cx) + .buffer() + .read(cx) + .text_for_range(step.source_range.clone()) + .collect::() + )); + match &step.operations { + Some(EditStepOperations::Parsed { + operations, + raw_output, + }) => { + output.push_str(&format!("Raw Output:\n{raw_output}\n")); + output.push_str("Parsed Operations:\n"); + for op in operations { + output.push_str(&format!(" {:?}\n", op)); + } + } + Some(EditStepOperations::Pending(_)) => { + output.push_str("Operations: Pending\n"); + } + None => { + output.push_str("Operations: None\n"); + } + } + output.push('\n'); + } + + let editor = self + .workspace + .update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx)); + + if let Ok(editor) = editor { + cx.spawn(|_, mut cx| async move { + let editor = editor.await?; + editor.update(&mut cx, |editor, cx| editor.set_text(output, cx)) + }) + .detach_and_notify_err(cx); + } + } + fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext) { let cursors = self.cursors(cx); self.context.update(cx, |context, cx| { @@ -1222,39 +1303,8 @@ impl ContextEditor { context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); }); } - ContextEvent::EditSuggestionsChanged => { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let excerpt_id = *buffer.as_singleton().unwrap().0; - let context = self.context.read(cx); - let highlighted_rows = context - .edit_suggestions() - .iter() - .map(|suggestion| { - let start = buffer - .anchor_in_excerpt(excerpt_id, suggestion.source_range.start) - .unwrap(); - let end = buffer - .anchor_in_excerpt(excerpt_id, suggestion.source_range.end) - .unwrap(); - start..=end - }) - .collect::>(); - - editor.clear_row_highlights::(); - for range in highlighted_rows { - editor.highlight_rows::( - range, - Some( - cx.theme() - .colors() - .editor_document_highlight_read_background, - ), - false, - cx, - ); - } - }); + ContextEvent::EditStepsChanged => { + cx.notify(); } ContextEvent::SummaryChanged => { cx.emit(EditorEvent::TitleChanged); @@ -1515,12 +1565,200 @@ impl ContextEditor { } EditorEvent::SelectionsChanged { .. } => { self.scroll_position = self.cursor_scroll_position(cx); + if self + .edit_step_for_cursor(cx) + .map(|step| step.source_range.start) + != self.active_edit_step.as_ref().map(|step| step.start) + { + if let Some(old_active_edit_step) = self.active_edit_step.take() { + if let Some(editor) = old_active_edit_step + .editor + .and_then(|editor| editor.upgrade()) + { + self.workspace + .update(cx, |workspace, cx| { + if let Some(pane) = workspace.pane_for(&editor) { + pane.update(cx, |pane, cx| { + let item_id = editor.entity_id(); + if pane.is_active_preview_item(item_id) { + pane.close_item_by_id( + item_id, + SaveIntent::Skip, + cx, + ) + .detach_and_log_err(cx); + } + }); + } + }) + .ok(); + } + } + + if let Some(new_active_step) = self.edit_step_for_cursor(cx) { + let suggestions = new_active_step.edit_suggestions(&self.project, cx); + self.active_edit_step = Some(ActiveEditStep { + start: new_active_step.source_range.start, + assist_ids: Vec::new(), + editor: None, + _open_editor: self.open_editor_for_edit_suggestions(suggestions, cx), + }); + } + } } _ => {} } cx.emit(event.clone()); } + fn open_editor_for_edit_suggestions( + &mut self, + edit_suggestions: Task, Vec>>, + cx: &mut ViewContext, + ) -> Task> { + let workspace = self.workspace.clone(); + let project = self.project.clone(); + let assistant_panel = self.assistant_panel.clone(); + cx.spawn(|this, mut cx| async move { + let edit_suggestions = edit_suggestions.await; + + let mut assist_ids = Vec::new(); + let editor = if edit_suggestions.is_empty() { + return Ok(()); + } else if edit_suggestions.len() == 1 + && edit_suggestions.values().next().unwrap().len() == 1 + { + // If there's only one buffer and one suggestion group, open it directly + let (buffer, suggestion_groups) = edit_suggestions.into_iter().next().unwrap(); + let suggestion_group = suggestion_groups.into_iter().next().unwrap(); + let editor = workspace.update(&mut cx, |workspace, cx| { + let active_pane = workspace.active_pane().clone(); + workspace.open_project_item::(active_pane, buffer, false, false, cx) + })?; + + cx.update(|cx| { + for suggestion in suggestion_group.suggestions { + let description = suggestion.description.unwrap_or_else(|| "Delete".into()); + let range = { + let buffer = editor.read(cx).buffer().read(cx).read(cx); + let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); + buffer + .anchor_in_excerpt(excerpt_id, suggestion.range.start) + .unwrap() + ..buffer + .anchor_in_excerpt(excerpt_id, suggestion.range.end) + .unwrap() + }; + let initial_text = suggestion.prepend_newline.then(|| "\n".into()); + InlineAssistant::update_global(cx, |assistant, cx| { + assist_ids.push(assistant.suggest_assist( + &editor, + range, + description, + initial_text, + Some(workspace.clone()), + assistant_panel.upgrade().as_ref(), + cx, + )); + }); + } + + // Scroll the editor to the suggested assist + editor.update(cx, |editor, cx| { + let anchor = { + let buffer = editor.buffer().read(cx).read(cx); + let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); + buffer + .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start) + .unwrap() + }; + + editor.set_scroll_anchor( + ScrollAnchor { + offset: gpui::Point::default(), + anchor, + }, + cx, + ); + }); + })?; + + editor + } else { + // If there are multiple buffers or suggestion groups, create a multibuffer + let mut inline_assist_suggestions = Vec::new(); + let multibuffer = cx.new_model(|cx| { + let replica_id = project.read(cx).replica_id(); + let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite); + for (buffer, suggestion_groups) in edit_suggestions { + let excerpt_ids = multibuffer.push_excerpts( + buffer, + suggestion_groups + .iter() + .map(|suggestion_group| ExcerptRange { + context: suggestion_group.context_range.clone(), + primary: None, + }), + cx, + ); + + for (excerpt_id, suggestion_group) in + excerpt_ids.into_iter().zip(suggestion_groups) + { + for suggestion in suggestion_group.suggestions { + let description = + suggestion.description.unwrap_or_else(|| "Delete".into()); + let range = { + let multibuffer = multibuffer.read(cx); + multibuffer + .anchor_in_excerpt(excerpt_id, suggestion.range.start) + .unwrap() + ..multibuffer + .anchor_in_excerpt(excerpt_id, suggestion.range.end) + .unwrap() + }; + let initial_text = + suggestion.prepend_newline.then(|| "\n".to_string()); + inline_assist_suggestions.push((range, description, initial_text)); + } + } + } + multibuffer + })?; + + let editor = cx + .new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx))?; + cx.update(|cx| { + InlineAssistant::update_global(cx, |assistant, cx| { + for (range, description, initial_text) in inline_assist_suggestions { + assist_ids.push(assistant.suggest_assist( + &editor, + range, + description, + initial_text, + Some(workspace.clone()), + assistant_panel.upgrade().as_ref(), + cx, + )); + } + }) + })?; + workspace.update(&mut cx, |workspace, cx| { + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx) + })?; + + editor + }; + + this.update(&mut cx, |this, _cx| { + if let Some(step) = this.active_edit_step.as_mut() { + step.assist_ids = assist_ids; + step.editor = Some(editor.downgrade()); + } + }) + }) + } + fn handle_editor_search_event( &mut self, _: View, @@ -1785,173 +2023,6 @@ impl ContextEditor { }); } - fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let project = workspace.read(cx).project().clone(); - - struct Edit { - old_text: String, - new_text: String, - } - - let context = self.context.read(cx); - let context_buffer = context.buffer().read(cx); - let context_buffer_snapshot = context_buffer.snapshot(); - - let selections = self.editor.read(cx).selections.disjoint_anchors(); - let mut selections = selections.iter().peekable(); - let selected_suggestions = context - .edit_suggestions() - .iter() - .filter(|suggestion| { - while let Some(selection) = selections.peek() { - if selection - .end - .text_anchor - .cmp(&suggestion.source_range.start, context_buffer) - .is_lt() - { - selections.next(); - continue; - } - if selection - .start - .text_anchor - .cmp(&suggestion.source_range.end, context_buffer) - .is_gt() - { - break; - } - return true; - } - false - }) - .cloned() - .collect::>(); - - let mut opened_buffers: HashMap>>> = HashMap::default(); - project.update(cx, |project, cx| { - for suggestion in &selected_suggestions { - opened_buffers - .entry(suggestion.full_path.clone()) - .or_insert_with(|| { - project.open_buffer_for_full_path(&suggestion.full_path, cx) - }); - } - }); - - cx.spawn(|this, mut cx| async move { - let mut buffers_by_full_path = HashMap::default(); - for (full_path, buffer) in opened_buffers { - if let Some(buffer) = buffer.await.log_err() { - buffers_by_full_path.insert(full_path, buffer); - } - } - - let mut suggestions_by_buffer = HashMap::default(); - cx.update(|cx| { - for suggestion in selected_suggestions { - if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) { - let (_, edits) = suggestions_by_buffer - .entry(buffer.clone()) - .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new())); - - let mut lines = context_buffer_snapshot - .as_rope() - .chunks_in_range( - suggestion.source_range.to_offset(&context_buffer_snapshot), - ) - .lines(); - if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) { - let old_text = context_buffer_snapshot - .text_for_range(suggestion.old_text_range) - .collect(); - let new_text = context_buffer_snapshot - .text_for_range(suggestion.new_text_range) - .collect(); - edits.push(Edit { old_text, new_text }); - } - } - } - })?; - - let edits_by_buffer = cx - .background_executor() - .spawn(async move { - let mut result = HashMap::default(); - for (buffer, (snapshot, suggestions)) in suggestions_by_buffer { - let edits = - result - .entry(buffer) - .or_insert(Vec::<(Range, _)>::new()); - for suggestion in suggestions { - if let Some(range) = - fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text) - { - let edit_start = snapshot.anchor_after(range.start); - let edit_end = snapshot.anchor_before(range.end); - if let Err(ix) = edits.binary_search_by(|(range, _)| { - range.start.cmp(&edit_start, &snapshot) - }) { - edits.insert( - ix, - (edit_start..edit_end, suggestion.new_text.clone()), - ); - } - } else { - log::info!( - "assistant edit did not match any text in buffer {:?}", - &suggestion.old_text - ); - } - } - } - result - }) - .await; - - let mut project_transaction = ProjectTransaction::default(); - let (editor, workspace, title) = this.update(&mut cx, |this, cx| { - for (buffer_handle, edits) in edits_by_buffer { - buffer_handle.update(cx, |buffer, cx| { - buffer.start_transaction(); - buffer.edit( - edits, - Some(AutoindentMode::Block { - original_indent_columns: Vec::new(), - }), - cx, - ); - buffer.end_transaction(cx); - if let Some(transaction) = buffer.finalize_last_transaction() { - project_transaction - .0 - .insert(buffer_handle.clone(), transaction.clone()); - } - }); - } - - ( - this.editor.downgrade(), - this.workspace.clone(), - this.title(cx), - ) - })?; - - Editor::open_project_transaction( - &editor, - workspace, - project_transaction, - format!("Edits from {}", title), - cx, - ) - .await - }) - .detach_and_log_err(cx); - } - fn save(&mut self, _: &Save, cx: &mut ViewContext) { self.context .update(cx, |context, cx| context.save(None, self.fs.clone(), cx)); @@ -1967,6 +2038,14 @@ impl ContextEditor { fn render_send_button(&self, cx: &mut ViewContext) -> impl IntoElement { let focus_handle = self.focus_handle(cx).clone(); + let button_text = match self.edit_step_for_cursor(cx) { + Some(edit_step) => match &edit_step.operations { + Some(EditStepOperations::Pending(_)) => "Computing Changes...", + Some(EditStepOperations::Parsed { .. }) => "Apply Changes", + None => "Send", + }, + None => "Send", + }; ButtonLike::new("send_button") .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) @@ -1974,11 +2053,38 @@ impl ContextEditor { KeyBinding::for_action_in(&Assist, &focus_handle, cx) .map(|binding| binding.into_any_element()), ) - .child(Label::new("Send")) + .child(Label::new(button_text)) .on_click(move |_event, cx| { focus_handle.dispatch_action(&Assist, cx); }) } + + fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> { + let newest_cursor = self + .editor + .read(cx) + .selections + .newest_anchor() + .head() + .text_anchor; + let context = self.context.read(cx); + let buffer = context.buffer().read(cx); + + let edit_steps = context.edit_steps(); + edit_steps + .binary_search_by(|step| { + let step_range = step.source_range.clone(); + if newest_cursor.cmp(&step_range.start, buffer).is_lt() { + Ordering::Greater + } else if newest_cursor.cmp(&step_range.end, buffer).is_gt() { + Ordering::Less + } else { + Ordering::Equal + } + }) + .ok() + .map(|index| &edit_steps[index]) + } } impl EventEmitter for ContextEditor {} @@ -1995,7 +2101,7 @@ impl Render for ContextEditor { .capture_action(cx.listener(ContextEditor::confirm_command)) .on_action(cx.listener(ContextEditor::assist)) .on_action(cx.listener(ContextEditor::split)) - .on_action(cx.listener(ContextEditor::apply_edit)) + .on_action(cx.listener(ContextEditor::debug_edit_steps)) .size_full() .v_flex() .child( diff --git a/crates/assistant/src/completion_provider.rs b/crates/assistant/src/completion_provider.rs index a51d3256e2..13f91f70e3 100644 --- a/crates/assistant/src/completion_provider.rs +++ b/crates/assistant/src/completion_provider.rs @@ -20,11 +20,10 @@ use crate::{ }; use anyhow::Result; use client::Client; -use futures::{future::BoxFuture, stream::BoxStream}; +use futures::{future::BoxFuture, stream::BoxStream, StreamExt}; use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext}; use settings::{Settings, SettingsStore}; -use std::time::Duration; -use std::{any::Any, sync::Arc}; +use std::{any::Any, pin::Pin, sync::Arc, task::Poll, time::Duration}; /// Choose which model to use for openai provider. /// If the model is not available, try to use the first available model, or fallback to the original model. @@ -55,10 +54,21 @@ pub fn init(client: Arc, cx: &mut AppContext) { } pub struct CompletionResponse { - pub inner: BoxFuture<'static, Result>>>, + inner: BoxStream<'static, Result>, _lock: SemaphoreGuardArc, } +impl futures::Stream for CompletionResponse { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.inner).poll_next(cx) + } +} + pub trait LanguageModelCompletionProvider: Send + Sync { fn available_models(&self, cx: &AppContext) -> Vec; fn settings_version(&self) -> usize; @@ -72,7 +82,7 @@ pub trait LanguageModelCompletionProvider: Send + Sync { request: LanguageModelRequest, cx: &AppContext, ) -> BoxFuture<'static, Result>; - fn complete( + fn stream_completion( &self, request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>>; @@ -136,20 +146,34 @@ impl CompletionProvider { self.provider.read().count_tokens(request, cx) } - pub fn complete( + pub fn stream_completion( &self, request: LanguageModelRequest, cx: &AppContext, - ) -> Task { + ) -> Task> { let rate_limiter = self.request_limiter.clone(); let provider = self.provider.clone(); - cx.background_executor().spawn(async move { + cx.foreground_executor().spawn(async move { let lock = rate_limiter.acquire_arc().await; - let response = provider.read().complete(request); - CompletionResponse { + let response = provider.read().stream_completion(request); + let response = response.await?; + Ok(CompletionResponse { inner: response, _lock: lock, + }) + }) + } + + pub fn complete(&self, request: LanguageModelRequest, cx: &AppContext) -> Task> { + let response = self.stream_completion(request, cx); + cx.foreground_executor().spawn(async move { + let mut chunks = response.await?; + let mut completion = String::new(); + while let Some(chunk) = chunks.next().await { + let chunk = chunk?; + completion.push_str(&chunk); } + Ok(completion) }) } } @@ -300,7 +324,7 @@ mod tests { // Enqueue some requests for i in 0..MAX_CONCURRENT_COMPLETION_REQUESTS * 2 { - let response = provider.complete( + let response = provider.stream_completion( LanguageModelRequest { temperature: i as f32 / 10.0, ..Default::default() @@ -309,8 +333,7 @@ mod tests { ); cx.background_executor() .spawn(async move { - let response = response.await; - let mut stream = response.inner.await.unwrap(); + let mut stream = response.await.unwrap(); while let Some(message) = stream.next().await { message.unwrap(); } @@ -326,7 +349,7 @@ mod tests { // Get the first completion request that is in flight and mark it as completed. let completion = fake_provider - .running_completions() + .pending_completions() .into_iter() .next() .unwrap(); @@ -347,7 +370,7 @@ mod tests { ); // Mark all completion requests as finished that are in flight. - for request in fake_provider.running_completions() { + for request in fake_provider.pending_completions() { fake_provider.finish_completion(&request); } @@ -362,7 +385,7 @@ mod tests { ); // Finish all remaining completion requests. - for request in fake_provider.running_completions() { + for request in fake_provider.pending_completions() { fake_provider.finish_completion(&request); } diff --git a/crates/assistant/src/completion_provider/anthropic.rs b/crates/assistant/src/completion_provider/anthropic.rs index b4c573588b..48d2020cbe 100644 --- a/crates/assistant/src/completion_provider/anthropic.rs +++ b/crates/assistant/src/completion_provider/anthropic.rs @@ -94,7 +94,7 @@ impl LanguageModelCompletionProvider for AnthropicCompletionProvider { count_open_ai_tokens(request, cx.background_executor()) } - fn complete( + fn stream_completion( &self, request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { diff --git a/crates/assistant/src/completion_provider/cloud.rs b/crates/assistant/src/completion_provider/cloud.rs index c02e531ee9..32b8587116 100644 --- a/crates/assistant/src/completion_provider/cloud.rs +++ b/crates/assistant/src/completion_provider/cloud.rs @@ -135,7 +135,7 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider { } } - fn complete( + fn stream_completion( &self, mut request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { diff --git a/crates/assistant/src/completion_provider/fake.rs b/crates/assistant/src/completion_provider/fake.rs index 434e584d00..e9ad8d9a0f 100644 --- a/crates/assistant/src/completion_provider/fake.rs +++ b/crates/assistant/src/completion_provider/fake.rs @@ -23,7 +23,7 @@ impl FakeCompletionProvider { this } - pub fn running_completions(&self) -> Vec { + pub fn pending_completions(&self) -> Vec { self.current_completion_txs .lock() .keys() @@ -35,7 +35,7 @@ impl FakeCompletionProvider { self.current_completion_txs.lock().len() } - pub fn send_completion(&self, request: &LanguageModelRequest, chunk: String) { + pub fn send_completion_chunk(&self, request: &LanguageModelRequest, chunk: String) { let json = serde_json::to_string(request).unwrap(); self.current_completion_txs .lock() @@ -45,10 +45,19 @@ impl FakeCompletionProvider { .unwrap(); } + pub fn send_last_completion_chunk(&self, chunk: String) { + self.send_completion_chunk(self.pending_completions().last().unwrap(), chunk); + } + pub fn finish_completion(&self, request: &LanguageModelRequest) { self.current_completion_txs .lock() - .remove(&serde_json::to_string(request).unwrap()); + .remove(&serde_json::to_string(request).unwrap()) + .unwrap(); + } + + pub fn finish_last_completion(&self) { + self.finish_completion(self.pending_completions().last().unwrap()); } } @@ -89,7 +98,7 @@ impl LanguageModelCompletionProvider for FakeCompletionProvider { futures::future::ready(Ok(0)).boxed() } - fn complete( + fn stream_completion( &self, _request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { diff --git a/crates/assistant/src/completion_provider/ollama.rs b/crates/assistant/src/completion_provider/ollama.rs index f782a20355..59d79e3ae7 100644 --- a/crates/assistant/src/completion_provider/ollama.rs +++ b/crates/assistant/src/completion_provider/ollama.rs @@ -91,7 +91,7 @@ impl LanguageModelCompletionProvider for OllamaCompletionProvider { async move { Ok(token_count) }.boxed() } - fn complete( + fn stream_completion( &self, request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { diff --git a/crates/assistant/src/completion_provider/open_ai.rs b/crates/assistant/src/completion_provider/open_ai.rs index 6c16e2c9a6..fd65d1afe5 100644 --- a/crates/assistant/src/completion_provider/open_ai.rs +++ b/crates/assistant/src/completion_provider/open_ai.rs @@ -179,7 +179,7 @@ impl LanguageModelCompletionProvider for OpenAiCompletionProvider { count_open_ai_tokens(request, cx.background_executor()) } - fn complete( + fn stream_completion( &self, request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index e3dcd36313..25f24753a1 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1,6 +1,6 @@ use crate::{ - slash_command::SlashCommandLine, CompletionProvider, LanguageModelRequest, - LanguageModelRequestMessage, MessageId, MessageStatus, Role, + prompt_library::PromptStore, slash_command::SlashCommandLine, CompletionProvider, + LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageStatus, Role, }; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ @@ -10,14 +10,21 @@ use client::{proto, telemetry::Telemetry}; use clock::ReplicaId; use collections::{HashMap, HashSet}; use fs::Fs; -use futures::{future::Shared, FutureExt, StreamExt}; +use futures::{ + future::{self, Shared}, + FutureExt, StreamExt, +}; use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task}; -use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; +use language::{ + AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, ParseStatus, Point, ToOffset, +}; use open_ai::Model as OpenAiModel; use paths::contexts_dir; +use project::Project; use serde::{Deserialize, Serialize}; use std::{ - cmp::Ordering, + cmp, + fmt::Debug, iter, mem, ops::Range, path::{Path, PathBuf}, @@ -26,7 +33,7 @@ use std::{ }; use telemetry_events::AssistantKind; use ui::SharedString; -use util::{post_inc, TryFutureExt}; +use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] @@ -265,7 +272,7 @@ impl ContextOperation { pub enum ContextEvent { MessagesEdited, SummaryChanged, - EditSuggestionsChanged, + EditStepsChanged, StreamedCompletion, PendingSlashCommandsUpdated { removed: Vec>, @@ -326,6 +333,331 @@ struct PendingCompletion { #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] pub struct SlashCommandId(clock::Lamport); +#[derive(Debug)] +pub struct EditStep { + pub source_range: Range, + pub operations: Option, +} + +#[derive(Debug)] +pub struct EditSuggestionGroup { + pub context_range: Range, + pub suggestions: Vec, +} + +#[derive(Debug)] +pub struct EditSuggestion { + pub range: Range, + /// If None, assume this is a suggestion to delete the range rather than transform it. + pub description: Option, + pub prepend_newline: bool, +} + +impl EditStep { + pub fn edit_suggestions( + &self, + project: &Model, + cx: &AppContext, + ) -> Task, Vec>> { + let Some(EditStepOperations::Parsed { operations, .. }) = &self.operations else { + return Task::ready(HashMap::default()); + }; + + let suggestion_tasks: Vec<_> = operations + .iter() + .map(|operation| operation.edit_suggestion(project.clone(), cx)) + .collect(); + + cx.spawn(|mut cx| async move { + let suggestions = future::join_all(suggestion_tasks) + .await + .into_iter() + .filter_map(|task| task.log_err()) + .collect::>(); + + let mut suggestions_by_buffer = HashMap::default(); + for (buffer, suggestion) in suggestions { + suggestions_by_buffer + .entry(buffer) + .or_insert_with(Vec::new) + .push(suggestion); + } + + let mut suggestion_groups_by_buffer = HashMap::default(); + for (buffer, mut suggestions) in suggestions_by_buffer { + let mut suggestion_groups = Vec::::new(); + buffer + .update(&mut cx, |buffer, _cx| { + // Sort suggestions by their range + suggestions.sort_by(|a, b| a.range.cmp(&b.range, buffer)); + + // Dedup overlapping suggestions + suggestions.dedup_by(|a, b| { + let a_range = a.range.to_offset(buffer); + let b_range = b.range.to_offset(buffer); + if a_range.start <= b_range.end && b_range.start <= a_range.end { + if b_range.start < a_range.start { + a.range.start = b.range.start; + } + if b_range.end > a_range.end { + a.range.end = b.range.end; + } + + if let (Some(a_desc), Some(b_desc)) = + (a.description.as_mut(), b.description.as_mut()) + { + b_desc.push('\n'); + b_desc.push_str(a_desc); + } else if a.description.is_some() { + b.description = a.description.take(); + } + + true + } else { + false + } + }); + + // Create context ranges for each suggestion + for suggestion in suggestions { + let context_range = { + let suggestion_point_range = suggestion.range.to_point(buffer); + let start_row = suggestion_point_range.start.row.saturating_sub(5); + let end_row = cmp::min( + suggestion_point_range.end.row + 5, + buffer.max_point().row, + ); + let start = buffer.anchor_before(Point::new(start_row, 0)); + let end = buffer + .anchor_after(Point::new(end_row, buffer.line_len(end_row))); + start..end + }; + + if let Some(last_group) = suggestion_groups.last_mut() { + if last_group + .context_range + .end + .cmp(&context_range.start, buffer) + .is_ge() + { + // Merge with the previous group if context ranges overlap + last_group.context_range.end = context_range.end; + last_group.suggestions.push(suggestion); + } else { + // Create a new group + suggestion_groups.push(EditSuggestionGroup { + context_range, + suggestions: vec![suggestion], + }); + } + } else { + // Create the first group + suggestion_groups.push(EditSuggestionGroup { + context_range, + suggestions: vec![suggestion], + }); + } + } + }) + .ok(); + suggestion_groups_by_buffer.insert(buffer, suggestion_groups); + } + + suggestion_groups_by_buffer + }) + } +} + +pub enum EditStepOperations { + Pending(Task>), + Parsed { + operations: Vec, + raw_output: String, + }, +} + +impl Debug for EditStepOperations { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EditStepOperations::Pending(_) => write!(f, "EditStepOperations::Pending"), + EditStepOperations::Parsed { + operations, + raw_output, + } => f + .debug_struct("EditStepOperations::Parsed") + .field("operations", operations) + .field("raw_output", raw_output) + .finish(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditOperation { + pub path: String, + pub kind: EditOperationKind, +} + +impl EditOperation { + fn edit_suggestion( + &self, + project: Model, + cx: &AppContext, + ) -> Task, EditSuggestion)>> { + let path = self.path.clone(); + let kind = self.kind.clone(); + cx.spawn(move |mut cx| async move { + let buffer = project + .update(&mut cx, |project, cx| { + let project_path = project + .project_path_for_full_path(Path::new(&path), cx) + .with_context(|| format!("worktree not found for {:?}", path))?; + anyhow::Ok(project.open_buffer(project_path, cx)) + })?? + .await?; + + let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?; + while *parse_status.borrow() != ParseStatus::Idle { + parse_status.changed().await?; + } + + let prepend_newline = kind.prepend_newline(); + let suggestion_range = if let Some(symbol) = kind.symbol() { + let outline = buffer + .update(&mut cx, |buffer, _| buffer.snapshot().outline(None))? + .context("no outline for buffer")?; + let candidate = outline + .path_candidates + .iter() + .find(|item| item.string == symbol) + .context("symbol not found")?; + buffer.update(&mut cx, |buffer, _| { + let outline_item = &outline.items[candidate.id]; + let symbol_range = outline_item.range.to_point(buffer); + let body_range = outline_item + .body_range + .as_ref() + .map(|range| range.to_point(buffer)) + .unwrap_or(symbol_range.clone()); + + match kind { + EditOperationKind::PrependChild { .. } => { + let position = buffer.anchor_after(body_range.start); + position..position + } + EditOperationKind::AppendChild { .. } => { + let position = buffer.anchor_before(body_range.end); + position..position + } + EditOperationKind::InsertSiblingBefore { .. } => { + let position = buffer.anchor_before(symbol_range.start); + position..position + } + EditOperationKind::InsertSiblingAfter { .. } => { + let position = buffer.anchor_after(symbol_range.end); + position..position + } + EditOperationKind::Update { .. } | EditOperationKind::Delete { .. } => { + let start = Point::new(symbol_range.start.row, 0); + let end = Point::new( + symbol_range.end.row, + buffer.line_len(symbol_range.end.row), + ); + buffer.anchor_before(start)..buffer.anchor_after(end) + } + EditOperationKind::Create { .. } => unreachable!(), + } + })? + } else { + match kind { + EditOperationKind::PrependChild { .. } => { + language::Anchor::MIN..language::Anchor::MIN + } + EditOperationKind::AppendChild { .. } | EditOperationKind::Create { .. } => { + language::Anchor::MAX..language::Anchor::MAX + } + _ => unreachable!("All other operations should have a symbol"), + } + }; + + Ok(( + buffer, + EditSuggestion { + range: suggestion_range, + description: kind.description().map(ToString::to_string), + prepend_newline, + }, + )) + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditOperationKind { + Update { + symbol: String, + description: String, + }, + Create { + description: String, + }, + InsertSiblingBefore { + symbol: String, + description: String, + }, + InsertSiblingAfter { + symbol: String, + description: String, + }, + PrependChild { + symbol: Option, + description: String, + }, + AppendChild { + symbol: Option, + description: String, + }, + Delete { + symbol: String, + }, +} + +impl EditOperationKind { + pub fn symbol(&self) -> Option<&str> { + match self { + Self::Update { symbol, .. } => Some(symbol), + Self::InsertSiblingBefore { symbol, .. } => Some(symbol), + Self::InsertSiblingAfter { symbol, .. } => Some(symbol), + Self::PrependChild { symbol, .. } => symbol.as_deref(), + Self::AppendChild { symbol, .. } => symbol.as_deref(), + Self::Delete { symbol } => Some(symbol), + Self::Create { .. } => None, + } + } + + pub fn description(&self) -> Option<&str> { + match self { + Self::Update { description, .. } => Some(description), + Self::Create { description } => Some(description), + Self::InsertSiblingBefore { description, .. } => Some(description), + Self::InsertSiblingAfter { description, .. } => Some(description), + Self::PrependChild { description, .. } => Some(description), + Self::AppendChild { description, .. } => Some(description), + Self::Delete { .. } => None, + } + } + + pub fn prepend_newline(&self) -> bool { + match self { + Self::PrependChild { .. } + | Self::AppendChild { .. } + | Self::InsertSiblingAfter { .. } + | Self::InsertSiblingBefore { .. } => true, + _ => false, + } + } +} + pub struct Context { id: ContextId, timestamp: clock::Lamport, @@ -333,7 +665,6 @@ pub struct Context { pending_ops: Vec, operations: Vec, buffer: Model, - edit_suggestions: Vec, pending_slash_commands: Vec, edits_since_last_slash_command_parse: language::Subscription, finished_slash_commands: HashSet, @@ -346,12 +677,12 @@ pub struct Context { pending_completions: Vec, token_count: Option, pending_token_count: Task>, - pending_edit_suggestion_parse: Option>, pending_save: Task>, path: Option, _subscriptions: Vec, telemetry: Option>, language_registry: Arc, + edit_steps: Vec, } impl EventEmitter for Context {} @@ -400,7 +731,6 @@ impl Context { operations: Vec::new(), message_anchors: Default::default(), messages_metadata: Default::default(), - edit_suggestions: Vec::new(), pending_slash_commands: Vec::new(), finished_slash_commands: HashSet::default(), slash_command_output_sections: Vec::new(), @@ -411,13 +741,13 @@ impl Context { pending_completions: Default::default(), token_count: None, pending_token_count: Task::ready(None), - pending_edit_suggestion_parse: None, _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], pending_save: Task::ready(Ok(())), path: None, buffer, telemetry, language_registry, + edit_steps: Vec::new(), }; let first_message_id = MessageId(clock::Lamport { @@ -735,8 +1065,8 @@ impl Context { self.summary.as_ref() } - pub fn edit_suggestions(&self) -> &[EditSuggestion] { - &self.edit_suggestions + pub fn edit_steps(&self) -> &[EditStep] { + &self.edit_steps } pub fn pending_slash_commands(&self) -> &[PendingSlashCommand] { @@ -771,8 +1101,8 @@ impl Context { )), language::Event::Edited => { self.count_remaining_tokens(cx); - self.reparse_edit_suggestions(cx); self.reparse_slash_commands(cx); + self.prune_invalid_edit_steps(cx); cx.emit(ContextEvent::MessagesEdited); } _ => {} @@ -880,65 +1210,245 @@ impl Context { } } - fn reparse_edit_suggestions(&mut self, cx: &mut ModelContext) { - self.pending_edit_suggestion_parse = Some(cx.spawn(|this, mut cx| async move { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - - this.update(&mut cx, |this, cx| { - this.reparse_edit_suggestions_in_range(0..this.buffer.read(cx).len(), cx); - }) - .ok(); - })); + fn prune_invalid_edit_steps(&mut self, cx: &mut ModelContext) { + let buffer = self.buffer.read(cx); + let prev_len = self.edit_steps.len(); + self.edit_steps.retain(|step| { + step.source_range.start.is_valid(buffer) && step.source_range.end.is_valid(buffer) + }); + if self.edit_steps.len() != prev_len { + cx.emit(ContextEvent::EditStepsChanged); + cx.notify(); + } } - fn reparse_edit_suggestions_in_range( - &mut self, - range: Range, - cx: &mut ModelContext, - ) { - self.buffer.update(cx, |buffer, _| { - let range_start = buffer.anchor_before(range.start); - let range_end = buffer.anchor_after(range.end); - let start_ix = self - .edit_suggestions - .binary_search_by(|probe| { - probe - .source_range - .end - .cmp(&range_start, buffer) - .then(Ordering::Greater) - }) - .unwrap_err(); - let end_ix = self - .edit_suggestions - .binary_search_by(|probe| { - probe - .source_range - .start - .cmp(&range_end, buffer) - .then(Ordering::Less) - }) - .unwrap_err(); + fn parse_edit_steps_in_range(&mut self, range: Range, cx: &mut ModelContext) { + let mut new_edit_steps = Vec::new(); - let mut new_edit_suggestions = Vec::new(); + self.buffer.update(cx, |buffer, _cx| { let mut message_lines = buffer.as_rope().chunks_in_range(range).lines(); - while let Some(suggestion) = parse_next_edit_suggestion(&mut message_lines) { - let start_anchor = buffer.anchor_after(suggestion.outer_range.start); - let end_anchor = buffer.anchor_before(suggestion.outer_range.end); - new_edit_suggestions.push(EditSuggestion { - source_range: start_anchor..end_anchor, - full_path: suggestion.path, - }); + let mut in_step = false; + let mut step_start = 0; + let mut line_start_offset = message_lines.offset(); + + while let Some(line) = message_lines.next() { + if let Some(step_start_index) = line.find("") { + if !in_step { + in_step = true; + step_start = line_start_offset + step_start_index; + } + } + + if let Some(step_end_index) = line.find("") { + if in_step { + let start_anchor = buffer.anchor_after(step_start); + let end_anchor = buffer + .anchor_before(line_start_offset + step_end_index + "".len()); + let source_range = start_anchor..end_anchor; + + // Check if a step with the same range already exists + let existing_step_index = self.edit_steps.binary_search_by(|probe| { + probe.source_range.cmp(&source_range, buffer) + }); + + if let Err(ix) = existing_step_index { + // Step doesn't exist, so add it + new_edit_steps.push(( + ix, + EditStep { + source_range, + operations: None, + }, + )); + } + + in_step = false; + } + } + + line_start_offset = message_lines.offset(); } - self.edit_suggestions - .splice(start_ix..end_ix, new_edit_suggestions); }); - cx.emit(ContextEvent::EditSuggestionsChanged); + + // Insert new steps and generate their corresponding tasks + for (index, mut step) in new_edit_steps.into_iter().rev() { + let task = self.generate_edit_step_operations(&step, cx); + step.operations = Some(EditStepOperations::Pending(task)); + self.edit_steps.insert(index, step); + } + + cx.emit(ContextEvent::EditStepsChanged); cx.notify(); } + fn generate_edit_step_operations( + &self, + edit_step: &EditStep, + cx: &mut ModelContext, + ) -> Task> { + let mut request = self.to_completion_request(cx); + let edit_step_range = edit_step.source_range.clone(); + let step_text = self + .buffer + .read(cx) + .text_for_range(edit_step_range.clone()) + .collect::(); + + cx.spawn(|this, mut cx| async move { + let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?; + + let mut prompt = prompt_store.operations_prompt(); + prompt.push_str(&step_text); + + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: prompt, + }); + + let raw_output = cx + .update(|cx| CompletionProvider::global(cx).complete(request, cx))? + .await?; + + let operations = Self::parse_edit_operations(&raw_output); + this.update(&mut cx, |this, cx| { + let step_index = this + .edit_steps + .binary_search_by(|step| { + step.source_range + .cmp(&edit_step_range, this.buffer.read(cx)) + }) + .map_err(|_| anyhow!("edit step not found"))?; + if let Some(edit_step) = this.edit_steps.get_mut(step_index) { + edit_step.operations = Some(EditStepOperations::Parsed { + operations, + raw_output, + }); + cx.emit(ContextEvent::EditStepsChanged); + } + anyhow::Ok(()) + })? + }) + } + + fn parse_edit_operations(xml: &str) -> Vec { + let Some(start_ix) = xml.find("") else { + return Vec::new(); + }; + let Some(end_ix) = xml[start_ix..].find("") else { + return Vec::new(); + }; + let end_ix = end_ix + start_ix + "".len(); + + let doc = roxmltree::Document::parse(&xml[start_ix..end_ix]).log_err(); + doc.map_or(Vec::new(), |doc| { + doc.root_element() + .children() + .map(|node| { + let tag_name = node.tag_name().name(); + let path = node + .attribute("path") + .with_context(|| { + format!("invalid node {node:?}, missing attribute 'path'") + })? + .to_string(); + let kind = match tag_name { + "update" => EditOperationKind::Update { + symbol: node + .attribute("symbol") + .with_context(|| { + format!("invalid node {node:?}, missing attribute 'symbol'") + })? + .to_string(), + description: node + .attribute("description") + .with_context(|| { + format!( + "invalid node {node:?}, missing attribute 'description'" + ) + })? + .to_string(), + }, + "create" => EditOperationKind::Create { + description: node + .attribute("description") + .with_context(|| { + format!( + "invalid node {node:?}, missing attribute 'description'" + ) + })? + .to_string(), + }, + "insert_sibling_after" => EditOperationKind::InsertSiblingAfter { + symbol: node + .attribute("symbol") + .with_context(|| { + format!("invalid node {node:?}, missing attribute 'symbol'") + })? + .to_string(), + description: node + .attribute("description") + .with_context(|| { + format!( + "invalid node {node:?}, missing attribute 'description'" + ) + })? + .to_string(), + }, + "insert_sibling_before" => EditOperationKind::InsertSiblingBefore { + symbol: node + .attribute("symbol") + .with_context(|| { + format!("invalid node {node:?}, missing attribute 'symbol'") + })? + .to_string(), + description: node + .attribute("description") + .with_context(|| { + format!( + "invalid node {node:?}, missing attribute 'description'" + ) + })? + .to_string(), + }, + "prepend_child" => EditOperationKind::PrependChild { + symbol: node.attribute("symbol").map(String::from), + description: node + .attribute("description") + .with_context(|| { + format!( + "invalid node {node:?}, missing attribute 'description'" + ) + })? + .to_string(), + }, + "append_child" => EditOperationKind::AppendChild { + symbol: node.attribute("symbol").map(String::from), + description: node + .attribute("description") + .with_context(|| { + format!( + "invalid node {node:?}, missing attribute 'description'" + ) + })? + .to_string(), + }, + "delete" => EditOperationKind::Delete { + symbol: node + .attribute("symbol") + .with_context(|| { + format!("invalid node {node:?}, missing attribute 'symbol'") + })? + .to_string(), + }, + _ => return Err(anyhow!("invalid node {node:?}")), + }; + anyhow::Ok(EditOperation { path, kind }) + }) + .filter_map(|op| op.log_err()) + .collect() + }) + } + pub fn pending_command_for_position( &mut self, position: language::Anchor, @@ -1092,159 +1602,120 @@ impl Context { self.count_remaining_tokens(cx); } - pub fn assist( - &mut self, - selected_messages: HashSet, - cx: &mut ModelContext, - ) -> Vec { - let mut user_messages = Vec::new(); + pub fn assist(&mut self, cx: &mut ModelContext) -> Option { + let last_message_id = self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + })?; - let last_message_id = if let Some(last_message_id) = - self.message_anchors.iter().rev().find_map(|message| { - message - .start - .is_valid(self.buffer.read(cx)) - .then_some(message.id) - }) { - last_message_id - } else { - return Default::default(); - }; - - let mut should_assist = false; - for selected_message_id in selected_messages { - let selected_message_role = - if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { - metadata.role - } else { - continue; - }; - - if selected_message_role == Role::Assistant { - if let Some(user_message) = self.insert_message_after( - selected_message_id, - Role::User, - MessageStatus::Done, - cx, - ) { - user_messages.push(user_message); - } - } else { - should_assist = true; - } + if !CompletionProvider::global(cx).is_authenticated() { + log::info!("completion provider has no credentials"); + return None; } - if should_assist { - if !CompletionProvider::global(cx).is_authenticated() { - log::info!("completion provider has no credentials"); - return Default::default(); - } + let request = self.to_completion_request(cx); + let stream = CompletionProvider::global(cx).stream_completion(request, cx); + let assistant_message = self + .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) + .unwrap(); - let request = self.to_completion_request(cx); - let stream = CompletionProvider::global(cx).complete(request, cx); - let assistant_message = self - .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) - .unwrap(); + // Queue up the user's next reply. + let user_message = self + .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) + .unwrap(); - // Queue up the user's next reply. - let user_message = self - .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) - .unwrap(); - user_messages.push(user_message); + let task = cx.spawn({ + |this, mut cx| async move { + let assistant_message_id = assistant_message.id; + let mut response_latency = None; + let stream_completion = async { + let request_start = Instant::now(); + let mut chunks = stream.await?; - let task = cx.spawn({ - |this, mut cx| async move { - let assistant_message_id = assistant_message.id; - let mut response_latency = None; - let stream_completion = async { - let request_start = Instant::now(); - let mut messages = stream.await.inner.await?; - - while let Some(message) = messages.next().await { - if response_latency.is_none() { - response_latency = Some(request_start.elapsed()); - } - let text = message?; - - this.update(&mut cx, |this, cx| { - let message_ix = this - .message_anchors - .iter() - .position(|message| message.id == assistant_message_id)?; - let message_range = this.buffer.update(cx, |buffer, cx| { - let message_start_offset = - this.message_anchors[message_ix].start.to_offset(buffer); - let message_old_end_offset = this.message_anchors - [message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message.start.to_offset(buffer).saturating_sub(1) - }); - let message_new_end_offset = - message_old_end_offset + text.len(); - buffer.edit( - [(message_old_end_offset..message_old_end_offset, text)], - None, - cx, - ); - message_start_offset..message_new_end_offset - }); - this.reparse_edit_suggestions_in_range(message_range, cx); - cx.emit(ContextEvent::StreamedCompletion); - - Some(()) - })?; - smol::future::yield_now().await; + while let Some(chunk) = chunks.next().await { + if response_latency.is_none() { + response_latency = Some(request_start.elapsed()); } + let chunk = chunk?; this.update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); + let message_ix = this + .message_anchors + .iter() + .position(|message| message.id == assistant_message_id)?; + let message_range = this.buffer.update(cx, |buffer, cx| { + let message_start_offset = + this.message_anchors[message_ix].start.to_offset(buffer); + let message_old_end_offset = this.message_anchors[message_ix + 1..] + .iter() + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message.start.to_offset(buffer).saturating_sub(1) + }); + let message_new_end_offset = message_old_end_offset + chunk.len(); + buffer.edit( + [(message_old_end_offset..message_old_end_offset, chunk)], + None, + cx, + ); + message_start_offset..message_new_end_offset + }); + this.parse_edit_steps_in_range(message_range, cx); + cx.emit(ContextEvent::StreamedCompletion); + + Some(()) })?; - - anyhow::Ok(()) - }; - - let result = stream_completion.await; + smol::future::yield_now().await; + } this.update(&mut cx, |this, cx| { - let error_message = result - .err() - .map(|error| error.to_string().trim().to_string()); + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + })?; - this.update_metadata(assistant_message_id, cx, |metadata| { - if let Some(error_message) = error_message.as_ref() { - metadata.status = - MessageStatus::Error(SharedString::from(error_message.clone())); - } else { - metadata.status = MessageStatus::Done; - } - }); + anyhow::Ok(()) + }; - if let Some(telemetry) = this.telemetry.as_ref() { - let model = CompletionProvider::global(cx).model(); - telemetry.report_assistant_event( - Some(this.id.0.clone()), - AssistantKind::Panel, - model.telemetry_id(), - response_latency, - error_message, - ); + let result = stream_completion.await; + + this.update(&mut cx, |this, cx| { + let error_message = result + .err() + .map(|error| error.to_string().trim().to_string()); + + this.update_metadata(assistant_message_id, cx, |metadata| { + if let Some(error_message) = error_message.as_ref() { + metadata.status = + MessageStatus::Error(SharedString::from(error_message.clone())); + } else { + metadata.status = MessageStatus::Done; } - }) - .ok(); - } - }); + }); - self.pending_completions.push(PendingCompletion { - id: post_inc(&mut self.completion_count), - _task: task, - }); - } + if let Some(telemetry) = this.telemetry.as_ref() { + let model = CompletionProvider::global(cx).model(); + telemetry.report_assistant_event( + Some(this.id.0.clone()), + AssistantKind::Panel, + model.telemetry_id(), + response_latency, + error_message, + ); + } + }) + .ok(); + } + }); - user_messages + self.pending_completions.push(PendingCompletion { + id: post_inc(&mut self.completion_count), + _task: task, + }); + + Some(user_message) } pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest { @@ -1515,10 +1986,10 @@ impl Context { temperature: 1.0, }; - let stream = CompletionProvider::global(cx).complete(request, cx); + let stream = CompletionProvider::global(cx).stream_completion(request, cx); self.pending_summary = cx.spawn(|this, mut cx| { async move { - let mut messages = stream.await.inner.await?; + let mut messages = stream.await?; while let Some(message) = messages.next().await { let text = message?; @@ -1723,99 +2194,6 @@ impl ContextVersion { } } -#[derive(Debug)] -enum EditParsingState { - None, - InOldText { - path: PathBuf, - start_offset: usize, - old_text_start_offset: usize, - }, - InNewText { - path: PathBuf, - start_offset: usize, - old_text_range: Range, - new_text_start_offset: usize, - }, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct EditSuggestion { - pub source_range: Range, - pub full_path: PathBuf, -} - -pub struct ParsedEditSuggestion { - pub path: PathBuf, - pub outer_range: Range, - pub old_text_range: Range, - pub new_text_range: Range, -} - -pub fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option { - let mut state = EditParsingState::None; - loop { - let offset = lines.offset(); - let message_line = lines.next()?; - match state { - EditParsingState::None => { - if let Some(rest) = message_line.strip_prefix("```edit ") { - let path = rest.trim(); - if !path.is_empty() { - state = EditParsingState::InOldText { - path: PathBuf::from(path), - start_offset: offset, - old_text_start_offset: lines.offset(), - }; - } - } - } - EditParsingState::InOldText { - path, - start_offset, - old_text_start_offset, - } => { - if message_line == "---" { - state = EditParsingState::InNewText { - path, - start_offset, - old_text_range: old_text_start_offset..offset, - new_text_start_offset: lines.offset(), - }; - } else { - state = EditParsingState::InOldText { - path, - start_offset, - old_text_start_offset, - }; - } - } - EditParsingState::InNewText { - path, - start_offset, - old_text_range, - new_text_start_offset, - } => { - if message_line == "```" { - return Some(ParsedEditSuggestion { - path, - outer_range: start_offset..offset + "```".len(), - old_text_range, - new_text_range: new_text_start_offset..offset, - }); - } else { - state = EditParsingState::InNewText { - path, - start_offset, - old_text_range, - new_text_start_offset, - }; - } - } - } - } -} - #[derive(Clone)] pub struct PendingSlashCommand { pub name: String, @@ -2097,22 +2475,22 @@ pub struct SavedContextMetadata { mod tests { use super::*; use crate::{ - assistant_panel, + assistant_panel, prompt_library, slash_command::{active_command, file_command}, FakeCompletionProvider, MessageId, }; use assistant_slash_command::{ArgumentCompletion, SlashCommand}; use fs::FakeFs; use gpui::{AppContext, TestAppContext, WeakView}; + use indoc::indoc; use language::LspAdapterDelegate; use parking_lot::Mutex; use project::Project; use rand::prelude::*; - use rope::Rope; use serde_json::json; use settings::SettingsStore; - use std::{cell::RefCell, env, path::Path, rc::Rc, sync::atomic::AtomicBool}; - use text::network::Network; + use std::{cell::RefCell, env, rc::Rc, sync::atomic::AtomicBool}; + use text::{network::Network, ToPoint}; use ui::WindowContext; use unindent::Unindent; use util::{test::marked_text_ranges, RandomCharIter}; @@ -2551,72 +2929,151 @@ mod tests { } } - #[test] - fn test_parse_next_edit_suggestion() { - let text = " - some output: + #[gpui::test] + async fn test_edit_step_parsing(cx: &mut TestAppContext) { + cx.update(prompt_library::init); + let settings_store = cx.update(SettingsStore::test); + cx.set_global(settings_store); + let fake_provider = cx.update(FakeCompletionProvider::setup_test); + cx.update(assistant_panel::init); + let registry = Arc::new(LanguageRegistry::test(cx.executor())); - ```edit src/foo.rs - let a = 1; - let b = 2; - --- - let w = 1; - let x = 2; - let y = 3; - let z = 4; + // Create a new context + let context = cx.new_model(|cx| Context::local(registry.clone(), None, cx)); + let buffer = context.read_with(cx, |context, _| context.buffer.clone()); + + // Simulate user input + let user_message = indoc! {r#" + Please refactor this code: + + fn main() { + println!("Hello, World!"); + } + "#}; + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, user_message)], None, cx); + }); + + // Simulate LLM response with edit steps + let llm_response = indoc! {r#" + Sure, I can help you refactor that code. Here's a step-by-step process: + + + First, let's extract the greeting into a separate function: + + ```rust + fn greet() { + println!("Hello, World!"); + } + + fn main() { + greet(); + } ``` + - some more output: + + Now, let's make the greeting customizable: - ```edit src/foo.rs - let c = 1; - --- + ```rust + fn greet(name: &str) { + println!("Hello, {}!", name); + } + + fn main() { + greet("World"); + } ``` + - and the conclusion. - " - .unindent(); + These changes make the code more modular and flexible. + "#}; - let rope = Rope::from(text.as_str()); - let mut lines = rope.chunks().lines(); - let mut suggestions = vec![]; - while let Some(suggestion) = parse_next_edit_suggestion(&mut lines) { - suggestions.push(( - suggestion.path.clone(), - text[suggestion.old_text_range].to_string(), - text[suggestion.new_text_range].to_string(), - )); + // Simulate the assist method to trigger the LLM response + context.update(cx, |context, cx| context.assist(cx)); + cx.run_until_parked(); + + // Retrieve the assistant response message's start from the context + let response_start_row = context.read_with(cx, |context, cx| { + let buffer = context.buffer.read(cx); + context.message_anchors[1].start.to_point(buffer).row + }); + + // Simulate the LLM completion + fake_provider.send_last_completion_chunk(llm_response.to_string()); + fake_provider.finish_last_completion(); + + // Wait for the completion to be processed + cx.run_until_parked(); + + // Verify that the edit steps were parsed correctly + context.read_with(cx, |context, cx| { + assert_eq!( + edit_steps(context, cx), + vec![ + Point::new(response_start_row + 2, 0)..Point::new(response_start_row + 14, 7), + Point::new(response_start_row + 16, 0)..Point::new(response_start_row + 28, 7), + ] + ); + }); + + fn edit_steps(context: &Context, cx: &AppContext) -> Vec> { + context + .edit_steps + .iter() + .map(|step| { + let buffer = context.buffer.read(cx); + step.source_range.to_point(buffer) + }) + .collect() } + } + #[test] + fn test_parse_edit_operations() { + let operations = indoc! {r#" + Here are the operations to make all fields of the Canvas struct private: + + + + + + + + "#}; + + let parsed_operations = Context::parse_edit_operations(operations); assert_eq!( - suggestions, + parsed_operations, vec![ - ( - Path::new("src/foo.rs").into(), - [ - " let a = 1;", // - " let b = 2;", - "", - ] - .join("\n"), - [ - " let w = 1;", - " let x = 2;", - " let y = 3;", - " let z = 4;", - "", - ] - .join("\n"), - ), - ( - Path::new("src/foo.rs").into(), - [ - " let c = 1;", // - "", - ] - .join("\n"), - String::new(), - ) + EditOperation { + path: "font-kit/src/canvas.rs".to_string(), + kind: EditOperationKind::Update { + symbol: "pub struct Canvas pub pixels".to_string(), + description: "Remove pub keyword from pixels field".to_string(), + }, + }, + EditOperation { + path: "font-kit/src/canvas.rs".to_string(), + kind: EditOperationKind::Update { + symbol: "pub struct Canvas pub size".to_string(), + description: "Remove pub keyword from size field".to_string(), + }, + }, + EditOperation { + path: "font-kit/src/canvas.rs".to_string(), + kind: EditOperationKind::Update { + symbol: "pub struct Canvas pub stride".to_string(), + description: "Remove pub keyword from stride field".to_string(), + }, + }, + EditOperation { + path: "font-kit/src/canvas.rs".to_string(), + kind: EditOperationKind::Update { + symbol: "pub struct Canvas pub format".to_string(), + description: "Remove pub keyword from format field".to_string(), + }, + }, ] ); } diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 4ea5696ca7..11672fcb67 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -16,7 +16,12 @@ use editor::{ ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; -use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use futures::{ + channel::mpsc, + future::LocalBoxFuture, + stream::{self, BoxStream}, + SinkExt, Stream, StreamExt, +}; use gpui::{ point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView, @@ -28,8 +33,11 @@ use parking_lot::Mutex; use rope::Rope; use settings::{update_settings_file, Settings}; use similar::TextDiff; +use smol::future::FutureExt; use std::{ - cmp, mem, + cmp, + future::Future, + mem, ops::{Range, RangeInclusive}, pin::Pin, sync::Arc, @@ -134,7 +142,6 @@ impl InlineAssistant { let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx)); let mut assists = Vec::new(); - let mut assist_blocks = Vec::new(); let mut assist_to_focus = None; for range in codegen_ranges { let assist_id = self.next_assist_id.post_inc(); @@ -142,6 +149,7 @@ impl InlineAssistant { Codegen::new( editor.read(cx).buffer().clone(), range.clone(), + None, self.telemetry.clone(), cx, ) @@ -174,42 +182,18 @@ impl InlineAssistant { } } - assist_blocks.push(BlockProperties { - style: BlockStyle::Sticky, - position: range.start, - height: prompt_editor.read(cx).height_in_lines, - render: build_assist_editor_renderer(&prompt_editor), - disposition: BlockDisposition::Above, - }); - assist_blocks.push(BlockProperties { - style: BlockStyle::Sticky, - position: range.end, - 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, - }); - assists.push((assist_id, prompt_editor)); - } + let [prompt_block_id, end_block_id] = + self.insert_assist_blocks(editor, &range, &prompt_editor, cx); - let assist_block_ids = editor.update(cx, |editor, cx| { - editor.insert_blocks(assist_blocks, None, cx) - }); + assists.push((assist_id, prompt_editor, prompt_block_id, end_block_id)); + } let editor_assists = self .assists_by_editor .entry(editor.downgrade()) .or_insert_with(|| EditorInlineAssists::new(&editor, cx)); let mut assist_group = InlineAssistGroup::new(); - for ((assist_id, prompt_editor), block_ids) in - assists.into_iter().zip(assist_block_ids.chunks_exact(2)) - { + for (assist_id, prompt_editor, prompt_block_id, end_block_id) in assists { self.assists.insert( assist_id, InlineAssist::new( @@ -218,8 +202,8 @@ impl InlineAssistant { assistant_panel.is_some(), editor, &prompt_editor, - block_ids[0], - block_ids[1], + prompt_block_id, + end_block_id, prompt_editor.read(cx).codegen.clone(), workspace.clone(), cx, @@ -235,6 +219,128 @@ impl InlineAssistant { } } + #[allow(clippy::too_many_arguments)] + pub fn suggest_assist( + &mut self, + editor: &View, + mut range: Range, + initial_prompt: String, + initial_insertion: Option, + workspace: Option>, + assistant_panel: Option<&View>, + cx: &mut WindowContext, + ) -> InlineAssistId { + let assist_group_id = self.next_assist_group_id.post_inc(); + let prompt_buffer = cx.new_model(|cx| Buffer::local(&initial_prompt, cx)); + let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx)); + + let assist_id = self.next_assist_id.post_inc(); + + let buffer = editor.read(cx).buffer().clone(); + let prepend_transaction_id = initial_insertion.and_then(|initial_insertion| { + buffer.update(cx, |buffer, cx| { + buffer.start_transaction(cx); + buffer.edit([(range.start..range.start, initial_insertion)], None, cx); + buffer.end_transaction(cx) + }) + }); + + range.start = range.start.bias_left(&buffer.read(cx).read(cx)); + range.end = range.end.bias_right(&buffer.read(cx).read(cx)); + + let codegen = cx.new_model(|cx| { + Codegen::new( + editor.read(cx).buffer().clone(), + range.clone(), + prepend_transaction_id, + self.telemetry.clone(), + cx, + ) + }); + + let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); + let prompt_editor = cx.new_view(|cx| { + PromptEditor::new( + assist_id, + gutter_dimensions.clone(), + self.prompt_history.clone(), + prompt_buffer.clone(), + codegen.clone(), + editor, + assistant_panel, + workspace.clone(), + self.fs.clone(), + cx, + ) + }); + + let [prompt_block_id, end_block_id] = + self.insert_assist_blocks(editor, &range, &prompt_editor, cx); + + let editor_assists = self + .assists_by_editor + .entry(editor.downgrade()) + .or_insert_with(|| EditorInlineAssists::new(&editor, cx)); + + let mut assist_group = InlineAssistGroup::new(); + self.assists.insert( + assist_id, + InlineAssist::new( + assist_id, + assist_group_id, + assistant_panel.is_some(), + editor, + &prompt_editor, + prompt_block_id, + end_block_id, + prompt_editor.read(cx).codegen.clone(), + workspace.clone(), + cx, + ), + ); + assist_group.assist_ids.push(assist_id); + editor_assists.assist_ids.push(assist_id); + self.assist_groups.insert(assist_group_id, assist_group); + assist_id + } + + fn insert_assist_blocks( + &self, + editor: &View, + range: &Range, + prompt_editor: &View, + cx: &mut WindowContext, + ) -> [BlockId; 2] { + let assist_blocks = vec![ + BlockProperties { + style: BlockStyle::Sticky, + position: range.start, + height: prompt_editor.read(cx).height_in_lines, + render: build_assist_editor_renderer(prompt_editor), + disposition: BlockDisposition::Above, + }, + BlockProperties { + style: BlockStyle::Sticky, + position: range.end, + 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, + }, + ]; + + editor.update(cx, |editor, cx| { + let block_ids = editor.insert_blocks(assist_blocks, None, cx); + [block_ids[0], block_ids[1]] + }) + } + fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { let assist = &self.assists[&assist_id]; let Some(decorations) = assist.decorations.as_ref() else { @@ -379,6 +485,14 @@ impl InlineAssistant { cx.propagate(); } + fn handle_editor_release(&mut self, editor: WeakView, cx: &mut WindowContext) { + if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) { + for assist_id in editor_assists.assist_ids.clone() { + self.finish_assist(assist_id, true, cx); + } + } + } + fn handle_editor_change(&mut self, editor: View, cx: &mut WindowContext) { let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else { return; @@ -698,7 +812,7 @@ impl InlineAssistant { assist_group.assist_ids.clone() } - fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -727,16 +841,26 @@ impl InlineAssistant { self.prompt_history.pop_front(); } - assist.codegen.update(cx, |codegen, cx| codegen.undo(cx)); let codegen = assist.codegen.clone(); - let request = self.request_for_inline_assist(assist_id, cx); - - cx.spawn(|mut cx| async move { - let request = request.await?; - codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let telemetry_id = CompletionProvider::global(cx).model().telemetry_id(); + let chunks: LocalBoxFuture>>> = + if user_prompt.trim().to_lowercase() == "delete" { + async { Ok(stream::empty().boxed()) }.boxed_local() + } else { + let request = self.request_for_inline_assist(assist_id, cx); + let mut cx = cx.to_async(); + async move { + let request = request.await?; + let chunks = cx + .update(|cx| CompletionProvider::global(cx).stream_completion(request, cx))? + .await?; + Ok(chunks.boxed()) + } + .boxed_local() + }; + codegen.update(cx, |codegen, cx| { + codegen.start(telemetry_id, chunks, cx); + }); } fn request_for_inline_assist( @@ -855,7 +979,7 @@ impl InlineAssistant { }) } - fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -1074,6 +1198,14 @@ impl EditorInlineAssists { } }), _subscriptions: vec![ + cx.observe_release(editor, { + let editor = editor.downgrade(); + |_, cx| { + InlineAssistant::update_global(cx, |this, cx| { + this.handle_editor_release(editor, cx); + }) + } + }), cx.observe(editor, move |editor, cx| { InlineAssistant::update_global(cx, |this, cx| { this.handle_editor_change(editor, cx) @@ -1138,7 +1270,7 @@ fn build_assist_editor_renderer(editor: &View) -> RenderBlock { } #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)] -struct InlineAssistId(usize); +pub struct InlineAssistId(usize); impl InlineAssistId { fn post_inc(&mut self) -> InlineAssistId { @@ -1882,7 +2014,8 @@ pub struct Codegen { range: Range, edit_position: Anchor, last_equal_ranges: Vec>, - transaction_id: Option, + prepend_transaction_id: Option, + generation_transaction_id: Option, status: CodegenStatus, generation: Task<()>, diff: Diff, @@ -1911,6 +2044,7 @@ impl Codegen { pub fn new( buffer: Model, range: Range, + prepend_transaction_id: Option, telemetry: Option>, cx: &mut ModelContext, ) -> Self { @@ -1943,7 +2077,8 @@ impl Codegen { range, snapshot, last_equal_ranges: Default::default(), - transaction_id: Default::default(), + prepend_transaction_id, + generation_transaction_id: None, status: CodegenStatus::Idle, generation: Task::ready(()), diff: Diff::default(), @@ -1959,8 +2094,13 @@ impl Codegen { cx: &mut ModelContext, ) { if let multi_buffer::Event::TransactionUndone { transaction_id } = event { - if self.transaction_id == Some(*transaction_id) { - self.transaction_id = None; + if self.generation_transaction_id == Some(*transaction_id) { + self.generation_transaction_id = None; + self.generation = Task::ready(()); + cx.emit(CodegenEvent::Undone); + } else if self.prepend_transaction_id == Some(*transaction_id) { + self.prepend_transaction_id = None; + self.generation_transaction_id = None; self.generation = Task::ready(()); cx.emit(CodegenEvent::Undone); } @@ -1971,7 +2111,12 @@ impl Codegen { &self.last_equal_ranges } - pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext) { + pub fn start( + &mut self, + telemetry_id: String, + stream: impl 'static + Future>>>, + cx: &mut ModelContext, + ) { let range = self.range.clone(); let snapshot = self.snapshot.clone(); let selected_text = snapshot @@ -1985,15 +2130,17 @@ impl Codegen { .next() .unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row))); - let model_telemetry_id = prompt.model.telemetry_id(); - let response = CompletionProvider::global(cx).complete(prompt, cx); let telemetry = self.telemetry.clone(); self.edit_position = range.start; self.diff = Diff::default(); self.status = CodegenStatus::Pending; + if let Some(transaction_id) = self.generation_transaction_id.take() { + self.buffer + .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } self.generation = cx.spawn(|this, mut cx| { async move { - let response = response.await; + let chunks = stream.await; let generate = async { let mut edit_start = range.start.to_offset(&snapshot); @@ -2003,7 +2150,7 @@ impl Codegen { let mut response_latency = None; let request_start = Instant::now(); let diff = async { - let chunks = StripInvalidSpans::new(response.inner.await?); + let chunks = StripInvalidSpans::new(chunks?); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -2086,7 +2233,7 @@ impl Codegen { telemetry.report_assistant_event( None, telemetry_events::AssistantKind::Inline, - model_telemetry_id, + telemetry_id, response_latency, error_message, ); @@ -2136,7 +2283,7 @@ impl Codegen { }); if let Some(transaction) = transaction { - if let Some(first_transaction) = this.transaction_id { + if let Some(first_transaction) = this.generation_transaction_id { // Group all assistant edits into the first transaction. this.buffer.update(cx, |buffer, cx| { buffer.merge_transactions( @@ -2146,7 +2293,7 @@ impl Codegen { ) }); } else { - this.transaction_id = Some(transaction); + this.generation_transaction_id = Some(transaction); this.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx) }); @@ -2189,7 +2336,12 @@ impl Codegen { } pub fn undo(&mut self, cx: &mut ModelContext) { - if let Some(transaction_id) = self.transaction_id.take() { + if let Some(transaction_id) = self.prepend_transaction_id.take() { + self.buffer + .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } + + if let Some(transaction_id) = self.generation_transaction_id.take() { self.buffer .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); } @@ -2451,11 +2603,8 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { #[cfg(test)] mod tests { - use std::sync::Arc; - - use crate::FakeCompletionProvider; - use super::*; + use crate::FakeCompletionProvider; use futures::stream::{self}; use gpui::{Context, TestAppContext}; use indoc::indoc; @@ -2466,6 +2615,7 @@ mod tests { use rand::prelude::*; use serde::Serialize; use settings::SettingsStore; + use std::{future, sync::Arc}; #[derive(Serialize)] pub struct DummyCompletionRequest { @@ -2475,7 +2625,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { cx.set_global(cx.update(SettingsStore::test)); - let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx)); + cx.update(|cx| FakeCompletionProvider::setup_test(cx)); cx.update(language_settings::init); let text = indoc! {" @@ -2493,14 +2643,17 @@ mod tests { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); - let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx)); + let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, None, cx)); + let (chunks_tx, chunks_rx) = mpsc::unbounded(); codegen.update(cx, |codegen, cx| { - codegen.start(LanguageModelRequest::default(), cx) + codegen.start( + String::new(), + future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())), + cx, + ) }); - cx.background_executor.run_until_parked(); - let mut new_text = concat!( " let mut x = 0;\n", " while x < 10 {\n", @@ -2511,11 +2664,11 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); - provider.send_completion(&LanguageModelRequest::default(), chunk.into()); + chunks_tx.unbounded_send(chunk.to_string()).unwrap(); new_text = suffix; cx.background_executor.run_until_parked(); } - provider.finish_completion(&LanguageModelRequest::default()); + drop(chunks_tx); cx.background_executor.run_until_parked(); assert_eq!( @@ -2536,7 +2689,6 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx)); cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); @@ -2552,10 +2704,16 @@ mod tests { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6)) }); - let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx)); + let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, None, cx)); - let request = LanguageModelRequest::default(); - codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + let (chunks_tx, chunks_rx) = mpsc::unbounded(); + codegen.update(cx, |codegen, cx| { + codegen.start( + String::new(), + future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())), + cx, + ) + }); cx.background_executor.run_until_parked(); @@ -2569,11 +2727,11 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); - provider.send_completion(&LanguageModelRequest::default(), chunk.into()); + chunks_tx.unbounded_send(chunk.to_string()).unwrap(); new_text = suffix; cx.background_executor.run_until_parked(); } - provider.finish_completion(&LanguageModelRequest::default()); + drop(chunks_tx); cx.background_executor.run_until_parked(); assert_eq!( @@ -2594,7 +2752,7 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx)); + cx.update(|cx| FakeCompletionProvider::setup_test(cx)); cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); @@ -2610,10 +2768,16 @@ mod tests { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2)) }); - let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx)); + let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, None, cx)); - let request = LanguageModelRequest::default(); - codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + let (chunks_tx, chunks_rx) = mpsc::unbounded(); + codegen.update(cx, |codegen, cx| { + codegen.start( + String::new(), + future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())), + cx, + ) + }); cx.background_executor.run_until_parked(); @@ -2627,11 +2791,11 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); - provider.send_completion(&LanguageModelRequest::default(), chunk.into()); + chunks_tx.unbounded_send(chunk.to_string()).unwrap(); new_text = suffix; cx.background_executor.run_until_parked(); } - provider.finish_completion(&LanguageModelRequest::default()); + drop(chunks_tx); cx.background_executor.run_until_parked(); assert_eq!( diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 515909983b..d09bc7995b 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -3,6 +3,7 @@ use crate::{ InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role, }; use anyhow::{anyhow, Result}; +use assets::Assets; use chrono::{DateTime, Utc}; use collections::{HashMap, HashSet}; use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle}; @@ -12,8 +13,8 @@ use futures::{ }; use fuzzy::StringMatchCandidate; use gpui::{ - actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter, - Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, + actions, point, size, transparent_black, AppContext, AssetSource, BackgroundExecutor, Bounds, + EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions, }; use heed::{types::SerdeBincode, Database, RoTxn}; @@ -1296,6 +1297,17 @@ impl PromptStore { fn first(&self) -> Option { self.metadata_cache.read().metadata.first().cloned() } + + pub fn operations_prompt(&self) -> String { + String::from_utf8( + Assets + .load("prompts/operations.md") + .unwrap() + .unwrap() + .to_vec(), + ) + .unwrap() + } } /// Wraps a shared future to a prompt store so it can be assigned as a context global. diff --git a/crates/assistant/src/search.rs b/crates/assistant/src/search.rs deleted file mode 100644 index 7e8b18ae50..0000000000 --- a/crates/assistant/src/search.rs +++ /dev/null @@ -1,171 +0,0 @@ -use language::Rope; -use std::ops::Range; - -/// Search the given buffer for the given substring, ignoring any differences -/// in line indentation between the query and the buffer. -/// -/// Returns a vector of ranges of byte offsets in the buffer corresponding -/// to the entire lines of the buffer. -pub fn fuzzy_search_lines(haystack: &Rope, needle: &str) -> Option> { - const SIMILARITY_THRESHOLD: f64 = 0.8; - - let mut best_match: Option<(Range, f64)> = None; // (range, score) - let mut haystack_lines = haystack.chunks().lines(); - let mut haystack_line_start = 0; - while let Some(mut haystack_line) = haystack_lines.next() { - let next_haystack_line_start = haystack_line_start + haystack_line.len() + 1; - let mut advanced_to_next_haystack_line = false; - - let mut matched = true; - let match_start = haystack_line_start; - let mut match_end = next_haystack_line_start; - let mut match_score = 0.0; - let mut needle_lines = needle.lines().peekable(); - while let Some(needle_line) = needle_lines.next() { - let similarity = line_similarity(haystack_line, needle_line); - if similarity >= SIMILARITY_THRESHOLD { - match_end = haystack_lines.offset(); - match_score += similarity; - - if needle_lines.peek().is_some() { - if let Some(next_haystack_line) = haystack_lines.next() { - advanced_to_next_haystack_line = true; - haystack_line = next_haystack_line; - } else { - matched = false; - break; - } - } else { - break; - } - } else { - matched = false; - break; - } - } - - if matched - && best_match - .as_ref() - .map(|(_, best_score)| match_score > *best_score) - .unwrap_or(true) - { - best_match = Some((match_start..match_end, match_score)); - } - - if advanced_to_next_haystack_line { - haystack_lines.seek(next_haystack_line_start); - } - haystack_line_start = next_haystack_line_start; - } - - best_match.map(|(range, _)| range) -} - -/// Calculates the similarity between two lines, ignoring leading and trailing whitespace, -/// using the Jaro-Winkler distance. -/// -/// Returns a value between 0.0 and 1.0, where 1.0 indicates an exact match. -fn line_similarity(line1: &str, line2: &str) -> f64 { - strsim::jaro_winkler(line1.trim(), line2.trim()) -} - -#[cfg(test)] -mod test { - use super::*; - use gpui::{AppContext, Context as _}; - use language::Buffer; - use unindent::Unindent as _; - use util::test::marked_text_ranges; - - #[gpui::test] - fn test_fuzzy_search_lines(cx: &mut AppContext) { - let (text, expected_ranges) = marked_text_ranges( - &r#" - fn main() { - if a() { - assert_eq!( - 1 + 2, - does_not_match, - ); - } - - println!("hi"); - - assert_eq!( - 1 + 2, - 3, - ); // this last line does not match - - « assert_eq!( - 1 + 2, - 3, - ); - » - - « assert_eq!( - "something", - "else", - ); - » - } - "# - .unindent(), - false, - ); - - let buffer = cx.new_model(|cx| Buffer::local(&text, cx)); - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - let actual_range = fuzzy_search_lines( - snapshot.as_rope(), - &" - assert_eq!( - 1 + 2, - 3, - ); - " - .unindent(), - ) - .unwrap(); - assert_eq!(actual_range, expected_ranges[0]); - - let actual_range = fuzzy_search_lines( - snapshot.as_rope(), - &" - assert_eq!( - 1 + 2, - 3, - ); - " - .unindent(), - ) - .unwrap(); - assert_eq!(actual_range, expected_ranges[0]); - - let actual_range = fuzzy_search_lines( - snapshot.as_rope(), - &" - asst_eq!( - \"something\", - \"els\" - ) - " - .unindent(), - ) - .unwrap(); - assert_eq!(actual_range, expected_ranges[1]); - - let actual_range = fuzzy_search_lines( - snapshot.as_rope(), - &" - assert_eq!( - 2 + 1, - 3, - ); - " - .unindent(), - ); - assert_eq!(actual_range, None); - } -} diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index ac0bb9af91..8715c46902 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -1026,7 +1026,7 @@ impl Codegen { let telemetry = self.telemetry.clone(); let model_telemetry_id = prompt.model.telemetry_id(); - let response = CompletionProvider::global(cx).complete(prompt, cx); + let response = CompletionProvider::global(cx).stream_completion(prompt, cx); self.generation = cx.spawn(|this, mut cx| async move { let response = response.await; @@ -1037,8 +1037,8 @@ impl Codegen { let mut response_latency = None; let request_start = Instant::now(); let task = async { - let mut response = response.inner.await?; - while let Some(chunk) = response.next().await { + let mut chunks = response?; + while let Some(chunk) = chunks.next().await { if response_latency.is_none() { response_latency = Some(request_start.elapsed()); } diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 1bc9438952..ee78ea3556 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -288,7 +288,12 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) { if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, cx); + workspace.activate_item(&existing, true, true, cx); } else { let workspace_handle = cx.view().downgrade(); let diagnostics = cx.new_view(|cx| { ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) }); - workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx); + workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx); } } diff --git a/crates/diagnostics/src/grouped_diagnostics.rs b/crates/diagnostics/src/grouped_diagnostics.rs index 92c4a7be32..99c6651399 100644 --- a/crates/diagnostics/src/grouped_diagnostics.rs +++ b/crates/diagnostics/src/grouped_diagnostics.rs @@ -250,13 +250,13 @@ impl GroupedDiagnosticsEditor { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, cx); + workspace.activate_item(&existing, true, true, cx); } else { let workspace_handle = cx.view().downgrade(); let diagnostics = cx.new_view(|cx| { GroupedDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) }); - workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx); + workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 42f9ca5782..d6bfa95b86 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1992,28 +1992,35 @@ impl Editor { _: &workspace::NewFile, cx: &mut ViewContext, ) { + Self::new_in_workspace(workspace, cx).detach_and_prompt_err( + "Failed to create buffer", + cx, + |e, _| match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( + "The remote instance of Zed does not support this yet. It must be upgraded to {}", + e.error_tag("required").unwrap_or("the latest version") + )), + _ => None, + }, + ); + } + + pub fn new_in_workspace( + workspace: &mut Workspace, + cx: &mut ViewContext, + ) -> Task>> { let project = workspace.project().clone(); let create = project.update(cx, |project, cx| project.create_buffer(cx)); cx.spawn(|workspace, mut cx| async move { let buffer = create.await?; workspace.update(&mut cx, |workspace, cx| { - workspace.add_item_to_active_pane( - Box::new( - cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), - ), - None, - cx, - ) + let editor = + cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx); + editor }) }) - .detach_and_prompt_err("Failed to create buffer", cx, |e, _| match e.error_code() { - ErrorCode::RemoteUpgradeRequired => Some(format!( - "The remote instance of Zed does not support this yet. It must be upgraded to {}", - e.error_tag("required").unwrap_or("the latest version") - )), - _ => None, - }); } pub fn new_file_in_direction( @@ -4658,7 +4665,7 @@ impl Editor { let project = workspace.project().clone(); let editor = cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx); editor.update(cx, |editor, cx| { editor.highlight_background::( &ranges_to_highlight, @@ -9093,7 +9100,13 @@ impl Editor { workspace.active_pane().clone() }; - workspace.open_project_item(pane, target.buffer.clone(), cx) + workspace.open_project_item( + pane, + target.buffer.clone(), + true, + true, + cx, + ) }); target_editor.update(cx, |target_editor, cx| { // When selecting a definition in a different buffer, disable the nav history @@ -9391,7 +9404,7 @@ impl Editor { None } }); - workspace.add_item_to_active_pane(item.clone(), destination_index, cx); + workspace.add_item_to_active_pane(item.clone(), destination_index, true, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -11342,7 +11355,8 @@ impl Editor { }; for (buffer, ranges) in new_selections_by_buffer { - let editor = workspace.open_project_item::(pane.clone(), buffer, cx); + let editor = + workspace.open_project_item::(pane.clone(), buffer, true, true, cx); editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::newest()), cx, |s| { s.select_ranges(ranges); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e0b910f3b6..f77738f876 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10445,7 +10445,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { workspace.active_item(cx).is_none(), "active item should be None before the first item is added" ); - workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), None, cx); + workspace.add_item_to_active_pane( + Box::new(multi_buffer_editor.clone()), + None, + true, + cx, + ); let active_item = workspace .active_item(cx) .expect("should have an active item after adding the multi buffer"); diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 8fe8f48668..9a4dfbdf2a 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -113,6 +113,7 @@ pub fn expand_macro_recursively( cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx)), ), None, + true, cx, ); }) diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index d96cf3c744..b04d0fb84e 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -48,10 +48,10 @@ pub fn init(cx: &mut AppContext) { .find_map(|item| item.downcast::()); if let Some(existing) = existing { - workspace.activate_item(&existing, cx); + workspace.activate_item(&existing, true, true, cx); } else { let extensions_page = ExtensionsPage::new(workspace, cx); - workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx) + workspace.add_item_to_active_pane(Box::new(extensions_page), None, true, cx) } }) .register_action(move |workspace, _: &InstallDevExtension, cx| { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7665940513..f8693f8baf 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1161,6 +1161,29 @@ impl<'a> WindowContext<'a> { ) } + /// Register a callback to be invoked when the given Model or View is released. + pub fn observe_release( + &mut self, + entity: &E, + mut on_release: impl FnOnce(&mut T, &mut WindowContext) + 'static, + ) -> Subscription + where + E: Entity, + T: 'static, + { + let entity_id = entity.entity_id(); + let window_handle = self.window.handle; + let (subscription, activate) = self.app.release_listeners.insert( + entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + let _ = window_handle.update(cx, |_, cx| on_release(entity, cx)); + }), + ); + activate(); + subscription + } + /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across /// await points in async code. pub fn to_async(&self) -> AsyncWindowContext { diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index af2d90421f..a9b9394d18 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -27,6 +27,7 @@ test-support = [ [dependencies] anyhow.workspace = true async-trait.workspace = true +async-watch.workspace = true clock.workspace = true collections.workspace = true futures.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index efa0a09b09..729dcb569a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -17,6 +17,7 @@ use crate::{ LanguageScope, Outline, RunnableCapture, RunnableTag, }; use anyhow::{anyhow, Context, Result}; +use async_watch as watch; pub use clock::ReplicaId; use futures::channel::oneshot; use gpui::{ @@ -32,7 +33,7 @@ use smol::future::yield_now; use std::{ any::Any, cell::Cell, - cmp::{self, Ordering}, + cmp::{self, Ordering, Reverse}, collections::BTreeMap, ffi::OsStr, fmt, @@ -104,6 +105,7 @@ pub struct Buffer { sync_parse_timeout: Duration, syntax_map: Mutex, parsing_in_background: bool, + parse_status: (watch::Sender, watch::Receiver), non_text_state_update_count: usize, diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>, remote_selections: TreeMap, @@ -119,6 +121,12 @@ pub struct Buffer { has_unsaved_edits: Cell<(clock::Global, bool)>, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ParseStatus { + Idle, + Parsing, +} + /// An immutable, cheaply cloneable representation of a fixed /// state of a buffer. pub struct BufferSnapshot { @@ -710,6 +718,7 @@ impl Buffer { parsing_in_background: false, non_text_state_update_count: 0, sync_parse_timeout: Duration::from_millis(1), + parse_status: async_watch::channel(ParseStatus::Idle), autoindent_requests: Default::default(), pending_autoindent: Default::default(), language: None, @@ -1059,6 +1068,7 @@ impl Buffer { } }); + self.parse_status.0.send(ParseStatus::Parsing).unwrap(); match cx .background_executor() .block_with_timeout(self.sync_parse_timeout, parse_task) @@ -1101,10 +1111,15 @@ impl Buffer { self.non_text_state_update_count += 1; self.syntax_map.lock().did_parse(syntax_snapshot); self.request_autoindent(cx); + self.parse_status.0.send(ParseStatus::Idle).unwrap(); cx.emit(Event::Reparsed); cx.notify(); } + pub fn parse_status(&self) -> watch::Receiver { + self.parse_status.1.clone() + } + /// Assign to the buffer a set of diagnostics created by a given language server. pub fn update_diagnostics( &mut self, @@ -2749,7 +2764,6 @@ impl BufferSnapshot { .map(|g| g.outline_config.as_ref().unwrap()) .collect::>(); - let mut stack = Vec::>::new(); let mut items = Vec::new(); while let Some(mat) = matches.peek() { let config = &configs[mat.grammar_index]; @@ -2767,6 +2781,9 @@ impl BufferSnapshot { continue; } + let mut open_index = None; + let mut close_index = None; + let mut buffer_ranges = Vec::new(); for capture in mat.captures { let node_is_name; @@ -2778,6 +2795,12 @@ impl BufferSnapshot { { node_is_name = false; } else { + if Some(capture.index) == config.open_capture_ix { + open_index = Some(capture.node.end_byte()); + } else if Some(capture.index) == config.close_capture_ix { + close_index = Some(capture.node.start_byte()); + } + continue; } @@ -2850,22 +2873,45 @@ impl BufferSnapshot { } matches.advance(); - while stack.last().map_or(false, |prev_range| { - prev_range.start > item_range.start || prev_range.end < item_range.end - }) { - stack.pop(); - } - stack.push(item_range.clone()); items.push(OutlineItem { - depth: stack.len() - 1, - range: self.anchor_after(item_range.start)..self.anchor_before(item_range.end), + depth: 0, // We'll calculate the depth later + range: item_range, text, highlight_ranges, name_ranges, - }) + body_range: open_index.zip(close_index).map(|(start, end)| start..end), + }); } - Some(items) + + items.sort_by_key(|item| (item.range.start, Reverse(item.range.end))); + + // Assign depths based on containment relationships and convert to anchors. + let mut item_ends_stack = Vec::::new(); + let mut anchor_items = Vec::new(); + for item in items { + while let Some(last_end) = item_ends_stack.last().copied() { + if last_end < item.range.end { + item_ends_stack.pop(); + } else { + break; + } + } + + anchor_items.push(OutlineItem { + depth: item_ends_stack.len(), + range: self.anchor_after(item.range.start)..self.anchor_before(item.range.end), + text: item.text, + highlight_ranges: item.highlight_ranges, + name_ranges: item.name_ranges, + body_range: item.body_range.map(|body_range| { + self.anchor_after(body_range.start)..self.anchor_before(body_range.end) + }), + }); + item_ends_stack.push(item.range.end); + } + + Some(anchor_items) } /// For each grammar in the language, runs the provided diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 2c50e6dc9e..8e7356057b 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -2615,7 +2615,8 @@ fn rust_lang() -> Language { "impl" @context trait: (_)? @name "for"? @context - type: (_) @name) @item + type: (_) @name + body: (_ "{" (_)* "}")) @item (function_item "fn" @context name: (_) @name) @item diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 19a0d97248..7b2e4916f3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -867,6 +867,8 @@ pub struct OutlineConfig { pub name_capture_ix: u32, pub context_capture_ix: Option, pub extra_context_capture_ix: Option, + pub open_capture_ix: Option, + pub close_capture_ix: Option, } #[derive(Debug)] @@ -1050,6 +1052,8 @@ impl Language { let mut name_capture_ix = None; let mut context_capture_ix = None; let mut extra_context_capture_ix = None; + let mut open_capture_ix = None; + let mut close_capture_ix = None; get_capture_indices( &query, &mut [ @@ -1057,6 +1061,8 @@ impl Language { ("name", &mut name_capture_ix), ("context", &mut context_capture_ix), ("context.extra", &mut extra_context_capture_ix), + ("open", &mut open_capture_ix), + ("close", &mut close_capture_ix), ], ); if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) { @@ -1066,6 +1072,8 @@ impl Language { name_capture_ix, context_capture_ix, extra_context_capture_ix, + open_capture_ix, + close_capture_ix, }); } Ok(self) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 53954a9fc8..af7f97fe98 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -23,6 +23,7 @@ pub struct OutlineItem { pub text: String, pub highlight_ranges: Vec<(Range, HighlightStyle)>, pub name_ranges: Vec>, + pub body_range: Option>, } impl Outline { diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 6bd4c2ca4f..274a9b7f51 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -115,6 +115,7 @@ pub fn init(cx: &mut AppContext) { LspLogView::new(workspace.project().clone(), log_store.clone(), cx) })), None, + true, cx, ); } diff --git a/crates/languages/src/rust/outline.scm b/crates/languages/src/rust/outline.scm index 5c89087ac0..8b53c21fdf 100644 --- a/crates/languages/src/rust/outline.scm +++ b/crates/languages/src/rust/outline.scm @@ -16,7 +16,8 @@ "impl" @context trait: (_)? @name "for"? @context - type: (_) @name) @item + type: (_) @name + body: (_ "{" @open (_)* "}" @close)) @item (trait_item (visibility_modifier)? @context diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a9ae9202db..3f12b3b44b 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -3639,6 +3639,12 @@ impl MultiBufferSnapshot { text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, + body_range: item.body_range.and_then(|body_range| { + Some( + self.anchor_in_excerpt(*excerpt_id, body_range.start)? + ..self.anchor_in_excerpt(*excerpt_id, body_range.end)?, + ) + }), }) }) .collect(), @@ -3668,6 +3674,12 @@ impl MultiBufferSnapshot { text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, + body_range: item.body_range.and_then(|body_range| { + Some( + self.anchor_in_excerpt(excerpt_id, body_range.start)? + ..self.anchor_in_excerpt(excerpt_id, body_range.end)?, + ) + }), }) }) .collect(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3022a2f097..7c03653412 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8403,6 +8403,37 @@ impl Project { }) } + /// Attempts to find a `ProjectPath` corresponding to the given full path. + /// + /// This method iterates through all worktrees in the project, trying to match + /// the given full path against each worktree's root name. If a match is found, + /// it returns a `ProjectPath` containing the worktree ID and the relative path + /// within that worktree. + /// + /// # Arguments + /// + /// * `full_path` - A reference to a `Path` representing the full path to resolve. + /// * `cx` - A reference to the `AppContext`. + /// + /// # Returns + /// + /// Returns `Some(ProjectPath)` if a matching worktree is found, otherwise `None`. + pub fn project_path_for_full_path( + &self, + full_path: &Path, + cx: &AppContext, + ) -> Option { + self.worktrees.iter().find_map(|worktree| { + let worktree = worktree.upgrade()?; + let worktree_root_name = worktree.read(cx).root_name(); + let relative_path = full_path.strip_prefix(worktree_root_name).ok()?; + Some(ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path.into(), + }) + }) + } + pub fn get_workspace_root( &self, project_path: &ProjectPath, diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 75b02e6826..0cb7ef6c71 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -131,7 +131,8 @@ impl PickerDelegate for ProjectSymbolsDelegate { workspace.active_pane().clone() }; - let editor = workspace.open_project_item::(pane, buffer, cx); + let editor = + workspace.open_project_item::(pane, buffer, true, true, cx); editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::center()), cx, |s| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 708916d2f9..e3b6d51610 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -716,7 +716,7 @@ impl ProjectSearchView { let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, None)); - workspace.add_item_to_active_pane(Box::new(search.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(search.clone()), None, true, cx); search.update(cx, |search, cx| { search .included_files_editor @@ -768,6 +768,7 @@ impl ProjectSearchView { workspace.add_item_to_active_pane( Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))), None, + true, cx, ); } @@ -800,7 +801,7 @@ impl ProjectSearchView { }); let search = if let Some(existing) = existing { - workspace.activate_item(&existing, cx); + workspace.activate_item(&existing, true, true, cx); existing } else { let settings = cx @@ -817,7 +818,7 @@ impl ProjectSearchView { let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let view = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings)); - workspace.add_item_to_active_pane(Box::new(view.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(view.clone()), None, true, cx); view }; diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 2106cbe831..e4bdc238f5 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -340,7 +340,7 @@ mod tests { workspace .update(cx, |workspace, cx| { // Now, let's switch the active item to .ts file. - workspace.activate_item(&editor1, cx); + workspace.activate_item(&editor1, true, true, cx); task_context(workspace, cx) }) .await, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index fe8cea217e..7b16f07d0a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -150,7 +150,7 @@ impl TerminalView { cx, ) }); - workspace.add_item_to_active_pane(Box::new(view), None, cx) + workspace.add_item_to_active_pane(Box::new(view), None, true, cx); } } diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index ee833326f5..1a54844821 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -136,6 +136,7 @@ where pub trait AnchorRangeExt { fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Ordering; + fn intersects(&self, other: &Range, buffer: &BufferSnapshot) -> bool; } impl AnchorRangeExt for Range { @@ -145,4 +146,8 @@ impl AnchorRangeExt for Range { ord => ord, } } + + fn intersects(&self, other: &Range, buffer: &BufferSnapshot) -> bool { + self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt() + } } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 3593eea378..718939ab9f 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace.register_action(|workspace, _: &Welcome, cx| { let welcome_page = WelcomePage::new(workspace, cx); - workspace.add_item_to_active_pane(Box::new(welcome_page), None, cx) + workspace.add_item_to_active_pane(Box::new(welcome_page), None, true, cx) }); workspace .register_action(|_workspace, _: &ResetHints, cx| MultibufferHint::set_count(0, cx)); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 695d5f136b..4e606c19d3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2380,9 +2380,17 @@ impl Workspace { &mut self, item: Box, destination_index: Option, + focus_item: bool, cx: &mut WindowContext, ) { - self.add_item(self.active_pane.clone(), item, destination_index, cx) + self.add_item( + self.active_pane.clone(), + item, + destination_index, + false, + focus_item, + cx, + ) } pub fn add_item( @@ -2390,6 +2398,8 @@ impl Workspace { pane: View, item: Box, destination_index: Option, + activate_pane: bool, + focus_item: bool, cx: &mut WindowContext, ) { if let Some(text) = item.telemetry_event_text(cx) { @@ -2399,7 +2409,7 @@ impl Workspace { } pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, destination_index, cx) + pane.add_item(item, activate_pane, focus_item, destination_index, cx) }); } @@ -2410,7 +2420,7 @@ impl Workspace { cx: &mut ViewContext, ) { let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx); - self.add_item(new_pane, item, None, cx); + self.add_item(new_pane, item, None, true, true, cx); } pub fn open_abs_path( @@ -2565,6 +2575,8 @@ impl Workspace { &mut self, pane: View, project_item: Model, + activate_pane: bool, + focus_item: bool, cx: &mut ViewContext, ) -> View where @@ -2577,7 +2589,7 @@ impl Workspace { .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx)) .and_then(|item| item.downcast()) { - self.activate_item(&item, cx); + self.activate_item(&item, activate_pane, focus_item, cx); return item; } @@ -2596,7 +2608,14 @@ impl Workspace { pane.set_preview_item_id(Some(item.item_id()), cx) }); - self.add_item(pane, Box::new(item.clone()), destination_index, cx); + self.add_item( + pane, + Box::new(item.clone()), + destination_index, + activate_pane, + focus_item, + cx, + ); item } @@ -2608,14 +2627,22 @@ impl Workspace { } } - pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool { + pub fn activate_item( + &mut self, + item: &dyn ItemHandle, + activate_pane: bool, + focus_item: bool, + cx: &mut WindowContext, + ) -> bool { let result = self.panes.iter().find_map(|pane| { pane.read(cx) .index_for_item(item) .map(|ix| (pane.clone(), ix)) }); if let Some((pane, ix)) = result { - pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); + pane.update(cx, |pane, cx| { + pane.activate_item(ix, activate_pane, focus_item, cx) + }); true } else { false @@ -5568,7 +5595,7 @@ mod tests { item }); workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx); }); item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0))); @@ -5580,7 +5607,7 @@ mod tests { item }); workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx); }); item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); @@ -5594,7 +5621,7 @@ mod tests { item }); workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx); }); item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); @@ -5638,7 +5665,7 @@ mod tests { // Add an item to an empty pane workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item1), None, cx) + workspace.add_item_to_active_pane(Box::new(item1), None, true, cx) }); project.update(cx, |project, cx| { assert_eq!( @@ -5652,7 +5679,7 @@ mod tests { // Add a second item to a non-empty pane workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item2), None, cx) + workspace.add_item_to_active_pane(Box::new(item2), None, true, cx) }); assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1")); project.update(cx, |project, cx| { @@ -5707,7 +5734,7 @@ mod tests { // When there are no dirty items, there's nothing to do. let item1 = cx.new_view(|cx| TestItem::new(cx)); workspace.update(cx, |w, cx| { - w.add_item_to_active_pane(Box::new(item1.clone()), None, cx) + w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx) }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); assert!(task.await.unwrap()); @@ -5721,8 +5748,8 @@ mod tests { .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); workspace.update(cx, |w, cx| { - w.add_item_to_active_pane(Box::new(item2.clone()), None, cx); - w.add_item_to_active_pane(Box::new(item3.clone()), None, cx); + w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx); + w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx); }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.executor().run_until_parked(); @@ -5762,8 +5789,8 @@ mod tests { .with_serialize(|| Some(Task::ready(Ok(())))) }); workspace.update(cx, |w, cx| { - w.add_item_to_active_pane(Box::new(item1.clone()), None, cx); - w.add_item_to_active_pane(Box::new(item2.clone()), None, cx); + w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx); + w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx); }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); assert!(task.await.unwrap()); @@ -5801,10 +5828,10 @@ mod tests { .with_project_items(&[TestProjectItem::new_untitled(cx)]) }); let pane = workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx); - workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx); - workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx); - workspace.add_item_to_active_pane(Box::new(item4.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx); + workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx); + workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx); + workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx); workspace.active_pane().clone() }); @@ -5926,9 +5953,9 @@ mod tests { // multi-entry items: (3, 4) let left_pane = workspace.update(cx, |workspace, cx| { let left_pane = workspace.active_pane().clone(); - workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx); for item in single_entry_items { - workspace.add_item_to_active_pane(Box::new(item), None, cx); + workspace.add_item_to_active_pane(Box::new(item), None, true, cx); } left_pane.update(cx, |pane, cx| { pane.activate_item(2, true, true, cx); @@ -5999,7 +6026,7 @@ mod tests { }); let item_id = item.entity_id(); workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx); }); // Autosave on window change. @@ -6084,7 +6111,7 @@ mod tests { // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx); }); item.update(cx, |item, cx| { item.project_items[0].update(cx, |item, _| { @@ -6122,7 +6149,7 @@ mod tests { let toolbar_notify_count = Rc::new(RefCell::new(0)); workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx); + workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx); let toolbar_notification_count = toolbar_notify_count.clone(); cx.observe(&toolbar, move |_, _, _| { *toolbar_notification_count.borrow_mut() += 1 diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 50481c5e55..e311b3024a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -670,7 +670,7 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { }) }); - workspace.add_item_to_active_pane(Box::new(editor), None, cx); + workspace.add_item_to_active_pane(Box::new(editor), None, true, cx); }) .log_err(); }) @@ -889,7 +889,9 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext