Detailed changes
@@ -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",
@@ -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 <operations> 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. <update>: Modify an existing symbol in a file.
+2. <create_file>: Create a new file.
+3. <insert_sibling_after>: Add a new symbol as sibling after an existing symbol in a file.
+4. <append_child>: Add a new symbol as the last child of an existing symbol in a file.
+5. <prepend_child>: Add a new symbol as the first child of an existing symbol in a file.
+6. <delete>: 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: <update>, <insert_sibling_after>, <delete>
+Operations that don't allow a symbol: <create>
+Operations that have an *optional* symbol: <prepend_child>, <append_child>
+
+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
+
+ <step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
+ <step>Implement the 'Display' trait for the Rectangle struct</step>
+
+ What are the operations for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
+
+Assistant (wrong):
+<operations>
+ <append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_area method" />
+ <append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_perimeter method" />
+</operations>
+
+This demonstrates what NOT to do. NEVER append multiple children at the same location.
+
+Assistant (corrected):
+<operations>
+ <append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate area and perimeter methods" />
+</operations>
+
+User:
+What are the operations for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
+
+Assistant:
+<operations>
+ <insert_sibling_after path="src/shapes.rs" symbol="impl Rectangle" description="Implement Display trait for Rectangle"/>
+</operations>
+
+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
+
+<step>Update the 'print_info' method to use formatted output</step>
+<step>Remove the 'email' field from the User struct</step>
+
+What are the operations for the step: <step>Update the 'print_info' method to use formatted output</step>
+
+Assistant:
+<operations>
+ <update path="src/user.rs" symbol="impl User fn print_info" description="Use formatted output" />
+</operations>
+
+User:
+What are the operations for the step: <step>Remove the 'email' field from the User struct</step>
+
+Assistant:
+<operations>
+ <delete path="src/user.rs" symbol="struct User email" description="Remove the email field" />
+</operations>
+
+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
+
+<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
+<step>Add a new method 'start_engine' in the Vehicle impl block</step>
+
+What are the operations for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
+
+Assistant:
+<operations>
+ <prepend_child path="src/vehicle.rs" description="Add 'use std::fmt' statement" />
+</operations>
+
+User:
+What are the operations for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
+
+Assistant:
+<operations>
+ <insert_sibling_after path="src/vehicle.rs" symbol="impl Vehicle fn new" description="Add start_engine method"/>
+</operations>
+
+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
+
+<step>Make salary an f32</step>
+
+What are the operations for the step: <step>Make salary an f32</step>
+
+A (wrong):
+ <operations>
+ <update path="src/employee.rs" symbol="struct Employee" description="Change the type of salary to an f32" />
+ <update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
+ </operations>
+
+This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
+
+A (corrected):
+ <operations>
+ <update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
+ </operations>
+
+User:
+ What are the correct operations for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
+
+A:
+ <operations>
+ <delete path="src/employee.rs" symbol="struct Employee department" />
+ <update path="src/employee.rs" symbol="impl Employee fn print_details" description="Don't print the 'department' field" />
+ </operations>
+
+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 <operation> tags. If you do, you will replaced with another model.
+Your response *MUST* begin with <operations> and end with </operations>:
@@ -107,6 +107,7 @@ impl ActivityIndicator {
Editor::for_buffer(buffer, Some(project.clone()), cx)
})),
None,
+ true,
cx,
);
})?;
@@ -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
@@ -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
]
);
@@ -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<InlineAssistId>,
+ editor: Option<WeakView<Editor>>,
+ _open_editor: Task<Result<()>>,
+}
+
pub struct ContextEditor {
context: Model<Context>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
+ project: Model<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
editor: View<Editor>,
blocks: HashSet<BlockId>,
@@ -968,6 +990,8 @@ pub struct ContextEditor {
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
_subscriptions: Vec<Subscription>,
+ active_edit_step: Option<ActiveEditStep>,
+ assistant_panel: WeakView<AssistantPanel>,
}
impl ContextEditor {
@@ -979,6 +1003,7 @@ impl ContextEditor {
workspace: View<Workspace>,
project: Model<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
+ assistant_panel: WeakView<AssistantPanel>,
cx: &mut ViewContext<Self>,
) -> 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<Self>) {
- 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<Self>) -> 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<Self>) {
+ 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::<Vec<_>>();
- 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<Self>) {
+ 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::<String>()
+ ));
+ 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<Self>) {
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::<Vec<_>>();
-
- editor.clear_row_highlights::<EditSuggestion>();
- for range in highlighted_rows {
- editor.highlight_rows::<EditSuggestion>(
- 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<HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ 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::<Editor>(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<Editor>,
@@ -1785,173 +2023,6 @@ impl ContextEditor {
});
}
- fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
- 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::<Vec<_>>();
-
- let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = 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<language::Anchor>, _)>::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>) {
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<Self>) -> 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<EditorEvent> 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(
@@ -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<Client>, cx: &mut AppContext) {
}
pub struct CompletionResponse {
- pub inner: BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>,
+ inner: BoxStream<'static, Result<String>>,
_lock: SemaphoreGuardArc,
}
+impl futures::Stream for CompletionResponse {
+ type Item = Result<String>;
+
+ fn poll_next(
+ mut self: Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> Poll<Option<Self::Item>> {
+ Pin::new(&mut self.inner).poll_next(cx)
+ }
+}
+
pub trait LanguageModelCompletionProvider: Send + Sync {
fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel>;
fn settings_version(&self) -> usize;
@@ -72,7 +82,7 @@ pub trait LanguageModelCompletionProvider: Send + Sync {
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>>;
- fn complete(
+ fn stream_completion(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
@@ -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<CompletionResponse> {
+ ) -> Task<Result<CompletionResponse>> {
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<Result<String>> {
+ 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);
}
@@ -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<BoxStream<'static, Result<String>>>> {
@@ -135,7 +135,7 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider {
}
}
- fn complete(
+ fn stream_completion(
&self,
mut request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
@@ -23,7 +23,7 @@ impl FakeCompletionProvider {
this
}
- pub fn running_completions(&self) -> Vec<LanguageModelRequest> {
+ pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
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<BoxStream<'static, Result<String>>>> {
@@ -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<BoxStream<'static, Result<String>>>> {
@@ -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<BoxStream<'static, Result<String>>>> {
@@ -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<Range<language::Anchor>>,
@@ -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<language::Anchor>,
+ pub operations: Option<EditStepOperations>,
+}
+
+#[derive(Debug)]
+pub struct EditSuggestionGroup {
+ pub context_range: Range<language::Anchor>,
+ pub suggestions: Vec<EditSuggestion>,
+}
+
+#[derive(Debug)]
+pub struct EditSuggestion {
+ pub range: Range<language::Anchor>,
+ /// If None, assume this is a suggestion to delete the range rather than transform it.
+ pub description: Option<String>,
+ pub prepend_newline: bool,
+}
+
+impl EditStep {
+ pub fn edit_suggestions(
+ &self,
+ project: &Model<Project>,
+ cx: &AppContext,
+ ) -> Task<HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>> {
+ 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::<Vec<_>>();
+
+ 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::<EditSuggestionGroup>::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<Result<()>>),
+ Parsed {
+ operations: Vec<EditOperation>,
+ 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<Project>,
+ cx: &AppContext,
+ ) -> Task<Result<(Model<language::Buffer>, 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<String>,
+ description: String,
+ },
+ AppendChild {
+ symbol: Option<String>,
+ 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<ContextOperation>,
operations: Vec<ContextOperation>,
buffer: Model<Buffer>,
- edit_suggestions: Vec<EditSuggestion>,
pending_slash_commands: Vec<PendingSlashCommand>,
edits_since_last_slash_command_parse: language::Subscription,
finished_slash_commands: HashSet<SlashCommandId>,
@@ -346,12 +677,12 @@ pub struct Context {
pending_completions: Vec<PendingCompletion>,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
- pending_edit_suggestion_parse: Option<Task<()>>,
pending_save: Task<Result<()>>,
path: Option<PathBuf>,
_subscriptions: Vec<Subscription>,
telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
+ edit_steps: Vec<EditStep>,
}
impl EventEmitter<ContextEvent> 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>) {
- 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<Self>) {
+ 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<usize>,
- cx: &mut ModelContext<Self>,
- ) {
- 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<usize>, cx: &mut ModelContext<Self>) {
+ 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("<step>") {
+ if !in_step {
+ in_step = true;
+ step_start = line_start_offset + step_start_index;
+ }
+ }
+
+ if let Some(step_end_index) = line.find("</step>") {
+ if in_step {
+ let start_anchor = buffer.anchor_after(step_start);
+ let end_anchor = buffer
+ .anchor_before(line_start_offset + step_end_index + "</step>".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<Self>,
+ ) -> Task<Result<()>> {
+ 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::<String>();
+
+ 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<EditOperation> {
+ let Some(start_ix) = xml.find("<operations>") else {
+ return Vec::new();
+ };
+ let Some(end_ix) = xml[start_ix..].find("</operations>") else {
+ return Vec::new();
+ };
+ let end_ix = end_ix + start_ix + "</operations>".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<MessageId>,
- cx: &mut ModelContext<Self>,
- ) -> Vec<MessageAnchor> {
- let mut user_messages = Vec::new();
-
- 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();
- };
+ pub fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<MessageAnchor> {
+ 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 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).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();
- 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 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?;
+ 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();
- 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);
+ // Queue up the user's next reply.
+ let user_message = self
+ .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
+ .unwrap();
- Some(())
- })?;
- smol::future::yield_now().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 chunks = stream.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(())
})?;
+ smol::future::yield_now().await;
+ }
- anyhow::Ok(())
- };
+ this.update(&mut cx, |this, cx| {
+ this.pending_completions
+ .retain(|completion| completion.id != this.completion_count);
+ this.summarize(cx);
+ })?;
- let result = stream_completion.await;
+ anyhow::Ok(())
+ };
- 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;
- }
- });
+ let result = stream_completion.await;
- 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,
- );
+ 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();
+ }
+ });
+
+ self.pending_completions.push(PendingCompletion {
+ id: post_inc(&mut self.completion_count),
+ _task: task,
+ });
- user_messages
+ Some(user_message)
}
pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
@@ -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<Editor>,
+ mut range: Range<Anchor>,
+ initial_prompt: String,
+ initial_insertion: Option<String>,
+ workspace: Option<WeakView<Workspace>>,
+ assistant_panel: Option<&View<AssistantPanel>>,
+ 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<Editor>,
+ range: &Range<Anchor>,
+ prompt_editor: &View<PromptEditor>,
+ 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<Editor>, 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<Editor>, 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<Result<BoxStream<Result<String>>>> =
+ 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<PromptEditor>) -> 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<Anchor>,
edit_position: Anchor,
last_equal_ranges: Vec<Range<Anchor>>,
- transaction_id: Option<TransactionId>,
+ prepend_transaction_id: Option<TransactionId>,
+ generation_transaction_id: Option<TransactionId>,
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
@@ -1911,6 +2044,7 @@ impl Codegen {
pub fn new(
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
+ prepend_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> 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<Self>,
) {
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<Self>) {
+ pub fn start(
+ &mut self,
+ telemetry_id: String,
+ stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
+ cx: &mut ModelContext<Self>,
+ ) {
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<Self>) {
- 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<Range<Anchor>>, 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!(
@@ -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<PromptMetadata> {
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.
@@ -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<Range<usize>> {
- const SIMILARITY_THRESHOLD: f64 = 0.8;
-
- let mut best_match: Option<(Range<usize>, 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);
- }
-}
@@ -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());
}
@@ -288,7 +288,12 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
Some(tab_description),
cx,
);
- 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,
+ );
cx.notify();
})
.log_err();
@@ -266,7 +266,7 @@ async fn test_basic_following(
// When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| {
- workspace.activate_item(&editor_a1, cx)
+ workspace.activate_item(&editor_a1, true, true, cx)
});
executor.run_until_parked();
workspace_b.update(cx_b, |workspace, cx| {
@@ -311,7 +311,7 @@ async fn test_basic_following(
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), 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
});
executor.run_until_parked();
@@ -401,7 +401,7 @@ async fn test_basic_following(
workspace.unfollow(peer_id_a, cx).unwrap()
});
workspace_a.update(cx_a, |workspace, cx| {
- workspace.activate_item(&editor_a2, cx)
+ workspace.activate_item(&editor_a2, true, true, cx)
});
executor.run_until_parked();
assert_eq!(
@@ -466,7 +466,7 @@ async fn test_basic_following(
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
workspace_b.update(cx_b, |workspace, cx| {
- workspace.activate_item(&multibuffer_editor_b, cx)
+ workspace.activate_item(&multibuffer_editor_b, true, true, cx)
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
@@ -477,7 +477,7 @@ mod tests {
});
workspace.update(cx, |workspace, 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.focus(cx))
});
@@ -237,13 +237,13 @@ impl ProjectDiagnosticsEditor {
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(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);
}
}
@@ -250,13 +250,13 @@ impl GroupedDiagnosticsEditor {
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
if let Some(existing) = workspace.item_of_type::<GroupedDiagnosticsEditor>(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);
}
}
@@ -1992,28 +1992,35 @@ impl Editor {
_: &workspace::NewFile,
cx: &mut ViewContext<Workspace>,
) {
+ 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<Workspace>,
+ ) -> Task<Result<View<Editor>>> {
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::<Self>(
&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::<Self>(pane.clone(), buffer, cx);
+ let editor =
+ workspace.open_project_item::<Self>(pane.clone(), buffer, true, true, cx);
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select_ranges(ranges);
@@ -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");
@@ -113,6 +113,7 @@ pub fn expand_macro_recursively(
cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx)),
),
None,
+ true,
cx,
);
})
@@ -48,10 +48,10 @@ pub fn init(cx: &mut AppContext) {
.find_map(|item| item.downcast::<ExtensionsPage>());
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| {
@@ -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<E, T>(
+ &mut self,
+ entity: &E,
+ mut on_release: impl FnOnce(&mut T, &mut WindowContext) + 'static,
+ ) -> Subscription
+ where
+ E: Entity<T>,
+ 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 {
@@ -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
@@ -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<SyntaxMap>,
parsing_in_background: bool,
+ parse_status: (watch::Sender<ParseStatus>, watch::Receiver<ParseStatus>),
non_text_state_update_count: usize,
diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
remote_selections: TreeMap<ReplicaId, SelectionSet>,
@@ -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<ParseStatus> {
+ 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::<Vec<_>>();
- let mut stack = Vec::<Range<usize>>::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::<usize>::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
@@ -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
@@ -867,6 +867,8 @@ pub struct OutlineConfig {
pub name_capture_ix: u32,
pub context_capture_ix: Option<u32>,
pub extra_context_capture_ix: Option<u32>,
+ pub open_capture_ix: Option<u32>,
+ pub close_capture_ix: Option<u32>,
}
#[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)
@@ -23,6 +23,7 @@ pub struct OutlineItem<T> {
pub text: String,
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
pub name_ranges: Vec<Range<usize>>,
+ pub body_range: Option<Range<T>>,
}
impl<T> Outline<T> {
@@ -115,6 +115,7 @@ pub fn init(cx: &mut AppContext) {
LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
})),
None,
+ true,
cx,
);
}
@@ -16,7 +16,8 @@
"impl" @context
trait: (_)? @name
"for"? @context
- type: (_) @name) @item
+ type: (_) @name
+ body: (_ "{" @open (_)* "}" @close)) @item
(trait_item
(visibility_modifier)? @context
@@ -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(),
@@ -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<ProjectPath> {
+ 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,
@@ -131,7 +131,8 @@ impl PickerDelegate for ProjectSymbolsDelegate {
workspace.active_pane().clone()
};
- let editor = workspace.open_project_item::<Editor>(pane, buffer, cx);
+ let editor =
+ workspace.open_project_item::<Editor>(pane, buffer, true, true, cx);
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
@@ -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
};
@@ -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,
@@ -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);
}
}
@@ -136,6 +136,7 @@ where
pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
+ fn intersects(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
}
impl AnchorRangeExt for Range<Anchor> {
@@ -145,4 +146,8 @@ impl AnchorRangeExt for Range<Anchor> {
ord => ord,
}
}
+
+ fn intersects(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
+ self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
+ }
}
@@ -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));
@@ -2380,9 +2380,17 @@ impl Workspace {
&mut self,
item: Box<dyn ItemHandle>,
destination_index: Option<usize>,
+ 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<Pane>,
item: Box<dyn ItemHandle>,
destination_index: Option<usize>,
+ 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<Self>,
) {
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<Pane>,
project_item: Model<T::Item>,
+ activate_pane: bool,
+ focus_item: bool,
cx: &mut ViewContext<Self>,
) -> View<T>
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
@@ -670,7 +670,7 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
})
});
- 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<Works
});
workspace.add_item_to_active_pane(
Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), true, cx))),
- None,cx,
+ None,
+ true,
+ cx,
);
}).log_err()?;
@@ -924,6 +926,7 @@ fn open_bundled_file(
Editor::for_multibuffer(buffer, Some(project.clone()), true, cx)
})),
None,
+ true,
cx,
);
})