Detailed changes
@@ -422,7 +422,6 @@ dependencies = [
"settings",
"similar",
"smol",
- "strsim 0.11.1",
"telemetry_events",
"terminal",
"terminal_view",
@@ -5952,6 +5951,7 @@ dependencies = [
"similar",
"smallvec",
"smol",
+ "strsim 0.11.1",
"sum_tree",
"task",
"text",
@@ -5994,6 +5994,7 @@ dependencies = [
"menu",
"ollama",
"open_ai",
+ "parking_lot",
"project",
"proto",
"rand 0.8.5",
@@ -67,7 +67,6 @@ serde_json.workspace = true
settings.workspace = true
similar.workspace = true
smol.workspace = true
-strsim.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true
@@ -86,6 +85,7 @@ ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
+language_model = { workspace = true, features = ["test-support"] }
log.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
@@ -9,10 +9,11 @@ use crate::{
},
terminal_inline_assistant::TerminalInlineAssistant,
Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
- DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepState,
- EditStepSuggestions, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
- MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
- RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
+ DebugEditSteps, DeployHistory, DeployPromptLibrary, EditSuggestionGroup, InlineAssist,
+ InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
+ PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
+ SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStep,
+ WorkflowStepEditSuggestions,
};
use crate::{ContextStoreEvent, ShowConfiguration};
use anyhow::{anyhow, Result};
@@ -39,7 +40,8 @@ use gpui::{
};
use indexed_docs::IndexedDocsStore;
use language::{
- language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
+ language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point,
+ ToOffset,
};
use language_model::{
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
@@ -1284,7 +1286,6 @@ struct ActiveEditStep {
start: language::Anchor,
assist_ids: Vec<InlineAssistId>,
editor: Option<WeakView<Editor>>,
- _open_editor: Task<Result<()>>,
}
pub struct ContextEditor {
@@ -1452,23 +1453,21 @@ impl ContextEditor {
.read(cx)
.buffer()
.read(cx)
- .text_for_range(step.source_range.clone())
+ .text_for_range(step.tagged_range.clone())
.collect::<String>()
));
- match &step.state {
- Some(EditStepState::Resolved(resolution)) => {
+ match &step.edit_suggestions {
+ WorkflowStepEditSuggestions::Resolved {
+ title,
+ edit_suggestions,
+ } => {
output.push_str("Resolution:\n");
- output.push_str(&format!(" {:?}\n", resolution.step_title));
- for op in &resolution.operations {
- output.push_str(&format!(" {:?}\n", op));
- }
+ output.push_str(&format!(" {:?}\n", title));
+ output.push_str(&format!(" {:?}\n", edit_suggestions));
}
- Some(EditStepState::Pending(_)) => {
+ WorkflowStepEditSuggestions::Pending(_) => {
output.push_str("Resolution: Pending\n");
}
- None => {
- output.push_str("Resolution: None\n");
- }
}
output.push('\n');
}
@@ -1875,222 +1874,165 @@ 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)
+ self.update_active_workflow_step(cx);
+ }
+ _ => {}
+ }
+ cx.emit(event.clone());
+ }
+
+ fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
+ if self
+ .workflow_step_for_cursor(cx)
+ .map(|step| step.tagged_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())
{
- 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);
- }
- });
+ 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();
- }
- }
+ });
+ }
+ })
+ .ok();
+ }
+ }
- if let Some(new_active_step) = self.edit_step_for_cursor(cx) {
- let start = new_active_step.source_range.start;
- let open_editor = new_active_step
- .edit_suggestions(&self.project, cx)
- .map(|suggestions| {
- self.open_editor_for_edit_suggestions(suggestions, cx)
- })
- .unwrap_or_else(|| Task::ready(Ok(())));
- self.active_edit_step = Some(ActiveEditStep {
- start,
- assist_ids: Vec::new(),
- editor: None,
- _open_editor: open_editor,
- });
+ if let Some(new_active_step) = self.workflow_step_for_cursor(cx) {
+ let start = new_active_step.tagged_range.start;
+
+ let mut editor = None;
+ let mut assist_ids = Vec::new();
+ if let WorkflowStepEditSuggestions::Resolved {
+ title,
+ edit_suggestions,
+ } = &new_active_step.edit_suggestions
+ {
+ if let Some((opened_editor, inline_assist_ids)) =
+ self.suggest_edits(title.clone(), edit_suggestions.clone(), cx)
+ {
+ editor = Some(opened_editor.downgrade());
+ assist_ids = inline_assist_ids;
}
}
+
+ self.active_edit_step = Some(ActiveEditStep {
+ start,
+ assist_ids,
+ editor,
+ });
}
- _ => {}
}
- cx.emit(event.clone());
}
- fn open_editor_for_edit_suggestions(
+ fn suggest_edits(
&mut self,
- edit_step_suggestions: Task<EditStepSuggestions>,
+ title: String,
+ edit_suggestions: 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_step_suggestions = edit_step_suggestions.await;
-
- let mut assist_ids = Vec::new();
- let editor = if edit_step_suggestions.suggestions.is_empty() {
- return Ok(());
- } else if edit_step_suggestions.suggestions.len() == 1
- && edit_step_suggestions
- .suggestions
- .values()
- .next()
- .unwrap()
- .len()
- == 1
- {
- // If there's only one buffer and one suggestion group, open it directly
- let (buffer, suggestion_groups) = edit_step_suggestions
- .suggestions
- .into_iter()
- .next()
- .unwrap();
- let suggestion_group = suggestion_groups.into_iter().next().unwrap();
- let editor = workspace.update(&mut cx, |workspace, cx| {
+ ) -> Option<(View<Editor>, Vec<InlineAssistId>)> {
+ let assistant_panel = self.assistant_panel.upgrade()?;
+ if edit_suggestions.is_empty() {
+ return None;
+ }
+
+ let editor;
+ let mut suggestion_groups = Vec::new();
+ 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, groups) = edit_suggestions.into_iter().next().unwrap();
+ let group = groups.into_iter().next().unwrap();
+ editor = self
+ .workspace
+ .update(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 multibuffer = editor.read(cx).buffer().read(cx).read(cx);
- let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
- multibuffer
- .anchor_in_excerpt(excerpt_id, suggestion.range.start)
- .unwrap()
- ..multibuffer
- .anchor_in_excerpt(excerpt_id, suggestion.range.end)
- .unwrap()
- };
-
- InlineAssistant::update_global(cx, |assistant, cx| {
- let suggestion_id = assistant.suggest_assist(
- &editor,
- range,
- description,
- suggestion.initial_insertion,
- Some(workspace.clone()),
- assistant_panel.upgrade().as_ref(),
- cx,
- );
- assist_ids.push(suggestion_id);
- });
- }
+ })
+ .log_err()?;
- // Scroll the editor to the suggested assist
- editor.update(cx, |editor, cx| {
- let multibuffer = editor.buffer().read(cx).snapshot(cx);
- let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
- let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0
- {
- Anchor::min()
- } else {
- multibuffer
- .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
- .unwrap()
- };
-
- editor.set_scroll_anchor(
- ScrollAnchor {
- offset: gpui::Point::default(),
- anchor,
- },
- cx,
- );
- });
- })?;
+ let (&excerpt_id, _, _) = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .as_singleton()
+ .unwrap();
+
+ // Scroll the editor to the suggested assist
+ editor.update(cx, |editor, cx| {
+ let multibuffer = editor.buffer().read(cx).snapshot(cx);
+ let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
+ let anchor = if group.context_range.start.to_offset(buffer) == 0 {
+ Anchor::min()
+ } else {
+ multibuffer
+ .anchor_in_excerpt(excerpt_id, group.context_range.start)
+ .unwrap()
+ };
- 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)
- .with_title(edit_step_suggestions.title);
- for (buffer, suggestion_groups) in edit_step_suggestions.suggestions {
- let excerpt_ids = multibuffer.push_excerpts(
- buffer,
- suggestion_groups
- .iter()
- .map(|suggestion_group| ExcerptRange {
- context: suggestion_group.context_range.clone(),
- primary: None,
- }),
- cx,
- );
+ editor.set_scroll_anchor(
+ ScrollAnchor {
+ offset: gpui::Point::default(),
+ anchor,
+ },
+ 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()
- };
- inline_assist_suggestions.push((
- range,
- description,
- suggestion.initial_insertion,
- ));
- }
- }
- }
- multibuffer
- })?;
+ suggestion_groups.push((excerpt_id, group));
+ } else {
+ // If there are multiple buffers or suggestion groups, create a multibuffer
+ let multibuffer = cx.new_model(|cx| {
+ let replica_id = self.project.read(cx).replica_id();
+ let mut multibuffer =
+ MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
+ for (buffer, groups) in edit_suggestions {
+ let excerpt_ids = multibuffer.push_excerpts(
+ buffer,
+ groups.iter().map(|suggestion_group| ExcerptRange {
+ context: suggestion_group.context_range.clone(),
+ primary: None,
+ }),
+ cx,
+ );
+ suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
+ }
+ 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_insertion) in inline_assist_suggestions {
- assist_ids.push(assistant.suggest_assist(
- &editor,
- range,
- description,
- initial_insertion,
- Some(workspace.clone()),
- assistant_panel.upgrade().as_ref(),
- cx,
- ));
- }
- })
- })?;
- workspace.update(&mut cx, |workspace, cx| {
+ editor = cx.new_view(|cx| {
+ Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx)
+ });
+ self.workspace
+ .update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
- })?;
-
- editor
- };
+ })
+ .log_err()?;
+ }
- 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());
- }
- })
- })
+ let mut assist_ids = Vec::new();
+ for (excerpt_id, suggestion_group) in suggestion_groups {
+ for suggestion in suggestion_group.suggestions {
+ assist_ids.extend(suggestion.show(
+ &editor,
+ excerpt_id,
+ &self.workspace,
+ &assistant_panel,
+ cx,
+ ));
+ }
+ }
+ Some((editor, assist_ids))
}
fn handle_editor_search_event(
@@ -2374,11 +2316,10 @@ 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.state {
- Some(EditStepState::Pending(_)) => "Computing Changes...",
- Some(EditStepState::Resolved(_)) => "Apply Changes",
- None => "Send",
+ let button_text = match self.workflow_step_for_cursor(cx) {
+ Some(edit_step) => match &edit_step.edit_suggestions {
+ WorkflowStepEditSuggestions::Pending(_) => "Computing Changes...",
+ WorkflowStepEditSuggestions::Resolved { .. } => "Apply Changes",
},
None => "Send",
};
@@ -2421,7 +2362,7 @@ impl ContextEditor {
})
}
- fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
+ fn workflow_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a WorkflowStep> {
let newest_cursor = self
.editor
.read(cx)
@@ -2435,7 +2376,7 @@ impl ContextEditor {
let edit_steps = context.edit_steps();
edit_steps
.binary_search_by(|step| {
- let step_range = step.source_range.clone();
+ let step_range = step.tagged_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() {
@@ -1,6 +1,6 @@
use crate::{
- prompt_library::PromptStore, slash_command::SlashCommandLine, InitialInsertion, MessageId,
- MessageStatus,
+ prompt_library::PromptStore, slash_command::SlashCommandLine, AssistantPanel, InitialInsertion,
+ InlineAssistId, InlineAssistant, MessageId, MessageStatus,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
@@ -9,14 +9,19 @@ use assistant_slash_command::{
use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId;
use collections::{HashMap, HashSet};
+use editor::Editor;
use fs::{Fs, RemoveOptions};
use futures::{
future::{self, Shared},
FutureExt, StreamExt,
};
-use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task};
+use gpui::{
+ AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task, UpdateGlobal,
+ View, WeakView,
+};
use language::{
- AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, ParseStatus, Point, ToOffset,
+ AnchorRangeExt, Bias, Buffer, BufferSnapshot, LanguageRegistry, OffsetRangeExt, ParseStatus,
+ Point, ToOffset,
};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelTool,
@@ -37,9 +42,10 @@ use std::{
time::{Duration, Instant},
};
use telemetry_events::AssistantKind;
-use ui::SharedString;
+use ui::{SharedString, WindowContext};
use util::{post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
+use workspace::Workspace;
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ContextId(String);
@@ -339,427 +345,229 @@ struct PendingCompletion {
pub struct SlashCommandId(clock::Lamport);
#[derive(Debug)]
-pub struct EditStep {
- pub source_range: Range<language::Anchor>,
- pub state: Option<EditStepState>,
-}
-
-#[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 initial_insertion: Option<InitialInsertion>,
+pub struct WorkflowStep {
+ pub tagged_range: Range<language::Anchor>,
+ pub edit_suggestions: WorkflowStepEditSuggestions,
}
-pub struct EditStepSuggestions {
- pub title: String,
- pub suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
-}
-
-impl EditStep {
- pub fn edit_suggestions(
- &self,
- project: &Model<Project>,
- cx: &AppContext,
- ) -> Option<Task<EditStepSuggestions>> {
- let Some(EditStepState::Resolved(resolution)) = &self.state else {
- return None;
- };
-
- let title = resolution.step_title.clone();
- let suggestion_tasks: Vec<_> = resolution
- .operations
- .iter()
- .map(|operation| operation.edit_suggestion(project.clone(), cx))
- .collect();
-
- Some(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);
- }
-
- EditStepSuggestions {
- title,
- suggestions: suggestion_groups_by_buffer,
- }
- }))
- }
-}
-
-pub enum EditStepState {
+pub enum WorkflowStepEditSuggestions {
Pending(Task<Option<()>>),
- Resolved(EditStepResolution),
-}
-
-impl Debug for EditStepState {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- EditStepState::Pending(_) => write!(f, "EditStepOperations::Pending"),
- EditStepState::Resolved(operations) => f
- .debug_struct("EditStepOperations::Parsed")
- .field("operations", operations)
- .finish(),
- }
- }
-}
-
-#[derive(Debug, Deserialize, JsonSchema)]
-pub struct EditStepResolution {
- /// An extremely short title for the edit step represented by these operations.
- pub step_title: String,
- /// A sequence of operations to apply to the codebase.
- /// When multiple operations are required for a step, be sure to include multiple operations in this list.
- pub operations: Vec<EditOperation>,
-}
-
-impl LanguageModelTool for EditStepResolution {
- fn name() -> String {
- "edit".into()
- }
-
- fn description() -> String {
- "suggest edits to one or more locations in the codebase".into()
- }
-}
-
-/// A description of an operation to apply to one location in the codebase.
-///
-/// This object represents a single edit operation that can be performed on a specific file
-/// in the codebase. It encapsulates both the location (file path) and the nature of the
-/// edit to be made.
-///
-/// # Fields
-///
-/// * `path`: A string representing the file path where the edit operation should be applied.
-/// This path is relative to the root of the project or repository.
-///
-/// * `kind`: An enum representing the specific type of edit operation to be performed.
-///
-/// # Usage
-///
-/// `EditOperation` is used within a code editor to represent and apply
-/// programmatic changes to source code. It provides a structured way to describe
-/// edits for features like refactoring tools or AI-assisted coding suggestions.
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
-pub struct EditOperation {
- /// The path to the file containing the relevant operation
- pub path: String,
- #[serde(flatten)]
- pub kind: EditOperationKind,
+ Resolved {
+ title: String,
+ edit_suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
+ },
}
-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
- .find_project_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 initial_insertion = kind.initial_insertion();
- 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()
- .max_by(|a, b| {
- strsim::jaro_winkler(&a.string, symbol)
- .total_cmp(&strsim::jaro_winkler(&b.string, symbol))
- })
- .with_context(|| {
- format!(
- "symbol {:?} not found in path {:?}.\ncandidates: {:?}.\nparse status: {:?}. text:\n{}",
- symbol,
- path,
- outline
- .path_candidates
- .iter()
- .map(|candidate| &candidate.string)
- .collect::<Vec<_>>(),
- *parse_status.borrow(),
- buffer.read_with(&cx, |buffer, _| buffer.text()).unwrap_or_else(|_| "error".to_string())
- )
- })?;
-
- buffer.update(&mut cx, |buffer, _| {
- let outline_item = &outline.items[candidate.id];
- let symbol_range = outline_item.range.to_point(buffer);
- let annotation_range = outline_item
- .annotation_range
- .as_ref()
- .map(|range| 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 anchor = buffer.anchor_after(body_range.start);
- anchor..anchor
- }
- EditOperationKind::AppendChild { .. } => {
- let anchor = buffer.anchor_before(body_range.end);
- anchor..anchor
- }
- EditOperationKind::InsertSiblingBefore { .. } => {
- let anchor = buffer.anchor_before(
- annotation_range.map_or(symbol_range.start, |annotation_range| {
- annotation_range.start
- }),
- );
- anchor..anchor
- }
- EditOperationKind::InsertSiblingAfter { .. } => {
- let anchor = buffer.anchor_after(symbol_range.end);
- anchor..anchor
- }
- EditOperationKind::Update { .. } | EditOperationKind::Delete { .. } => {
- let start = annotation_range.map_or(symbol_range.start, |range| range.start);
- let start = Point::new(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),
- initial_insertion,
- },
- ))
- })
- }
+#[derive(Clone, Debug)]
+pub struct EditSuggestionGroup {
+ pub context_range: Range<language::Anchor>,
+ pub suggestions: Vec<EditSuggestion>,
}
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
-#[serde(tag = "kind")]
-pub enum EditOperationKind {
- /// Rewrites the specified symbol entirely based on the given description.
- /// This operation completely replaces the existing symbol with new content.
+#[derive(Clone, Debug)]
+pub enum EditSuggestion {
Update {
- /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
- /// The path should uniquely identify the symbol within the containing file.
- symbol: String,
- /// A brief description of the transformation to apply to the symbol.
+ range: Range<language::Anchor>,
description: String,
},
- /// Creates a new file with the given path based on the provided description.
- /// This operation adds a new file to the codebase.
- Create {
- /// A brief description of the file to be created.
+ CreateFile {
description: String,
},
- /// Inserts a new symbol based on the given description before the specified symbol.
- /// This operation adds new content immediately preceding an existing symbol.
InsertSiblingBefore {
- /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
- /// The new content will be inserted immediately before this symbol.
- symbol: String,
- /// A brief description of the new symbol to be inserted.
+ position: language::Anchor,
description: String,
},
- /// Inserts a new symbol based on the given description after the specified symbol.
- /// This operation adds new content immediately following an existing symbol.
InsertSiblingAfter {
- /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
- /// The new content will be inserted immediately after this symbol.
- symbol: String,
- /// A brief description of the new symbol to be inserted.
+ position: language::Anchor,
description: String,
},
- /// Inserts a new symbol as a child of the specified symbol at the start.
- /// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
PrependChild {
- /// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
- /// If provided, the new content will be inserted as the first child of this symbol.
- /// If not provided, the new content will be inserted at the top of the file.
- symbol: Option<String>,
- /// A brief description of the new symbol to be inserted.
+ position: language::Anchor,
description: String,
},
- /// Inserts a new symbol as a child of the specified symbol at the end.
- /// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
AppendChild {
- /// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
- /// If provided, the new content will be inserted as the last child of this symbol.
- /// If not provided, the new content will be applied at the bottom of the file.
- symbol: Option<String>,
- /// A brief description of the new symbol to be inserted.
+ position: language::Anchor,
description: String,
},
- /// Deletes the specified symbol from the containing file.
Delete {
- /// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
- symbol: String,
+ range: Range<language::Anchor>,
},
}
-impl EditOperationKind {
- pub fn symbol(&self) -> Option<&str> {
+impl EditSuggestion {
+ pub fn range(&self) -> Range<language::Anchor> {
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,
+ EditSuggestion::Update { range, .. } => range.clone(),
+ EditSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
+ EditSuggestion::InsertSiblingBefore { position, .. }
+ | EditSuggestion::InsertSiblingAfter { position, .. }
+ | EditSuggestion::PrependChild { position, .. }
+ | EditSuggestion::AppendChild { position, .. } => *position..*position,
+ EditSuggestion::Delete { range } => range.clone(),
}
}
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,
+ EditSuggestion::Update { description, .. }
+ | EditSuggestion::CreateFile { description }
+ | EditSuggestion::InsertSiblingBefore { description, .. }
+ | EditSuggestion::InsertSiblingAfter { description, .. }
+ | EditSuggestion::PrependChild { description, .. }
+ | EditSuggestion::AppendChild { description, .. } => Some(description),
+ EditSuggestion::Delete { .. } => None,
}
}
- pub fn initial_insertion(&self) -> Option<InitialInsertion> {
+ fn description_mut(&mut self) -> Option<&mut String> {
match self {
- EditOperationKind::InsertSiblingBefore { .. } => Some(InitialInsertion::NewlineAfter),
- EditOperationKind::InsertSiblingAfter { .. } => Some(InitialInsertion::NewlineBefore),
- EditOperationKind::PrependChild { .. } => Some(InitialInsertion::NewlineAfter),
- EditOperationKind::AppendChild { .. } => Some(InitialInsertion::NewlineBefore),
- _ => None,
+ EditSuggestion::Update { description, .. }
+ | EditSuggestion::CreateFile { description }
+ | EditSuggestion::InsertSiblingBefore { description, .. }
+ | EditSuggestion::InsertSiblingAfter { description, .. }
+ | EditSuggestion::PrependChild { description, .. }
+ | EditSuggestion::AppendChild { description, .. } => Some(description),
+ EditSuggestion::Delete { .. } => None,
+ }
+ }
+
+ fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
+ let range = self.range();
+ let other_range = other.range();
+
+ // Don't merge if we don't contain the other suggestion.
+ if range.start.cmp(&other_range.start, buffer).is_gt()
+ || range.end.cmp(&other_range.end, buffer).is_lt()
+ {
+ return false;
+ }
+
+ if let Some(description) = self.description_mut() {
+ if let Some(other_description) = other.description() {
+ description.push('\n');
+ description.push_str(other_description);
+ }
+ }
+ true
+ }
+
+ pub fn show(
+ &self,
+ editor: &View<Editor>,
+ excerpt_id: editor::ExcerptId,
+ workspace: &WeakView<Workspace>,
+ assistant_panel: &View<AssistantPanel>,
+ cx: &mut WindowContext,
+ ) -> Option<InlineAssistId> {
+ let mut initial_transaction_id = None;
+ let initial_prompt;
+ let suggestion_range;
+ let buffer = editor.read(cx).buffer().clone();
+ let snapshot = buffer.read(cx).snapshot(cx);
+
+ match self {
+ EditSuggestion::Update { range, description } => {
+ initial_prompt = description.clone();
+ suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
+ ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
+ }
+ EditSuggestion::CreateFile { description } => {
+ initial_prompt = description.clone();
+ suggestion_range = editor::Anchor::min()..editor::Anchor::min();
+ }
+ EditSuggestion::InsertSiblingBefore {
+ position,
+ description,
+ } => {
+ let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
+ initial_prompt = description.clone();
+ suggestion_range = buffer.update(cx, |buffer, cx| {
+ buffer.start_transaction(cx);
+ let line_start = buffer.insert_empty_line(position, true, true, cx);
+ initial_transaction_id = buffer.end_transaction(cx);
+
+ let line_start = buffer.read(cx).anchor_before(line_start);
+ line_start..line_start
+ });
+ }
+ EditSuggestion::InsertSiblingAfter {
+ position,
+ description,
+ } => {
+ let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
+ initial_prompt = description.clone();
+ suggestion_range = buffer.update(cx, |buffer, cx| {
+ buffer.start_transaction(cx);
+ let line_start = buffer.insert_empty_line(position, true, true, cx);
+ initial_transaction_id = buffer.end_transaction(cx);
+
+ let line_start = buffer.read(cx).anchor_before(line_start);
+ line_start..line_start
+ });
+ }
+ EditSuggestion::PrependChild {
+ position,
+ description,
+ } => {
+ let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
+ initial_prompt = description.clone();
+ suggestion_range = buffer.update(cx, |buffer, cx| {
+ buffer.start_transaction(cx);
+ let line_start = buffer.insert_empty_line(position, false, true, cx);
+ initial_transaction_id = buffer.end_transaction(cx);
+
+ let line_start = buffer.read(cx).anchor_before(line_start);
+ line_start..line_start
+ });
+ }
+ EditSuggestion::AppendChild {
+ position,
+ description,
+ } => {
+ let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
+ initial_prompt = description.clone();
+ suggestion_range = buffer.update(cx, |buffer, cx| {
+ buffer.start_transaction(cx);
+ let line_start = buffer.insert_empty_line(position, true, false, cx);
+ initial_transaction_id = buffer.end_transaction(cx);
+
+ let line_start = buffer.read(cx).anchor_before(line_start);
+ line_start..line_start
+ });
+ }
+ EditSuggestion::Delete { range } => {
+ initial_prompt = "Delete".to_string();
+ suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
+ ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
+ }
+ }
+
+ InlineAssistant::update_global(cx, |inline_assistant, cx| {
+ Some(inline_assistant.suggest_assist(
+ editor,
+ suggestion_range,
+ initial_prompt,
+ initial_transaction_id,
+ Some(workspace.clone()),
+ Some(assistant_panel),
+ cx,
+ ))
+ })
+ }
+}
+
+impl Debug for WorkflowStepEditSuggestions {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ WorkflowStepEditSuggestions::Pending(_) => write!(f, "EditStepOperations::Pending"),
+ WorkflowStepEditSuggestions::Resolved {
+ title,
+ edit_suggestions,
+ } => f
+ .debug_struct("EditStepOperations::Parsed")
+ .field("title", title)
+ .field("edit_suggestions", edit_suggestions)
+ .finish(),
}
}
}
@@ -788,7 +596,8 @@ pub struct Context {
_subscriptions: Vec<Subscription>,
telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
- edit_steps: Vec<EditStep>,
+ edit_steps: Vec<WorkflowStep>,
+ project: Option<Model<Project>>,
}
impl EventEmitter<ContextEvent> for Context {}
@@ -796,6 +605,7 @@ impl EventEmitter<ContextEvent> for Context {}
impl Context {
pub fn local(
language_registry: Arc<LanguageRegistry>,
+ project: Option<Model<Project>>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> Self {
@@ -804,6 +614,7 @@ impl Context {
ReplicaId::default(),
language::Capability::ReadWrite,
language_registry,
+ project,
telemetry,
cx,
)
@@ -814,6 +625,7 @@ impl Context {
replica_id: ReplicaId,
capability: language::Capability,
language_registry: Arc<LanguageRegistry>,
+ project: Option<Model<Project>>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> Self {
@@ -852,6 +664,7 @@ impl Context {
path: None,
buffer,
telemetry,
+ project,
language_registry,
edit_steps: Vec::new(),
};
@@ -923,6 +736,7 @@ impl Context {
saved_context: SavedContext,
path: PathBuf,
language_registry: Arc<LanguageRegistry>,
+ project: Option<Model<Project>>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> Self {
@@ -932,6 +746,7 @@ impl Context {
ReplicaId::default(),
language::Capability::ReadWrite,
language_registry,
+ project,
telemetry,
cx,
);
@@ -1171,7 +986,7 @@ impl Context {
self.summary.as_ref()
}
- pub fn edit_steps(&self) -> &[EditStep] {
+ pub fn edit_steps(&self) -> &[WorkflowStep] {
&self.edit_steps
}
@@ -1319,7 +1134,7 @@ impl Context {
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)
+ step.tagged_range.start.is_valid(buffer) && step.tagged_range.end.is_valid(buffer)
});
if self.edit_steps.len() != prev_len {
cx.emit(ContextEvent::EditStepsChanged);
@@ -1327,58 +1142,65 @@ impl Context {
}
}
- fn parse_edit_steps_in_range(&mut self, range: Range<usize>, cx: &mut ModelContext<Self>) {
+ fn parse_edit_steps_in_range(
+ &mut self,
+ range: Range<usize>,
+ project: Model<Project>,
+ cx: &mut ModelContext<Self>,
+ ) {
let mut new_edit_steps = Vec::new();
- self.buffer.update(cx, |buffer, _cx| {
- let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
- 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;
- }
+ let buffer = self.buffer.read(cx).snapshot();
+ let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
+ 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 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 tagged_range = start_anchor..end_anchor;
- if let Err(ix) = existing_step_index {
- // Step doesn't exist, so add it
- new_edit_steps.push((
- ix,
- EditStep {
- source_range,
- state: None,
- },
- ));
- }
+ // Check if a step with the same range already exists
+ let existing_step_index = self
+ .edit_steps
+ .binary_search_by(|probe| probe.tagged_range.cmp(&tagged_range, &buffer));
- in_step = false;
+ if let Err(ix) = existing_step_index {
+ // Step doesn't exist, so add it
+ let task = self.compute_workflow_step_edit_suggestions(
+ tagged_range.clone(),
+ project.clone(),
+ cx,
+ );
+ new_edit_steps.push((
+ ix,
+ WorkflowStep {
+ tagged_range,
+ edit_suggestions: WorkflowStepEditSuggestions::Pending(task),
+ },
+ ));
}
- }
- line_start_offset = message_lines.offset();
+ in_step = false;
+ }
}
- });
+
+ line_start_offset = message_lines.offset();
+ }
// 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.state = Some(EditStepState::Pending(task));
+ for (index, step) in new_edit_steps.into_iter().rev() {
self.edit_steps.insert(index, step);
}
@@ -1386,9 +1208,10 @@ impl Context {
cx.notify();
}
- fn generate_edit_step_operations(
+ fn compute_workflow_step_edit_suggestions(
&self,
- edit_step: &EditStep,
+ tagged_range: Range<language::Anchor>,
+ project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Option<()>> {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
@@ -1396,11 +1219,10 @@ impl Context {
};
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())
+ .text_for_range(tagged_range.clone())
.collect::<String>();
cx.spawn(|this, mut cx| {
@@ -1415,18 +1237,99 @@ impl Context {
content: prompt,
});
- let resolution = model.use_tool::<EditStepResolution>(request, &cx).await?;
+ // Invoke the model to get its edit suggestions for this workflow step.
+ let step_suggestions = model
+ .use_tool::<tool::WorkflowStepEditSuggestions>(request, &cx)
+ .await?;
+
+ // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
+ let suggestion_tasks: Vec<_> = step_suggestions
+ .edit_suggestions
+ .iter()
+ .map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
+ .collect();
+
+ // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
+ 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();
+ let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
+ // Sort suggestions by their range so that earlier, larger ranges come first
+ suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
+
+ // Merge overlapping suggestions
+ suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
+
+ // Create context ranges for each suggestion
+ for suggestion in suggestions {
+ let context_range = {
+ let suggestion_point_range = suggestion.range().to_point(&snapshot);
+ let start_row = suggestion_point_range.start.row.saturating_sub(5);
+ let end_row = cmp::min(
+ suggestion_point_range.end.row + 5,
+ snapshot.max_point().row,
+ );
+ let start = snapshot.anchor_before(Point::new(start_row, 0));
+ let end = snapshot
+ .anchor_after(Point::new(end_row, snapshot.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, &snapshot)
+ .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],
+ });
+ }
+ }
+
+ suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
+ }
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))
+ step.tagged_range.cmp(&tagged_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.state = Some(EditStepState::Resolved(resolution));
+ edit_step.edit_suggestions = WorkflowStepEditSuggestions::Resolved {
+ title: step_suggestions.step_title,
+ edit_suggestions: suggestion_groups_by_buffer,
+ };
cx.emit(ContextEvent::EditStepsChanged);
}
anyhow::Ok(())
@@ -1651,7 +1554,9 @@ impl Context {
);
message_start_offset..message_new_end_offset
});
- this.parse_edit_steps_in_range(message_range, cx);
+ if let Some(project) = this.project.clone() {
+ this.parse_edit_steps_in_range(message_range, project, cx);
+ }
cx.emit(ContextEvent::StreamedCompletion);
Some(())
@@ -2514,12 +2419,12 @@ mod tests {
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
- language_model::LanguageModelRegistry::test(cx);
+ LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let context = cx.new_model(|cx| Context::local(registry, None, cx));
+ let context = cx.new_model(|cx| Context::local(registry, None, None, cx));
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -2646,11 +2551,11 @@ mod tests {
fn test_message_splitting(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
- language_model::LanguageModelRegistry::test(cx);
+ LanguageModelRegistry::test(cx);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let context = cx.new_model(|cx| Context::local(registry, None, cx));
+ let context = cx.new_model(|cx| Context::local(registry, None, None, cx));
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -330,7 +330,12 @@ impl ContextStore {
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
let context = cx.new_model(|cx| {
- Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
+ Context::local(
+ self.languages.clone(),
+ Some(self.project.clone()),
+ Some(self.telemetry.clone()),
+ cx,
+ )
});
self.register_context(&context, cx);
context
@@ -351,6 +356,7 @@ impl ContextStore {
let replica_id = project.replica_id();
let capability = project.capability();
let language_registry = self.languages.clone();
+ let project = self.project.clone();
let telemetry = self.telemetry.clone();
let request = self.client.request(proto::CreateContext { project_id });
cx.spawn(|this, mut cx| async move {
@@ -363,6 +369,7 @@ impl ContextStore {
replica_id,
capability,
language_registry,
+ Some(project),
Some(telemetry),
cx,
)
@@ -401,6 +408,7 @@ impl ContextStore {
let fs = self.fs.clone();
let languages = self.languages.clone();
+ let project = self.project.clone();
let telemetry = self.telemetry.clone();
let load = cx.background_executor().spawn({
let path = path.clone();
@@ -413,7 +421,14 @@ impl ContextStore {
cx.spawn(|this, mut cx| async move {
let saved_context = load.await?;
let context = cx.new_model(|cx| {
- Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
+ Context::deserialize(
+ saved_context,
+ path.clone(),
+ languages,
+ Some(project),
+ Some(telemetry),
+ cx,
+ )
})?;
this.update(&mut cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
@@ -472,6 +487,7 @@ impl ContextStore {
let replica_id = project.replica_id();
let capability = project.capability();
let language_registry = self.languages.clone();
+ let project = self.project.clone();
let telemetry = self.telemetry.clone();
let request = self.client.request(proto::OpenContext {
project_id,
@@ -486,6 +502,7 @@ impl ContextStore {
replica_id,
capability,
language_registry,
+ Some(project),
Some(telemetry),
cx,
)
@@ -237,7 +237,7 @@ impl InlineAssistant {
editor: &View<Editor>,
mut range: Range<Anchor>,
initial_prompt: String,
- initial_insertion: Option<InitialInsertion>,
+ initial_transaction_id: Option<TransactionId>,
workspace: Option<WeakView<Workspace>>,
assistant_panel: Option<&View<AssistantPanel>>,
cx: &mut WindowContext,
@@ -251,28 +251,15 @@ impl InlineAssistant {
let buffer = editor.read(cx).buffer().clone();
{
let snapshot = buffer.read(cx).read(cx);
-
- let mut point_range = range.to_point(&snapshot);
- if point_range.is_empty() {
- point_range.start.column = 0;
- point_range.end.column = 0;
- } else {
- point_range.start.column = 0;
- if point_range.end.row > point_range.start.row && point_range.end.column == 0 {
- point_range.end.row -= 1;
- }
- point_range.end.column = snapshot.line_len(MultiBufferRow(point_range.end.row));
- }
-
- range.start = snapshot.anchor_before(point_range.start);
- range.end = snapshot.anchor_after(point_range.end);
+ range.start = range.start.bias_left(&snapshot);
+ range.end = range.end.bias_right(&snapshot);
}
let codegen = cx.new_model(|cx| {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
- initial_insertion,
+ initial_transaction_id,
self.telemetry.clone(),
cx,
)
@@ -873,13 +860,20 @@ impl InlineAssistant {
for assist_id in assist_ids {
if let Some(assist) = self.assists.get(assist_id) {
let codegen = assist.codegen.read(cx);
+ let buffer = codegen.buffer.read(cx).read(cx);
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
- gutter_pending_ranges
- .push(codegen.edit_position.unwrap_or(assist.range.start)..assist.range.end);
+ let pending_range =
+ codegen.edit_position.unwrap_or(assist.range.start)..assist.range.end;
+ if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
+ gutter_pending_ranges.push(pending_range);
+ }
if let Some(edit_position) = codegen.edit_position {
- gutter_transformed_ranges.push(assist.range.start..edit_position);
+ let edited_range = assist.range.start..edit_position;
+ if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
+ gutter_transformed_ranges.push(edited_range);
+ }
}
if assist.decorations.is_some() {
@@ -1997,13 +1991,13 @@ pub struct Codegen {
snapshot: MultiBufferSnapshot,
edit_position: Option<Anchor>,
last_equal_ranges: Vec<Range<Anchor>>,
- transaction_id: Option<TransactionId>,
+ initial_transaction_id: Option<TransactionId>,
+ transformation_transaction_id: Option<TransactionId>,
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
- initial_insertion: Option<InitialInsertion>,
}
enum CodegenStatus {
@@ -2027,7 +2021,7 @@ impl Codegen {
pub fn new(
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
- initial_insertion: Option<InitialInsertion>,
+ initial_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> Self {
@@ -2059,13 +2053,13 @@ impl Codegen {
edit_position: None,
snapshot,
last_equal_ranges: Default::default(),
- transaction_id: None,
+ transformation_transaction_id: None,
status: CodegenStatus::Idle,
generation: Task::ready(()),
diff: Diff::default(),
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
- initial_insertion,
+ initial_transaction_id,
}
}
@@ -2076,8 +2070,8 @@ 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.transformation_transaction_id == Some(*transaction_id) {
+ self.transformation_transaction_id = None;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Undone);
}
@@ -2105,7 +2099,7 @@ impl Codegen {
pub fn start(
&mut self,
- mut edit_range: Range<Anchor>,
+ edit_range: Range<Anchor>,
user_prompt: String,
assistant_panel_context: Option<LanguageModelRequest>,
cx: &mut ModelContext<Self>,
@@ -2114,34 +2108,13 @@ impl Codegen {
.active_model()
.context("no active model")?;
- self.undo(cx);
-
- // Handle initial insertion
- self.transaction_id = if let Some(initial_insertion) = self.initial_insertion {
+ if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() {
self.buffer.update(cx, |buffer, cx| {
- buffer.start_transaction(cx);
- let offset = edit_range.start.to_offset(&self.snapshot);
- let edit_position;
- match initial_insertion {
- InitialInsertion::NewlineBefore => {
- buffer.edit([(offset..offset, "\n\n")], None, cx);
- self.snapshot = buffer.snapshot(cx);
- edit_position = self.snapshot.anchor_after(offset + 1);
- }
- InitialInsertion::NewlineAfter => {
- buffer.edit([(offset..offset, "\n")], None, cx);
- self.snapshot = buffer.snapshot(cx);
- edit_position = self.snapshot.anchor_after(offset);
- }
- }
- self.edit_position = Some(edit_position);
- edit_range = edit_position.bias_left(&self.snapshot)..edit_position;
- buffer.end_transaction(cx)
- })
- } else {
- self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
- None
- };
+ buffer.undo_transaction(transformation_transaction_id, cx)
+ });
+ }
+
+ self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
let telemetry_id = model.telemetry_id();
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> = if user_prompt
@@ -2406,7 +2379,8 @@ impl Codegen {
});
if let Some(transaction) = transaction {
- if let Some(first_transaction) = this.transaction_id {
+ if let Some(first_transaction) = this.transformation_transaction_id
+ {
// Group all assistant edits into the first transaction.
this.buffer.update(cx, |buffer, cx| {
buffer.merge_transactions(
@@ -2416,7 +2390,7 @@ impl Codegen {
)
});
} else {
- this.transaction_id = Some(transaction);
+ this.transformation_transaction_id = Some(transaction);
this.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx)
});
@@ -2459,10 +2433,15 @@ impl Codegen {
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
- if let Some(transaction_id) = self.transaction_id.take() {
- self.buffer
- .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
- }
+ self.buffer.update(cx, |buffer, cx| {
+ if let Some(transaction_id) = self.transformation_transaction_id.take() {
+ buffer.undo_transaction(transaction_id, cx);
+ }
+
+ if let Some(transaction_id) = self.initial_transaction_id.take() {
+ buffer.undo_transaction(transaction_id, cx);
+ }
+ });
}
fn update_diff(&mut self, edit_range: Range<Anchor>, cx: &mut ModelContext<Self>) {
@@ -53,6 +53,7 @@ settings.workspace = true
similar.workspace = true
smallvec.workspace = true
smol.workspace = true
+strsim.workspace = true
sum_tree.workspace = true
task.workspace = true
text.workspace = true
@@ -1876,6 +1876,63 @@ impl Buffer {
cx.notify();
}
+ // Inserts newlines at the given position to create an empty line, returning the start of the new line.
+ // You can also request the insertion of empty lines above and below the line starting at the returned point.
+ pub fn insert_empty_line(
+ &mut self,
+ position: impl ToPoint,
+ space_above: bool,
+ space_below: bool,
+ cx: &mut ModelContext<Self>,
+ ) -> Point {
+ let mut position = position.to_point(self);
+
+ self.start_transaction();
+
+ self.edit(
+ [(position..position, "\n")],
+ Some(AutoindentMode::EachLine),
+ cx,
+ );
+
+ if position.column > 0 {
+ position += Point::new(1, 0);
+ }
+
+ if !self.is_line_blank(position.row) {
+ self.edit(
+ [(position..position, "\n")],
+ Some(AutoindentMode::EachLine),
+ cx,
+ );
+ }
+
+ if space_above {
+ if position.row > 0 && !self.is_line_blank(position.row - 1) {
+ self.edit(
+ [(position..position, "\n")],
+ Some(AutoindentMode::EachLine),
+ cx,
+ );
+ position.row += 1;
+ }
+ }
+
+ if space_below {
+ if position.row == self.max_point().row || !self.is_line_blank(position.row + 1) {
+ self.edit(
+ [(position..position, "\n")],
+ Some(AutoindentMode::EachLine),
+ cx,
+ );
+ }
+ }
+
+ self.end_transaction(cx);
+
+ position
+ }
+
/// Applies the given remote operations to the buffer.
pub fn apply_ops<I: IntoIterator<Item = Operation>>(
&mut self,
@@ -1822,6 +1822,92 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
});
}
+#[gpui::test]
+fn test_insert_empty_line(cx: &mut AppContext) {
+ init_settings(cx, |_| {});
+
+ // Insert empty line at the beginning, requesting an empty line above
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndef\nghi", cx);
+ let point = buffer.insert_empty_line(Point::new(0, 0), true, false, cx);
+ assert_eq!(buffer.text(), "\nabc\ndef\nghi");
+ assert_eq!(point, Point::new(0, 0));
+ buffer
+ });
+
+ // Insert empty line at the beginning, requesting an empty line above and below
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndef\nghi", cx);
+ let point = buffer.insert_empty_line(Point::new(0, 0), true, true, cx);
+ assert_eq!(buffer.text(), "\n\nabc\ndef\nghi");
+ assert_eq!(point, Point::new(0, 0));
+ buffer
+ });
+
+ // Insert empty line at the start of a line, requesting empty lines above and below
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndef\nghi", cx);
+ let point = buffer.insert_empty_line(Point::new(2, 0), true, true, cx);
+ assert_eq!(buffer.text(), "abc\ndef\n\n\n\nghi");
+ assert_eq!(point, Point::new(3, 0));
+ buffer
+ });
+
+ // Insert empty line in the middle of a line, requesting empty lines above and below
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
+ let point = buffer.insert_empty_line(Point::new(1, 3), true, true, cx);
+ assert_eq!(buffer.text(), "abc\ndef\n\n\n\nghi\njkl");
+ assert_eq!(point, Point::new(3, 0));
+ buffer
+ });
+
+ // Insert empty line in the middle of a line, requesting empty line above only
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
+ let point = buffer.insert_empty_line(Point::new(1, 3), true, false, cx);
+ assert_eq!(buffer.text(), "abc\ndef\n\n\nghi\njkl");
+ assert_eq!(point, Point::new(3, 0));
+ buffer
+ });
+
+ // Insert empty line in the middle of a line, requesting empty line below only
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
+ let point = buffer.insert_empty_line(Point::new(1, 3), false, true, cx);
+ assert_eq!(buffer.text(), "abc\ndef\n\n\nghi\njkl");
+ assert_eq!(point, Point::new(2, 0));
+ buffer
+ });
+
+ // Insert empty line at the end, requesting empty lines above and below
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndef\nghi", cx);
+ let point = buffer.insert_empty_line(Point::new(2, 3), true, true, cx);
+ assert_eq!(buffer.text(), "abc\ndef\nghi\n\n\n");
+ assert_eq!(point, Point::new(4, 0));
+ buffer
+ });
+
+ // Insert empty line at the end, requesting empty line above only
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndef\nghi", cx);
+ let point = buffer.insert_empty_line(Point::new(2, 3), true, false, cx);
+ assert_eq!(buffer.text(), "abc\ndef\nghi\n\n");
+ assert_eq!(point, Point::new(4, 0));
+ buffer
+ });
+
+ // Insert empty line at the end, requesting empty line below only
+ cx.new_model(|cx| {
+ let mut buffer = Buffer::local("abc\ndef\nghi", cx);
+ let point = buffer.insert_empty_line(Point::new(2, 3), false, true, cx);
+ assert_eq!(buffer.text(), "abc\ndef\nghi\n\n");
+ assert_eq!(point, Point::new(3, 0));
+ buffer
+ });
+}
+
#[gpui::test]
fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
init_settings(cx, |_| {});
@@ -1,3 +1,4 @@
+use crate::{BufferSnapshot, Point, ToPoint};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{relative, AppContext, BackgroundExecutor, HighlightStyle, StyledText, TextStyle};
use settings::Settings;
@@ -24,6 +25,27 @@ pub struct OutlineItem<T> {
pub annotation_range: Option<Range<T>>,
}
+impl<T: ToPoint> OutlineItem<T> {
+ /// Converts to an equivalent outline item, but with parameterized over Points.
+ pub fn to_point(&self, buffer: &BufferSnapshot) -> OutlineItem<Point> {
+ OutlineItem {
+ depth: self.depth,
+ range: self.range.start.to_point(buffer)..self.range.end.to_point(buffer),
+ text: self.text.clone(),
+ highlight_ranges: self.highlight_ranges.clone(),
+ name_ranges: self.name_ranges.clone(),
+ body_range: self
+ .body_range
+ .as_ref()
+ .map(|r| r.start.to_point(buffer)..r.end.to_point(buffer)),
+ annotation_range: self
+ .annotation_range
+ .as_ref()
+ .map(|r| r.start.to_point(buffer)..r.end.to_point(buffer)),
+ }
+ }
+}
+
impl<T> Outline<T> {
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
let mut candidates = Vec::new();
@@ -62,6 +84,16 @@ impl<T> Outline<T> {
}
}
+ /// Find the most similar symbol to the provided query according to the Jaro-Winkler distance measure.
+ pub fn find_most_similar(&self, query: &str) -> Option<&OutlineItem<T>> {
+ let candidate = self.path_candidates.iter().max_by(|a, b| {
+ strsim::jaro_winkler(&a.string, query)
+ .total_cmp(&strsim::jaro_winkler(&b.string, query))
+ })?;
+ Some(&self.items[candidate.id])
+ }
+
+ /// Find all outline symbols according to a longest subsequence match with the query, ordered descending by match score.
pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
let query = query.trim_start();
let is_path_query = query.contains(' ');
@@ -37,6 +37,7 @@ log.workspace = true
menu.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
+parking_lot.workspace = true
proto = { workspace = true, features = ["test-support"] }
project.workspace = true
schemars.workspace = true
@@ -75,6 +75,11 @@ pub trait LanguageModel: Send + Sync {
schema: serde_json::Value,
cx: &AsyncAppContext,
) -> BoxFuture<'static, Result<serde_json::Value>>;
+
+ #[cfg(any(test, feature = "test-support"))]
+ fn as_fake(&self) -> &provider::fake::FakeLanguageModel {
+ unimplemented!()
+ }
}
impl dyn LanguageModel {
@@ -3,15 +3,17 @@ use crate::{
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest,
};
-use anyhow::anyhow;
-use collections::HashMap;
-use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
+use anyhow::Context as _;
+use futures::{
+ channel::{mpsc, oneshot},
+ future::BoxFuture,
+ stream::BoxStream,
+ FutureExt, StreamExt,
+};
use gpui::{AnyView, AppContext, AsyncAppContext, Task};
use http_client::Result;
-use std::{
- future,
- sync::{Arc, Mutex},
-};
+use parking_lot::Mutex;
+use std::sync::Arc;
use ui::WindowContext;
pub fn language_model_id() -> LanguageModelId {
@@ -31,9 +33,7 @@ pub fn provider_name() -> LanguageModelProviderName {
}
#[derive(Clone, Default)]
-pub struct FakeLanguageModelProvider {
- current_completion_txs: Arc<Mutex<HashMap<String, mpsc::UnboundedSender<String>>>>,
-}
+pub struct FakeLanguageModelProvider;
impl LanguageModelProviderState for FakeLanguageModelProvider {
type ObservableEntity = ();
@@ -53,9 +53,7 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
}
fn provided_models(&self, _: &AppContext) -> Vec<Arc<dyn LanguageModel>> {
- vec![Arc::new(FakeLanguageModel {
- current_completion_txs: self.current_completion_txs.clone(),
- })]
+ vec![Arc::new(FakeLanguageModel::default())]
}
fn is_authenticated(&self, _: &AppContext) -> bool {
@@ -77,55 +75,80 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
impl FakeLanguageModelProvider {
pub fn test_model(&self) -> FakeLanguageModel {
- FakeLanguageModel {
- current_completion_txs: self.current_completion_txs.clone(),
- }
+ FakeLanguageModel::default()
}
}
+#[derive(Debug, PartialEq)]
+pub struct ToolUseRequest {
+ pub request: LanguageModelRequest,
+ pub name: String,
+ pub description: String,
+ pub schema: serde_json::Value,
+}
+
+#[derive(Default)]
pub struct FakeLanguageModel {
- current_completion_txs: Arc<Mutex<HashMap<String, mpsc::UnboundedSender<String>>>>,
+ current_completion_txs: Mutex<Vec<(LanguageModelRequest, mpsc::UnboundedSender<String>)>>,
+ current_tool_use_txs: Mutex<Vec<(ToolUseRequest, oneshot::Sender<Result<serde_json::Value>>)>>,
}
impl FakeLanguageModel {
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
self.current_completion_txs
.lock()
- .unwrap()
- .keys()
- .map(|k| serde_json::from_str(k).unwrap())
+ .iter()
+ .map(|(request, _)| request.clone())
.collect()
}
pub fn completion_count(&self) -> usize {
- self.current_completion_txs.lock().unwrap().len()
+ self.current_completion_txs.lock().len()
}
- pub fn send_completion_chunk(&self, request: &LanguageModelRequest, chunk: String) {
- let json = serde_json::to_string(request).unwrap();
+ pub fn stream_completion_response(&self, request: &LanguageModelRequest, chunk: String) {
+ let current_completion_txs = self.current_completion_txs.lock();
+ let tx = current_completion_txs
+ .iter()
+ .find(|(req, _)| req == request)
+ .map(|(_, tx)| tx)
+ .unwrap();
+ tx.unbounded_send(chunk).unwrap();
+ }
+
+ pub fn end_completion_stream(&self, request: &LanguageModelRequest) {
self.current_completion_txs
.lock()
- .unwrap()
- .get(&json)
- .unwrap()
- .unbounded_send(chunk)
- .unwrap();
+ .retain(|(req, _)| req != request);
}
- pub fn send_last_completion_chunk(&self, chunk: String) {
- self.send_completion_chunk(self.pending_completions().last().unwrap(), chunk);
+ pub fn stream_last_completion_response(&self, chunk: String) {
+ self.stream_completion_response(self.pending_completions().last().unwrap(), chunk);
}
- pub fn finish_completion(&self, request: &LanguageModelRequest) {
- self.current_completion_txs
- .lock()
- .unwrap()
- .remove(&serde_json::to_string(request).unwrap())
- .unwrap();
+ pub fn end_last_completion_stream(&self) {
+ self.end_completion_stream(self.pending_completions().last().unwrap());
+ }
+
+ pub fn respond_to_tool_use(
+ &self,
+ tool_call: &ToolUseRequest,
+ response: Result<serde_json::Value>,
+ ) {
+ let mut current_tool_call_txs = self.current_tool_use_txs.lock();
+ if let Some(index) = current_tool_call_txs
+ .iter()
+ .position(|(call, _)| call == tool_call)
+ {
+ let (_, tx) = current_tool_call_txs.remove(index);
+ tx.send(response).unwrap();
+ }
}
- pub fn finish_last_completion(&self) {
- self.finish_completion(self.pending_completions().last().unwrap());
+ pub fn respond_to_last_tool_use(&self, response: Result<serde_json::Value>) {
+ let mut current_tool_call_txs = self.current_tool_use_txs.lock();
+ let (_, tx) = current_tool_call_txs.pop().unwrap();
+ tx.send(response).unwrap();
}
}
@@ -168,21 +191,30 @@ impl LanguageModel for FakeLanguageModel {
_: &AsyncAppContext,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let (tx, rx) = mpsc::unbounded();
- self.current_completion_txs
- .lock()
- .unwrap()
- .insert(serde_json::to_string(&request).unwrap(), tx);
+ self.current_completion_txs.lock().push((request, tx));
async move { Ok(rx.map(Ok).boxed()) }.boxed()
}
fn use_any_tool(
&self,
- _request: LanguageModelRequest,
- _name: String,
- _description: String,
- _schema: serde_json::Value,
+ request: LanguageModelRequest,
+ name: String,
+ description: String,
+ schema: serde_json::Value,
_cx: &AsyncAppContext,
) -> BoxFuture<'static, Result<serde_json::Value>> {
- future::ready(Err(anyhow!("not implemented"))).boxed()
+ let (tx, rx) = oneshot::channel();
+ let tool_call = ToolUseRequest {
+ request,
+ name,
+ description,
+ schema,
+ };
+ self.current_tool_use_txs.lock().push((tool_call, tx));
+ async move { rx.await.context("FakeLanguageModel was dropped")? }.boxed()
+ }
+
+ fn as_fake(&self) -> &Self {
+ self
}
}
@@ -103,7 +103,7 @@ impl LanguageModelRegistry {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> crate::provider::fake::FakeLanguageModelProvider {
- let fake_provider = crate::provider::fake::FakeLanguageModelProvider::default();
+ let fake_provider = crate::provider::fake::FakeLanguageModelProvider;
let registry = cx.new_model(|cx| {
let mut registry = Self::default();
registry.register_provider(fake_provider.clone(), cx);
@@ -239,7 +239,7 @@ mod tests {
let registry = cx.new_model(|_| LanguageModelRegistry::default());
registry.update(cx, |registry, cx| {
- registry.register_provider(FakeLanguageModelProvider::default(), cx);
+ registry.register_provider(FakeLanguageModelProvider, cx);
});
let providers = registry.read(cx).providers();
@@ -1,13 +1,13 @@
use crate::role::Role;
use serde::{Deserialize, Serialize};
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
pub struct LanguageModelRequestMessage {
pub role: Role,
pub content: String,
}
-#[derive(Debug, Default, Serialize, Deserialize)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct LanguageModelRequest {
pub messages: Vec<LanguageModelRequestMessage>,
pub stop: Vec<String>,
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
-#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
@@ -742,6 +742,33 @@ impl MultiBuffer {
tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
}
+ // Inserts newlines at the given position to create an empty line, returning the start of the new line.
+ // You can also request the insertion of empty lines above and below the line starting at the returned point.
+ // Panics if the given position is invalid.
+ pub fn insert_empty_line(
+ &mut self,
+ position: impl ToPoint,
+ space_above: bool,
+ space_below: bool,
+ cx: &mut ModelContext<Self>,
+ ) -> Point {
+ let multibuffer_point = position.to_point(&self.read(cx));
+ if let Some(buffer) = self.as_singleton() {
+ buffer.update(cx, |buffer, cx| {
+ buffer.insert_empty_line(multibuffer_point, space_above, space_below, cx)
+ })
+ } else {
+ let (buffer, buffer_point, _) =
+ self.point_to_buffer_point(multibuffer_point, cx).unwrap();
+ self.start_transaction(cx);
+ let empty_line_start = buffer.update(cx, |buffer, cx| {
+ buffer.insert_empty_line(buffer_point, space_above, space_below, cx)
+ });
+ self.end_transaction(cx);
+ multibuffer_point + (empty_line_start - buffer_point)
+ }
+ }
+
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
self.start_transaction_at(Instant::now(), cx)
}
@@ -1448,6 +1475,29 @@ impl MultiBuffer {
})
}
+ // If point is at the end of the buffer, the last excerpt is returned
+ pub fn point_to_buffer_point<T: ToPoint>(
+ &self,
+ point: T,
+ cx: &AppContext,
+ ) -> Option<(Model<Buffer>, Point, ExcerptId)> {
+ let snapshot = self.read(cx);
+ let point = point.to_point(&snapshot);
+ let mut cursor = snapshot.excerpts.cursor::<Point>();
+ cursor.seek(&point, Bias::Right, &());
+ if cursor.item().is_none() {
+ cursor.prev(&());
+ }
+
+ cursor.item().map(|excerpt| {
+ let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
+ let buffer_point = excerpt_start + point - *cursor.start();
+ let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
+
+ (buffer, buffer_point, excerpt.id)
+ })
+ }
+
pub fn range_to_buffer_ranges<T: ToOffset>(
&self,
range: Range<T>,
@@ -136,7 +136,6 @@ 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> {
@@ -146,8 +145,4 @@ 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()
- }
}