Compare commits
18 Commits
acp-rewind
...
store-reso
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
104b5ef882 | ||
|
|
b6ab4c2d65 | ||
|
|
a4b9d034bc | ||
|
|
8eaaaf7d45 | ||
|
|
e2b0c1fc9e | ||
|
|
9710542ce2 | ||
|
|
b5f7bf1cb5 | ||
|
|
9c591fc571 | ||
|
|
0b702273e3 | ||
|
|
ecf4dbad60 | ||
|
|
91e4026fd6 | ||
|
|
6207c8a3e2 | ||
|
|
bb15bd76e6 | ||
|
|
440967dae9 | ||
|
|
8dfc3a38e4 | ||
|
|
863981bcbd | ||
|
|
0f85facb0e | ||
|
|
6f3b7158d0 |
@@ -1,6 +1,5 @@
|
||||
use crate::slash_command::file_command::codeblock_fence_for_path;
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
humanize_token_count,
|
||||
@@ -13,14 +12,15 @@ use crate::{
|
||||
},
|
||||
slash_command_picker,
|
||||
terminal_inline_assistant::TerminalInlineAssistant,
|
||||
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
|
||||
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
|
||||
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
|
||||
InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
|
||||
MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext,
|
||||
ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
|
||||
RequestType, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
|
||||
Assist, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context, ContextEvent,
|
||||
ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory,
|
||||
DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles, InsertIntoEditor,
|
||||
InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
|
||||
MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, ParsedSlashCommand,
|
||||
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, RequestType,
|
||||
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
|
||||
};
|
||||
use crate::{PatchId, ResolvedPatch, ToolWorkingSet};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||
use client::{proto, zed_urls, Client, Status};
|
||||
@@ -1481,13 +1481,15 @@ struct ScrollPosition {
|
||||
|
||||
struct PatchViewState {
|
||||
crease_id: CreaseId,
|
||||
multibuffer_range: Range<Anchor>,
|
||||
height: u32,
|
||||
editor: Option<PatchEditorState>,
|
||||
update_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
struct PatchEditorState {
|
||||
editor: WeakView<ProposedChangesEditor>,
|
||||
opened_patch: AssistantPatch,
|
||||
opened_patch: ResolvedPatch,
|
||||
}
|
||||
|
||||
type MessageHeader = MessageMetadata;
|
||||
@@ -1517,8 +1519,8 @@ pub struct ContextEditor {
|
||||
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
|
||||
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
patches: HashMap<Range<language::Anchor>, PatchViewState>,
|
||||
active_patch: Option<Range<language::Anchor>>,
|
||||
patches: HashMap<PatchId, PatchViewState>,
|
||||
active_patch: Option<PatchId>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
last_error: Option<AssistError>,
|
||||
show_accept_terms: bool,
|
||||
@@ -1573,7 +1575,7 @@ impl ContextEditor {
|
||||
];
|
||||
|
||||
let sections = context.read(cx).slash_command_output_sections().to_vec();
|
||||
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
|
||||
let patch_ids = context.read(cx).patch_ids().collect::<Vec<_>>();
|
||||
let slash_commands = context.read(cx).slash_commands.clone();
|
||||
let tools = context.read(cx).tools.clone();
|
||||
let mut this = Self {
|
||||
@@ -1604,7 +1606,9 @@ impl ContextEditor {
|
||||
this.update_message_headers(cx);
|
||||
this.update_image_blocks(cx);
|
||||
this.insert_slash_command_output_sections(sections, false, cx);
|
||||
this.patches_updated(&Vec::new(), &patch_ranges, cx);
|
||||
for patch_id in patch_ids {
|
||||
this.patch_updated(patch_id, cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
@@ -1636,7 +1640,7 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Some((_range, patch)) = self.active_patch() {
|
||||
if let Some(patch) = self.active_patch() {
|
||||
if let Some(editor) = patch
|
||||
.editor
|
||||
.as_ref()
|
||||
@@ -1956,9 +1960,8 @@ impl ContextEditor {
|
||||
);
|
||||
});
|
||||
}
|
||||
ContextEvent::PatchesUpdated { removed, updated } => {
|
||||
self.patches_updated(removed, updated, cx);
|
||||
}
|
||||
ContextEvent::PatchUpdated(patch_id) => self.patch_updated(*patch_id, cx),
|
||||
ContextEvent::PatchRemoved(patch_id) => self.patch_removed(*patch_id, cx),
|
||||
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -2220,125 +2223,124 @@ impl ContextEditor {
|
||||
});
|
||||
}
|
||||
|
||||
fn patches_updated(
|
||||
&mut self,
|
||||
removed: &Vec<Range<text::Anchor>>,
|
||||
updated: &Vec<Range<text::Anchor>>,
|
||||
cx: &mut ViewContext<ContextEditor>,
|
||||
) {
|
||||
fn patch_updated(&mut self, patch_id: PatchId, cx: &mut ViewContext<ContextEditor>) {
|
||||
let this = cx.view().downgrade();
|
||||
let mut editors_to_close = Vec::new();
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let multibuffer = &snapshot.buffer_snapshot;
|
||||
let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
|
||||
|
||||
let mut removed_crease_ids = Vec::new();
|
||||
let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
|
||||
for range in removed {
|
||||
if let Some(state) = self.patches.remove(range) {
|
||||
let patch_start = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.start)
|
||||
.unwrap();
|
||||
let patch_end = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.end)
|
||||
.unwrap();
|
||||
let Some(patch) = self.context.read(cx).patch_for_id(patch_id, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
|
||||
ranges_to_unfold.push(patch_start..patch_end);
|
||||
removed_crease_ids.push(state.crease_id);
|
||||
let path_count = patch.path_count();
|
||||
let patch_start = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.start)
|
||||
.unwrap();
|
||||
let patch_end = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.end)
|
||||
.unwrap();
|
||||
let render_block: RenderBlock = Arc::new({
|
||||
let this = this.clone();
|
||||
move |cx: &mut BlockContext<'_, '_>| {
|
||||
let max_width = cx.max_width;
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
let selected = cx.selected;
|
||||
this.update(&mut **cx, |this, cx| {
|
||||
this.render_patch_block(
|
||||
patch_id,
|
||||
max_width,
|
||||
gutter_width,
|
||||
block_id,
|
||||
selected,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Empty.into_any())
|
||||
}
|
||||
});
|
||||
|
||||
let mut height = path_count as u32 + 1;
|
||||
if patch.status == AssistantPatchStatus::Pending {
|
||||
height += 1;
|
||||
}
|
||||
editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
|
||||
editor.remove_creases(removed_crease_ids, cx);
|
||||
|
||||
for range in updated {
|
||||
let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
|
||||
continue;
|
||||
};
|
||||
let crease = Crease::block(
|
||||
patch_start..patch_end,
|
||||
height,
|
||||
BlockStyle::Flex,
|
||||
render_block.clone(),
|
||||
);
|
||||
|
||||
let path_count = patch.path_count();
|
||||
let patch_start = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.start)
|
||||
.unwrap();
|
||||
let patch_end = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.end)
|
||||
.unwrap();
|
||||
let render_block: RenderBlock = Arc::new({
|
||||
let this = this.clone();
|
||||
let patch_range = range.clone();
|
||||
move |cx: &mut BlockContext<'_, '_>| {
|
||||
let max_width = cx.max_width;
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
let selected = cx.selected;
|
||||
this.update(&mut **cx, |this, cx| {
|
||||
this.render_patch_block(
|
||||
patch_range.clone(),
|
||||
max_width,
|
||||
gutter_width,
|
||||
block_id,
|
||||
selected,
|
||||
cx,
|
||||
)
|
||||
let should_refold;
|
||||
if let Some(state) = self.patches.get_mut(&patch_id) {
|
||||
if state.multibuffer_range != (patch_start..patch_end) || state.height != height {
|
||||
editor.remove_creases([state.crease_id], cx);
|
||||
state.crease_id = editor.insert_creases([crease.clone()], cx)[0];
|
||||
state.height = height;
|
||||
state.multibuffer_range = patch_start..patch_end;
|
||||
should_refold = snapshot.intersects_fold(patch_start.to_offset(&multibuffer));
|
||||
} else {
|
||||
should_refold = false;
|
||||
}
|
||||
|
||||
if state.editor.is_some() {
|
||||
let resolved_patch = self.context.read(cx).resolve_patch(patch_id, cx);
|
||||
state.update_task = Some({
|
||||
let this = this.clone();
|
||||
cx.spawn(|_, cx| async move {
|
||||
if let Some(resolved_patch) = resolved_patch.await.log_err() {
|
||||
Self::update_patch_editor(
|
||||
this.clone(),
|
||||
patch_id,
|
||||
resolved_patch,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Empty.into_any())
|
||||
}
|
||||
});
|
||||
|
||||
let height = path_count as u32 + 1;
|
||||
let crease = Crease::block(
|
||||
patch_start..patch_end,
|
||||
height,
|
||||
BlockStyle::Flex,
|
||||
render_block.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
|
||||
self.patches.insert(
|
||||
patch_id,
|
||||
PatchViewState {
|
||||
crease_id,
|
||||
multibuffer_range: patch_start..patch_end,
|
||||
height,
|
||||
editor: None,
|
||||
update_task: None,
|
||||
},
|
||||
);
|
||||
|
||||
let should_refold;
|
||||
if let Some(state) = self.patches.get_mut(&range) {
|
||||
if let Some(editor_state) = &state.editor {
|
||||
if editor_state.opened_patch != patch {
|
||||
state.update_task = Some({
|
||||
let this = this.clone();
|
||||
cx.spawn(|_, cx| async move {
|
||||
Self::update_patch_editor(this.clone(), patch, cx)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
should_refold = true;
|
||||
}
|
||||
|
||||
should_refold =
|
||||
snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
|
||||
} else {
|
||||
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
|
||||
self.patches.insert(
|
||||
range.clone(),
|
||||
PatchViewState {
|
||||
crease_id,
|
||||
editor: None,
|
||||
update_task: None,
|
||||
},
|
||||
);
|
||||
|
||||
should_refold = true;
|
||||
}
|
||||
|
||||
if should_refold {
|
||||
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
|
||||
editor.fold_creases(vec![crease], false, cx);
|
||||
}
|
||||
if should_refold {
|
||||
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
|
||||
editor.fold_creases(vec![crease], false, cx);
|
||||
}
|
||||
});
|
||||
self.update_active_patch(cx);
|
||||
}
|
||||
|
||||
for editor in editors_to_close {
|
||||
fn patch_removed(&mut self, patch_id: PatchId, cx: &mut ViewContext<ContextEditor>) {
|
||||
let Some(state) = self.patches.remove(&patch_id) else {
|
||||
return;
|
||||
};
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.unfold_ranges(&[state.multibuffer_range], true, false, cx);
|
||||
editor.remove_creases([state.crease_id], cx);
|
||||
});
|
||||
|
||||
if let Some(editor) = state.editor.and_then(|state| state.editor.upgrade()) {
|
||||
self.close_patch_editor(editor, cx);
|
||||
}
|
||||
|
||||
self.update_active_patch(cx);
|
||||
}
|
||||
|
||||
@@ -2419,9 +2421,8 @@ impl ContextEditor {
|
||||
cx.emit(event.clone());
|
||||
}
|
||||
|
||||
fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
|
||||
let patch = self.active_patch.as_ref()?;
|
||||
Some((patch.clone(), self.patches.get(&patch)?))
|
||||
fn active_patch(&self) -> Option<&PatchViewState> {
|
||||
self.patches.get(self.active_patch.as_ref()?)
|
||||
}
|
||||
|
||||
fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -2430,9 +2431,8 @@ impl ContextEditor {
|
||||
});
|
||||
let context = self.context.read(cx);
|
||||
|
||||
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
|
||||
|
||||
if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
|
||||
let new_patch_id = context.patch_containing(newest_cursor, cx);
|
||||
if new_patch_id == self.active_patch {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2446,16 +2446,14 @@ impl ContextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_patch) = new_patch {
|
||||
self.active_patch = Some(new_patch.range.clone());
|
||||
if let Some(patch_id) = new_patch_id {
|
||||
self.active_patch = Some(patch_id);
|
||||
|
||||
if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
|
||||
let mut editor = None;
|
||||
if let Some(state) = &patch_state.editor {
|
||||
if let Some(opened_editor) = state.editor.upgrade() {
|
||||
editor = Some(opened_editor);
|
||||
}
|
||||
}
|
||||
if let Some(patch_state) = self.patches.get_mut(&patch_id) {
|
||||
let editor = patch_state
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|state| state.editor.upgrade());
|
||||
|
||||
if let Some(editor) = editor {
|
||||
self.workspace
|
||||
@@ -2464,8 +2462,13 @@ impl ContextEditor {
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
let resolved_patch = self.context.read(cx).resolve_patch(patch_id, cx);
|
||||
patch_state.update_task = Some(cx.spawn(move |this, cx| async move {
|
||||
Self::open_patch_editor(this, new_patch, cx).await.log_err();
|
||||
if let Some(resolved_patch) = resolved_patch.await.log_err() {
|
||||
Self::open_patch_editor(this, patch_id, resolved_patch, cx)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -2494,35 +2497,25 @@ impl ContextEditor {
|
||||
|
||||
async fn open_patch_editor(
|
||||
this: WeakView<Self>,
|
||||
patch: AssistantPatch,
|
||||
patch_id: PatchId,
|
||||
patch: ResolvedPatch,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let project = this.update(&mut cx, |this, _| this.project.clone())?;
|
||||
let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let editor = ProposedChangesEditor::new(
|
||||
patch.title.clone(),
|
||||
resolved_patch
|
||||
.edit_groups
|
||||
.iter()
|
||||
.map(|(buffer, groups)| ProposedChangeLocation {
|
||||
buffer: buffer.clone(),
|
||||
ranges: groups
|
||||
.iter()
|
||||
.map(|group| group.context_range.clone())
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
let mut editor = ProposedChangesEditor::new(
|
||||
SharedString::from(""),
|
||||
Vec::<ProposedChangeLocation<usize>>::new(),
|
||||
Some(project.clone()),
|
||||
cx,
|
||||
);
|
||||
resolved_patch.apply(&editor, cx);
|
||||
patch.apply(&mut editor, None, cx);
|
||||
editor
|
||||
})?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(patch_state) = this.patches.get_mut(&patch.range) {
|
||||
if let Some(patch_state) = this.patches.get_mut(&patch_id) {
|
||||
patch_state.editor = Some(PatchEditorState {
|
||||
editor: editor.downgrade(),
|
||||
opened_patch: patch,
|
||||
@@ -2540,34 +2533,18 @@ impl ContextEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_patch_editor(
|
||||
fn update_patch_editor(
|
||||
this: WeakView<Self>,
|
||||
patch: AssistantPatch,
|
||||
patch_id: PatchId,
|
||||
patch: ResolvedPatch,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let project = this.update(&mut cx, |this, _| this.project.clone())?;
|
||||
let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let patch_state = this.patches.get_mut(&patch.range)?;
|
||||
|
||||
let locations = resolved_patch
|
||||
.edit_groups
|
||||
.iter()
|
||||
.map(|(buffer, groups)| ProposedChangeLocation {
|
||||
buffer: buffer.clone(),
|
||||
ranges: groups
|
||||
.iter()
|
||||
.map(|group| group.context_range.clone())
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let patch_state = this.patches.get_mut(&patch_id)?;
|
||||
if let Some(state) = &mut patch_state.editor {
|
||||
if let Some(editor) = state.editor.upgrade() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_title(patch.title.clone(), cx);
|
||||
editor.reset_locations(locations, cx);
|
||||
resolved_patch.apply(editor, cx);
|
||||
patch.apply(editor, Some(&state.opened_patch), cx);
|
||||
});
|
||||
|
||||
state.opened_patch = patch;
|
||||
@@ -2576,7 +2553,6 @@ impl ContextEditor {
|
||||
}
|
||||
}
|
||||
patch_state.update_task.take();
|
||||
|
||||
Some(())
|
||||
})?;
|
||||
Ok(())
|
||||
@@ -3451,23 +3427,16 @@ impl ContextEditor {
|
||||
|
||||
fn render_patch_block(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
patch_id: PatchId,
|
||||
max_width: Pixels,
|
||||
gutter_width: Pixels,
|
||||
id: BlockId,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
|
||||
let excerpt_id = *excerpt_id;
|
||||
let anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)
|
||||
.unwrap();
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
|
||||
let patch = self.context.read(cx).patch_for_id(patch_id, cx)?;
|
||||
let anchor = self.patches.get(&patch_id)?.multibuffer_range.start;
|
||||
let paths = patch
|
||||
.paths()
|
||||
.map(|p| SharedString::from(p.to_string()))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
|
||||
use crate::patch::{PatchId, PatchStore};
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
prompts::PromptBuilder,
|
||||
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
||||
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
|
||||
};
|
||||
use crate::{PatchStoreEvent, ResolvedPatch, ToolWorkingSet};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
|
||||
@@ -367,10 +368,8 @@ pub enum ContextEvent {
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
StreamedCompletion,
|
||||
PatchesUpdated {
|
||||
removed: Vec<Range<language::Anchor>>,
|
||||
updated: Vec<Range<language::Anchor>>,
|
||||
},
|
||||
PatchUpdated(PatchId),
|
||||
PatchRemoved(PatchId),
|
||||
InvokedSlashCommandChanged {
|
||||
command_id: InvokedSlashCommandId,
|
||||
},
|
||||
@@ -560,9 +559,10 @@ pub struct Context {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
patches: Vec<AssistantPatch>,
|
||||
patches: Vec<(Range<text::Anchor>, PatchId)>,
|
||||
patch_store: Model<PatchStore>,
|
||||
xml_tags: Vec<XmlTag>,
|
||||
project: Option<Model<Project>>,
|
||||
project: Model<Project>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
@@ -576,9 +576,9 @@ impl ContextAnnotation for ParsedSlashCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextAnnotation for AssistantPatch {
|
||||
impl ContextAnnotation for (Range<language::Anchor>, PatchId) {
|
||||
fn range(&self) -> &Range<language::Anchor> {
|
||||
&self.range
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +593,7 @@ impl EventEmitter<ContextEvent> for Context {}
|
||||
impl Context {
|
||||
pub fn local(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Option<Model<Project>>,
|
||||
project: Model<Project>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
@@ -623,7 +623,7 @@ impl Context {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
project: Option<Model<Project>>,
|
||||
project: Model<Project>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
@@ -639,6 +639,14 @@ impl Context {
|
||||
});
|
||||
let edits_since_last_slash_command_parse =
|
||||
buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let patch_store = cx.new_model(|_| PatchStore::new(project.clone()));
|
||||
cx.subscribe(&patch_store, |_, _, event, cx| {
|
||||
cx.emit(match event {
|
||||
PatchStoreEvent::PatchUpdated(id) => ContextEvent::PatchUpdated(*id),
|
||||
PatchStoreEvent::PatchRemoved(id) => ContextEvent::PatchRemoved(*id),
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
let mut this = Self {
|
||||
id,
|
||||
timestamp: clock::Lamport::new(replica_id),
|
||||
@@ -663,6 +671,7 @@ impl Context {
|
||||
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
|
||||
pending_save: Task::ready(Ok(())),
|
||||
path: None,
|
||||
patch_store,
|
||||
buffer,
|
||||
telemetry,
|
||||
project,
|
||||
@@ -746,7 +755,7 @@ impl Context {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
project: Option<Model<Project>>,
|
||||
project: Model<Project>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
@@ -1029,7 +1038,7 @@ impl Context {
|
||||
self.language_registry.clone()
|
||||
}
|
||||
|
||||
pub fn project(&self) -> Option<Model<Project>> {
|
||||
pub fn project(&self) -> Model<Project> {
|
||||
self.project.clone()
|
||||
}
|
||||
|
||||
@@ -1045,50 +1054,46 @@ impl Context {
|
||||
self.summary.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn patch_containing(
|
||||
&self,
|
||||
pub(crate) fn patch_containing<'a>(
|
||||
&'a self,
|
||||
position: Point,
|
||||
cx: &AppContext,
|
||||
) -> Option<&AssistantPatch> {
|
||||
cx: &'a AppContext,
|
||||
) -> Option<PatchId> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self.patches.binary_search_by(|patch| {
|
||||
let patch_range = patch.range.to_point(&buffer);
|
||||
if position < patch_range.start {
|
||||
Ordering::Greater
|
||||
} else if position > patch_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
if let Ok(ix) = index {
|
||||
Some(&self.patches[ix])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let index = self
|
||||
.patches
|
||||
.binary_search_by(|patch| {
|
||||
let patch_range = patch.0.to_point(&buffer);
|
||||
if position < patch_range.start {
|
||||
Ordering::Greater
|
||||
} else if position > patch_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(self.patches[index].1)
|
||||
}
|
||||
|
||||
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
||||
self.patches.iter().map(|patch| patch.range.clone())
|
||||
pub fn patch_ids(&self) -> impl Iterator<Item = PatchId> + '_ {
|
||||
self.patches.iter().map(|patch| patch.1)
|
||||
}
|
||||
|
||||
pub(crate) fn patch_for_range(
|
||||
&self,
|
||||
range: &Range<language::Anchor>,
|
||||
cx: &AppContext,
|
||||
pub(crate) fn patch_for_id<'a>(
|
||||
&'a self,
|
||||
id: PatchId,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<&AssistantPatch> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self.patch_index_for_range(range, buffer).ok()?;
|
||||
Some(&self.patches[index])
|
||||
self.patch_store.read(cx).get(id)
|
||||
}
|
||||
|
||||
fn patch_index_for_range(
|
||||
pub(crate) fn resolve_patch(
|
||||
&self,
|
||||
tagged_range: &Range<text::Anchor>,
|
||||
buffer: &text::BufferSnapshot,
|
||||
) -> Result<usize, usize> {
|
||||
self.patches
|
||||
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
||||
id: PatchId,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<ResolvedPatch>> {
|
||||
self.patch_store.read(cx).resolve_patch(id, cx)
|
||||
}
|
||||
|
||||
pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
|
||||
@@ -1386,8 +1391,6 @@ impl Context {
|
||||
|
||||
let mut removed_parsed_slash_command_ranges = Vec::new();
|
||||
let mut updated_parsed_slash_commands = Vec::new();
|
||||
let mut removed_patches = Vec::new();
|
||||
let mut updated_patches = Vec::new();
|
||||
while let Some(mut row_range) = row_ranges.next() {
|
||||
while let Some(next_row_range) = row_ranges.peek() {
|
||||
if row_range.end >= next_row_range.start {
|
||||
@@ -1412,13 +1415,7 @@ impl Context {
|
||||
cx,
|
||||
);
|
||||
self.invalidate_pending_slash_commands(&buffer, cx);
|
||||
self.reparse_patches_in_range(
|
||||
start..end,
|
||||
&buffer,
|
||||
&mut updated_patches,
|
||||
&mut removed_patches,
|
||||
cx,
|
||||
);
|
||||
self.reparse_patches_in_range(start..end, &buffer, cx);
|
||||
}
|
||||
|
||||
if !updated_parsed_slash_commands.is_empty()
|
||||
@@ -1429,13 +1426,6 @@ impl Context {
|
||||
updated: updated_parsed_slash_commands,
|
||||
});
|
||||
}
|
||||
|
||||
if !updated_patches.is_empty() || !removed_patches.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated {
|
||||
removed: removed_patches,
|
||||
updated: updated_patches,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_slash_commands_in_range(
|
||||
@@ -1530,8 +1520,6 @@ impl Context {
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer: &BufferSnapshot,
|
||||
updated: &mut Vec<Range<text::Anchor>>,
|
||||
removed: &mut Vec<Range<text::Anchor>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// Rebuild the XML tags in the edited range.
|
||||
@@ -1547,13 +1535,13 @@ impl Context {
|
||||
|
||||
// Reparse all tags after the last unchanged patch before the change.
|
||||
let mut tags_start_ix = 0;
|
||||
if let Some(preceding_unchanged_patch) =
|
||||
if let Some((preceding_unchanged_patch_range, _)) =
|
||||
self.patches[..intersecting_patches_range.start].last()
|
||||
{
|
||||
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||
tag.range
|
||||
.start
|
||||
.cmp(&preceding_unchanged_patch.range.end, buffer)
|
||||
.cmp(&preceding_unchanged_patch_range.end, buffer)
|
||||
.then(Ordering::Less)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
@@ -1562,13 +1550,28 @@ impl Context {
|
||||
|
||||
// Rebuild the patches in the range.
|
||||
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
|
||||
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
|
||||
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
|
||||
removed.extend(
|
||||
removed_patches
|
||||
.map(|patch| patch.range)
|
||||
.filter(|range| !updated.contains(&range)),
|
||||
);
|
||||
self.patch_store.update(cx, |patch_store, cx| {
|
||||
let mut removed_entries = self.patches[intersecting_patches_range.clone()]
|
||||
.iter()
|
||||
.map(|(range, id)| (range.start, *id))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let added_entries = new_patches.into_iter().map(|patch| {
|
||||
let id;
|
||||
let range = patch.range.clone();
|
||||
if let Some(existing_id) = removed_entries.remove(&range.start) {
|
||||
id = existing_id;
|
||||
patch_store.update(id, patch, cx).ok();
|
||||
} else {
|
||||
id = patch_store.insert(patch.clone(), cx);
|
||||
};
|
||||
(range, id)
|
||||
});
|
||||
self.patches
|
||||
.splice(intersecting_patches_range, added_entries);
|
||||
for id in removed_entries.into_values() {
|
||||
patch_store.remove(id, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_xml_tags_in_range(
|
||||
@@ -1646,7 +1649,7 @@ impl Context {
|
||||
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
|
||||
patch_tag_depth += 1;
|
||||
let patch_start = tag.range.start;
|
||||
let mut edits = Vec::<Result<AssistantEdit>>::new();
|
||||
let mut edits = Vec::<AssistantEdit>::new();
|
||||
let mut patch = AssistantPatch {
|
||||
range: patch_start..patch_start,
|
||||
title: String::new().into(),
|
||||
@@ -1658,7 +1661,7 @@ impl Context {
|
||||
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
|
||||
patch_tag_depth -= 1;
|
||||
if patch_tag_depth == 0 {
|
||||
patch.range.end = tag.range.end;
|
||||
patch.range.end = tag.range.end.bias_right(buffer);
|
||||
|
||||
// Include the line immediately after this <patch> tag if it's empty.
|
||||
let patch_end_offset = patch.range.end.to_offset(buffer);
|
||||
@@ -1675,13 +1678,7 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
if let (Ok(a), Ok(b)) = (a, b) {
|
||||
a.path.cmp(&b.path)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
edits.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
patch.edits = edits.into();
|
||||
patch.status = AssistantPatchStatus::Ready;
|
||||
new_patches.push(patch);
|
||||
@@ -1711,13 +1708,17 @@ impl Context {
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||
edits.push(AssistantEdit::new(
|
||||
if let Some(edit) = AssistantEdit::new(
|
||||
path,
|
||||
operation,
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
));
|
||||
)
|
||||
.log_err()
|
||||
{
|
||||
edits.push(edit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2601,14 +2602,8 @@ impl Context {
|
||||
}
|
||||
|
||||
let buffer = self.buffer.read(cx).text_snapshot();
|
||||
let mut updated = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
for range in ranges {
|
||||
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||
}
|
||||
|
||||
if !updated.is_empty() || !removed.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
|
||||
self.reparse_patches_in_range(range, &buffer, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{AssistantEdit, MessageCacheMetadata};
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
||||
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
|
||||
MessageStatus, PromptBuilder,
|
||||
};
|
||||
use crate::{PatchId, ToolWorkingSet};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
|
||||
@@ -26,6 +26,7 @@ use project::Project;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::mem;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env,
|
||||
@@ -45,23 +46,7 @@ use workspace::Workspace;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::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 prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let context = init_test(cx);
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
@@ -186,24 +171,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_message_splitting(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
LanguageModelRegistry::test(cx);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let context = init_test(cx);
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
@@ -291,23 +259,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::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 prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let context = init_test(cx);
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
@@ -386,13 +338,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(Project::init_settings);
|
||||
cx.update(assistant_panel::init);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
@@ -406,24 +352,11 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let context = cx.update(|cx| init_test_with_fs(fs, cx));
|
||||
|
||||
let slash_command_registry = cx.update(SlashCommandRegistry::default_global);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, false);
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
#[derive(Default)]
|
||||
struct ContextRanges {
|
||||
parsed_commands: HashSet<Range<language::Anchor>>,
|
||||
@@ -680,7 +613,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
cx.update(prompt_library::init);
|
||||
let mut settings_store = cx.update(SettingsStore::test);
|
||||
cx.update(|cx| {
|
||||
@@ -706,7 +639,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
Some(project),
|
||||
project.clone(),
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
@@ -715,6 +648,17 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
)
|
||||
});
|
||||
|
||||
let events = Rc::new(RefCell::new(Vec::new()));
|
||||
context.update(cx, |_, cx| {
|
||||
let events = events.clone();
|
||||
cx.subscribe(&context, move |_, _, event, _cx| match event {
|
||||
ContextEvent::PatchUpdated(id) => events.borrow_mut().push(("updated", *id)),
|
||||
ContextEvent::PatchRemoved(id) => events.borrow_mut().push(("removed", *id)),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
// Insert an assistant message to simulate a response.
|
||||
let assistant_message_id = context.update(cx, |context, cx| {
|
||||
let user_message_id = context.messages(cx).next().unwrap().id;
|
||||
@@ -744,8 +688,9 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(mem::take(&mut *events.borrow_mut()), &[]);
|
||||
|
||||
// Partial edit step tag is added
|
||||
// Partial patch tag is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
@@ -781,7 +726,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
<edit>»",
|
||||
cx,
|
||||
);
|
||||
expect_patches(
|
||||
let patch_ids = expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
@@ -793,8 +738,12 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
&[&[]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// The full patch is added
|
||||
// Add one edit to the patch
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
@@ -811,7 +760,59 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</edit>»",
|
||||
cx,
|
||||
);
|
||||
let patch_ids = expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>»",
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn one".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: Some("add a `two` function".into()),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// The full patch is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>«
|
||||
</patch>
|
||||
|
||||
also,»",
|
||||
@@ -847,6 +848,10 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// The step is manually edited.
|
||||
edit(
|
||||
@@ -901,6 +906,10 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// When setting the message role to User, the steps are cleared.
|
||||
context.update(cx, |context, cx| {
|
||||
@@ -930,6 +939,10 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("removed", patch_ids[0])]
|
||||
);
|
||||
|
||||
// When setting the message role back to Assistant, the steps are reparsed.
|
||||
context.update(cx, |context, cx| {
|
||||
@@ -976,7 +989,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
project,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -1027,26 +1040,28 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
expected_marked_text: &str,
|
||||
expected_suggestions: &[&[AssistantEdit]],
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
) -> Vec<PatchId> {
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
|
||||
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
|
||||
let (buffer_text, ranges, patch_ids, patches) = context.update(cx, |context, cx| {
|
||||
let patch_store = context.patch_store.read(cx);
|
||||
context.buffer.read_with(cx, |buffer, _| {
|
||||
let ranges = context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|entry| entry.range.to_offset(buffer))
|
||||
.map(|entry| entry.0.to_offset(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
(
|
||||
buffer.text(),
|
||||
ranges,
|
||||
context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|step| step.edits.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
let patch_ids = context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|(_, id)| *id)
|
||||
.collect::<Vec<_>>();
|
||||
let patches = patch_ids
|
||||
.iter()
|
||||
.map(|id| patch_store.get(*id).unwrap().clone())
|
||||
.collect::<Vec<_>>();
|
||||
(buffer.text(), ranges, patch_ids, patches)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1060,41 +1075,24 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
.iter()
|
||||
.map(|patch| {
|
||||
patch
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| {
|
||||
let edit = edit.as_ref().unwrap();
|
||||
AssistantEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
}
|
||||
.map(|edit| AssistantEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
expected_suggestions
|
||||
);
|
||||
patch_ids
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_serialization(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(assistant_panel::init);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let context = cx.update(init_test);
|
||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
||||
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
|
||||
let message_1 = context.update(cx, |context, cx| {
|
||||
@@ -1132,11 +1130,11 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
Context::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
context.read(cx).language_registry.clone(),
|
||||
context.read(cx).prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
context.read(cx).project.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -1172,8 +1170,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
|
||||
cx.update(assistant_panel::init);
|
||||
cx.update(Project::init_settings);
|
||||
|
||||
let slash_commands = cx.update(SlashCommandRegistry::default_global);
|
||||
slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
|
||||
slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
|
||||
@@ -1187,6 +1186,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let context_id = ContextId::new();
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
for i in 0..num_peers {
|
||||
let project =
|
||||
cx.update(|cx| Project::empty(FakeFs::new(cx.background_executor().clone()), cx));
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
@@ -1196,7 +1197,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
project,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -1443,23 +1444,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|
||||
#[gpui::test]
|
||||
fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::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 prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let context = init_test(cx);
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
// Create a test cache configuration
|
||||
@@ -1603,6 +1588,32 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext) -> Model<Context> {
|
||||
init_test_with_fs(FakeFs::new(cx.background_executor().clone()), cx)
|
||||
}
|
||||
|
||||
fn init_test_with_fs(fs: Arc<FakeFs>, cx: &mut AppContext) -> Model<Context> {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let project = Project::empty(fs, cx);
|
||||
cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
project,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
|
||||
context
|
||||
.read(cx)
|
||||
|
||||
@@ -363,7 +363,7 @@ impl ContextStore {
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
self.languages.clone(),
|
||||
Some(self.project.clone()),
|
||||
self.project.clone(),
|
||||
Some(self.telemetry.clone()),
|
||||
self.prompt_builder.clone(),
|
||||
self.slash_commands.clone(),
|
||||
@@ -406,7 +406,7 @@ impl ContextStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
Some(project),
|
||||
project,
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
@@ -468,7 +468,7 @@ impl ContextStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
Some(project),
|
||||
project,
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
@@ -548,7 +548,7 @@ impl ContextStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
Some(project),
|
||||
project,
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
608
crates/assistant/src/patch/patch_tests.rs
Normal file
608
crates/assistant/src/patch/patch_tests.rs
Normal file
@@ -0,0 +1,608 @@
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext, Context, TestAppContext};
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use ui::BorrowAppContext;
|
||||
use unindent::Unindent as _;
|
||||
use util::test::{generate_marked_text, marked_text_ranges};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_patch_store(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(language::init);
|
||||
cx.update(Project::init_settings);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"src": {
|
||||
"lib.rs": "
|
||||
fn one() -> usize {
|
||||
1
|
||||
}
|
||||
fn two() -> usize {
|
||||
2
|
||||
}
|
||||
fn three() -> usize {
|
||||
3
|
||||
}
|
||||
".unindent(),
|
||||
"main.rs": "
|
||||
use crate::one;
|
||||
fn main() { one(); }
|
||||
".unindent(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||
project.update(cx, |project, _| {
|
||||
project.languages().add(Arc::new(rust_lang()));
|
||||
});
|
||||
let patch_store = cx.new_model(|_| PatchStore::new(project.clone()));
|
||||
let context_buffer = cx.new_model(|cx| Buffer::local("hello", cx));
|
||||
let context_buffer = context_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let range = context_buffer.anchor_before(0)..context_buffer.anchor_before(1);
|
||||
|
||||
let patch_id = patch_store.update(cx, |store, cx| {
|
||||
store.insert(
|
||||
AssistantPatch {
|
||||
range: range.clone(),
|
||||
title: "first patch".into(),
|
||||
edits: vec![AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::Update {
|
||||
old_text: "1".into(),
|
||||
new_text: "100".into(),
|
||||
description: None,
|
||||
},
|
||||
}]
|
||||
.into(),
|
||||
status: AssistantPatchStatus::Pending,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let patch = patch_store
|
||||
.update(cx, |store, cx| store.resolve_patch(patch_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_apply_patch(
|
||||
&patch,
|
||||
cx,
|
||||
&[(
|
||||
Path::new("src/lib.rs").into(),
|
||||
"
|
||||
fn one() -> usize {
|
||||
100
|
||||
}
|
||||
fn two() -> usize {
|
||||
2
|
||||
}
|
||||
fn three() -> usize {
|
||||
3
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
)],
|
||||
);
|
||||
|
||||
patch_store.update(cx, |store, cx| {
|
||||
store
|
||||
.update(
|
||||
patch_id,
|
||||
AssistantPatch {
|
||||
range: range.clone(),
|
||||
title: "first patch".into(),
|
||||
edits: vec![
|
||||
AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::Update {
|
||||
old_text: "1".into(),
|
||||
new_text: "100".into(),
|
||||
description: None,
|
||||
},
|
||||
},
|
||||
AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::Update {
|
||||
old_text: "3".into(),
|
||||
new_text: "300".into(),
|
||||
description: None,
|
||||
},
|
||||
},
|
||||
]
|
||||
.into(),
|
||||
status: AssistantPatchStatus::Pending,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let patch = patch_store
|
||||
.update(cx, |store, cx| store.resolve_patch(patch_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_apply_patch(
|
||||
&patch,
|
||||
cx,
|
||||
&[(
|
||||
Path::new("src/lib.rs").into(),
|
||||
"
|
||||
fn one() -> usize {
|
||||
100
|
||||
}
|
||||
fn two() -> usize {
|
||||
2
|
||||
}
|
||||
fn three() -> usize {
|
||||
300
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_location(cx: &mut AppContext) {
|
||||
assert_location_resolution(
|
||||
concat!(
|
||||
" Lorem\n",
|
||||
"« ipsum\n",
|
||||
" dolor sit amet»\n",
|
||||
" consecteur",
|
||||
),
|
||||
"ipsum\ndolor",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
«fn foo1(a: usize) -> usize {
|
||||
40
|
||||
}»
|
||||
|
||||
fn foo2(b: usize) -> usize {
|
||||
42
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
"fn foo1(b: usize) {\n40\n}",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
fn main() {
|
||||
« Foo
|
||||
.bar()
|
||||
.baz()
|
||||
.qux()»
|
||||
}
|
||||
|
||||
fn foo2(b: usize) -> usize {
|
||||
42
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
"Foo.bar.baz.qux()",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
class Something {
|
||||
one() { return 1; }
|
||||
« two() { return 2222; }
|
||||
three() { return 333; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }
|
||||
» seven() { return 7; }
|
||||
eight() { return 8; }
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
&"
|
||||
two() { return 2222; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_resolve_edits(cx: &mut TestAppContext) {
|
||||
cx.update(init_test);
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
name: String,
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Ensure InsertBefore merges correctly with Update of the same text
|
||||
assert_edits(
|
||||
"
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::InsertBefore {
|
||||
old_text: "
|
||||
fn foo() {"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn bar() {
|
||||
qux();
|
||||
}"
|
||||
.unindent(),
|
||||
description: Some("implement bar".into()),
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn foo() {
|
||||
bar();
|
||||
}"
|
||||
.unindent(),
|
||||
description: Some("call bar in foo".into()),
|
||||
},
|
||||
AssistantEditKind::InsertAfter {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: Some("implement qux".into()),
|
||||
},
|
||||
],
|
||||
"
|
||||
fn bar() {
|
||||
qux();
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar();
|
||||
}
|
||||
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Correctly indent new text when replacing multiple adjacent indented blocks.
|
||||
assert_edits(
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
impl Person {
|
||||
fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
return self.name;
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "self.name = name;".unindent(),
|
||||
new_text: "self._name = name;".unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "return self.name;\n".unindent(),
|
||||
new_text: "return self._name;\n".unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
impl Person {
|
||||
fn set_name(&mut self, name: String) {
|
||||
self._name = name;
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
return self._name;
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_apply_patch(
|
||||
patch: &ResolvedPatch,
|
||||
cx: &mut TestAppContext,
|
||||
expected_output: &[(Arc<Path>, String)],
|
||||
) {
|
||||
let mut actual_output = Vec::new();
|
||||
for (buffer, edit_groups) in &patch.edit_groups {
|
||||
let branch = buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
cx.update(|cx| {
|
||||
ResolvedPatch::apply_buffer_edits(&Vec::new(), edit_groups, &branch, cx);
|
||||
actual_output.push((
|
||||
buffer.read(cx).file().unwrap().path().clone(),
|
||||
branch.read(cx).text(),
|
||||
));
|
||||
});
|
||||
}
|
||||
pretty_assertions::assert_eq!(actual_output, expected_output);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut AppContext) {
|
||||
let (text, _) = marked_text_ranges(text_with_expected_range, false);
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let range = AssistantEditKind::resolve_location(snapshot.as_rope(), query).to_offset(&snapshot);
|
||||
let text_with_actual_range = generate_marked_text(&text, &[range], false);
|
||||
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
|
||||
}
|
||||
|
||||
async fn assert_edits(
|
||||
old_text: String,
|
||||
edits: Vec<AssistantEditKind>,
|
||||
new_text: String,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({"file.rs": old_text})).await;
|
||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||
project.update(cx, |project, _| {
|
||||
project.languages().add(Arc::new(rust_lang()));
|
||||
});
|
||||
let patch_store = cx.new_model(|_| PatchStore::new(project));
|
||||
let patch_range = language::Anchor::MIN..language::Anchor::MAX;
|
||||
let patch_id = patch_store.update(cx, |patch_store, cx| {
|
||||
patch_store.insert(
|
||||
AssistantPatch {
|
||||
range: patch_range.clone(),
|
||||
title: "test-patch".into(),
|
||||
edits: edits
|
||||
.into_iter()
|
||||
.map(|kind| AssistantEdit {
|
||||
path: "file.rs".into(),
|
||||
kind,
|
||||
})
|
||||
.collect(),
|
||||
status: AssistantPatchStatus::Ready,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let patch = patch_store
|
||||
.update(cx, |patch_store, cx| {
|
||||
patch_store.resolve_patch(patch_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (buffer, edit_groups) = patch.edit_groups.into_iter().next().unwrap();
|
||||
cx.update(|cx| ResolvedPatch::apply_buffer_edits(&Vec::new(), &edit_groups, &buffer, cx));
|
||||
let actual_new_text = buffer.read_with(cx, |buffer, _| buffer.text());
|
||||
pretty_assertions::assert_eq!(actual_new_text, new_text);
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(language::tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -1067,8 +1067,11 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.block_for_id(id)
|
||||
}
|
||||
|
||||
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
|
||||
self.fold_snapshot.intersects_fold(offset)
|
||||
pub fn intersects_fold<T: ToPoint>(&self, point: T) -> bool {
|
||||
let point = point.to_point(&self.buffer_snapshot);
|
||||
self.block_snapshot
|
||||
.is_line_replaced(MultiBufferRow(point.row))
|
||||
|| self.fold_snapshot.intersects_fold(point)
|
||||
}
|
||||
|
||||
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
|
||||
@@ -110,6 +110,10 @@ impl ProposedChangesEditor {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn multibuffer(&self) -> Model<MultiBuffer> {
|
||||
self.multibuffer.clone()
|
||||
}
|
||||
|
||||
pub fn branch_buffer_for_base(&self, base_buffer: &Model<Buffer>) -> Option<Model<Buffer>> {
|
||||
self.buffer_entries.iter().find_map(|entry| {
|
||||
if &entry.base == base_buffer {
|
||||
@@ -153,26 +157,9 @@ impl ProposedChangesEditor {
|
||||
multibuffer.clear(cx);
|
||||
});
|
||||
|
||||
let mut buffer_entries = Vec::new();
|
||||
self.buffer_entries = Vec::new();
|
||||
for location in locations {
|
||||
let branch_buffer;
|
||||
if let Some(ix) = self
|
||||
.buffer_entries
|
||||
.iter()
|
||||
.position(|entry| entry.base == location.buffer)
|
||||
{
|
||||
let entry = self.buffer_entries.remove(ix);
|
||||
branch_buffer = entry.branch.clone();
|
||||
buffer_entries.push(entry);
|
||||
} else {
|
||||
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
buffer_entries.push(BufferEntry {
|
||||
branch: branch_buffer.clone(),
|
||||
base: location.buffer.clone(),
|
||||
_subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
|
||||
});
|
||||
}
|
||||
|
||||
let branch_buffer = self.add_buffer(location.buffer, cx);
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
branch_buffer,
|
||||
@@ -185,12 +172,36 @@ impl ProposedChangesEditor {
|
||||
});
|
||||
}
|
||||
|
||||
self.buffer_entries = buffer_entries;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| selections.refresh())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_buffer(
|
||||
&mut self,
|
||||
base_buffer: Model<Buffer>,
|
||||
cx: &mut ViewContext<ProposedChangesEditor>,
|
||||
) -> Model<Buffer> {
|
||||
let branch_buffer;
|
||||
if let Some(ix) = self
|
||||
.buffer_entries
|
||||
.iter()
|
||||
.position(|entry| entry.base == base_buffer)
|
||||
{
|
||||
let entry = self.buffer_entries.remove(ix);
|
||||
branch_buffer = entry.branch.clone();
|
||||
self.buffer_entries.push(entry);
|
||||
} else {
|
||||
branch_buffer = base_buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
self.buffer_entries.push(BufferEntry {
|
||||
branch: branch_buffer.clone(),
|
||||
base: base_buffer.clone(),
|
||||
_subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
|
||||
});
|
||||
}
|
||||
branch_buffer
|
||||
}
|
||||
|
||||
pub fn recalculate_all_buffer_diffs(&self) {
|
||||
for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
|
||||
self.recalculate_diffs_tx
|
||||
|
||||
@@ -575,7 +575,7 @@ impl<'a, 'b> DerefMut for ChunkRendererContext<'a, 'b> {
|
||||
pub struct Diff {
|
||||
pub(crate) base_version: clock::Global,
|
||||
line_ending: LineEnding,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
pub edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -1597,69 +1597,88 @@ impl Buffer {
|
||||
let old_text = old_text.to_string();
|
||||
let line_ending = LineEnding::detect(&new_text);
|
||||
LineEnding::normalize(&mut new_text);
|
||||
Self::diff_internal(old_text, new_text, base_version, line_ending)
|
||||
})
|
||||
}
|
||||
|
||||
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
|
||||
let empty: Arc<str> = Arc::default();
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut old_offset = 0;
|
||||
let mut new_offset = 0;
|
||||
let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
|
||||
for change in diff.iter_all_changes().map(Some).chain([None]) {
|
||||
if let Some(change) = &change {
|
||||
let len = change.value().len();
|
||||
match change.tag() {
|
||||
ChangeTag::Equal => {
|
||||
old_offset += len;
|
||||
new_offset += len;
|
||||
}
|
||||
ChangeTag::Delete => {
|
||||
let old_end_offset = old_offset + len;
|
||||
if let Some((last_old_range, _)) = &mut last_edit {
|
||||
last_old_range.end = old_end_offset;
|
||||
} else {
|
||||
last_edit =
|
||||
Some((old_offset..old_end_offset, new_offset..new_offset));
|
||||
}
|
||||
old_offset = old_end_offset;
|
||||
}
|
||||
ChangeTag::Insert => {
|
||||
let new_end_offset = new_offset + len;
|
||||
if let Some((_, last_new_range)) = &mut last_edit {
|
||||
last_new_range.end = new_end_offset;
|
||||
} else {
|
||||
last_edit =
|
||||
Some((old_offset..old_offset, new_offset..new_end_offset));
|
||||
}
|
||||
new_offset = new_end_offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((old_range, new_range)) = &last_edit {
|
||||
if old_offset > old_range.end
|
||||
|| new_offset > new_range.end
|
||||
|| change.is_none()
|
||||
{
|
||||
let text = if new_range.is_empty() {
|
||||
empty.clone()
|
||||
} else {
|
||||
new_text[new_range.clone()].into()
|
||||
};
|
||||
edits.push((old_range.clone(), text));
|
||||
last_edit.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Diff {
|
||||
pub fn diff_rope(&self, new_text: &Rope, cx: &AppContext) -> Task<Diff> {
|
||||
let old_text = self.as_rope().clone();
|
||||
let new_text = new_text.clone();
|
||||
let base_version = self.version();
|
||||
let line_ending = self.line_ending();
|
||||
cx.background_executor()
|
||||
.spawn_labeled(*BUFFER_DIFF_TASK, async move {
|
||||
Self::diff_internal(
|
||||
old_text.to_string(),
|
||||
new_text.to_string(),
|
||||
base_version,
|
||||
line_ending,
|
||||
edits,
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn diff_internal(
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
base_version: clock::Global,
|
||||
line_ending: LineEnding,
|
||||
) -> Diff {
|
||||
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
|
||||
let empty: Arc<str> = Arc::default();
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut old_offset = 0;
|
||||
let mut new_offset = 0;
|
||||
let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
|
||||
for change in diff.iter_all_changes().map(Some).chain([None]) {
|
||||
if let Some(change) = &change {
|
||||
let len = change.value().len();
|
||||
match change.tag() {
|
||||
ChangeTag::Equal => {
|
||||
old_offset += len;
|
||||
new_offset += len;
|
||||
}
|
||||
ChangeTag::Delete => {
|
||||
let old_end_offset = old_offset + len;
|
||||
if let Some((last_old_range, _)) = &mut last_edit {
|
||||
last_old_range.end = old_end_offset;
|
||||
} else {
|
||||
last_edit = Some((old_offset..old_end_offset, new_offset..new_offset));
|
||||
}
|
||||
old_offset = old_end_offset;
|
||||
}
|
||||
ChangeTag::Insert => {
|
||||
let new_end_offset = new_offset + len;
|
||||
if let Some((_, last_new_range)) = &mut last_edit {
|
||||
last_new_range.end = new_end_offset;
|
||||
} else {
|
||||
last_edit = Some((old_offset..old_offset, new_offset..new_end_offset));
|
||||
}
|
||||
new_offset = new_end_offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((old_range, new_range)) = &last_edit {
|
||||
if old_offset > old_range.end || new_offset > new_range.end || change.is_none() {
|
||||
let text = if new_range.is_empty() {
|
||||
empty.clone()
|
||||
} else {
|
||||
new_text[new_range.clone()].into()
|
||||
};
|
||||
edits.push((old_range.clone(), text));
|
||||
last_edit.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Diff {
|
||||
base_version,
|
||||
line_ending,
|
||||
edits,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a background task that searches the buffer for any whitespace
|
||||
/// at the ends of a lines, and returns a `Diff` that removes that whitespace.
|
||||
pub fn remove_trailing_whitespace(&self, cx: &AppContext) -> Task<Diff> {
|
||||
|
||||
@@ -1169,31 +1169,35 @@ impl Project {
|
||||
project
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn empty(fs: Arc<dyn Fs>, cx: &mut gpui::AppContext) -> Model<Project> {
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::Context;
|
||||
|
||||
let languages = LanguageRegistry::test(cx.background_executor().clone());
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let client = client::Client::new(clock, http_client.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
Project::local(
|
||||
client,
|
||||
node_runtime::NodeRuntime::unavailable(),
|
||||
user_store,
|
||||
Arc::new(languages),
|
||||
fs,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn test(
|
||||
fs: Arc<dyn Fs>,
|
||||
root_paths: impl IntoIterator<Item = &Path>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Model<Project> {
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::Context;
|
||||
let project = cx.update(|cx| Self::empty(fs, cx));
|
||||
|
||||
let languages = LanguageRegistry::test(cx.executor());
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
client,
|
||||
node_runtime::NodeRuntime::unavailable(),
|
||||
user_store,
|
||||
Arc::new(languages),
|
||||
fs,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
for path in root_paths {
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
|
||||
Reference in New Issue
Block a user