Compare commits

...

18 Commits

Author SHA1 Message Date
Max Brunsfeld
104b5ef882 Fix resolving of multiple edits at a location 2024-11-20 16:05:12 -08:00
Max Brunsfeld
b6ab4c2d65 Reorganize patch files
* Move patch tests to a separate file.
* Organize types and methods.
2024-11-19 16:00:14 -08:00
Max Brunsfeld
a4b9d034bc Merge branch 'main' into store-resolved-patches-2 2024-11-19 14:31:04 -08:00
Max Brunsfeld
8eaaaf7d45 Avoid unnecessary patch update events 2024-11-18 17:31:03 -08:00
Max Brunsfeld
e2b0c1fc9e Fix dropping of update task when patch is updated rapidly 2024-11-18 17:10:55 -08:00
Max Brunsfeld
9710542ce2 Account for 'generating...' line in patch blocks' height
Co-authored-by: Marshall <marshall@zed.dev>
2024-11-18 15:18:52 -08:00
Max Brunsfeld
b5f7bf1cb5 Don't cancel in-progress patch location
Co-authored-by: Marshall <marshall@zed.dev>
2024-11-18 12:01:32 -08:00
Max Brunsfeld
9c591fc571 Only refold if patch range or height changes
Co-authored-by: Marshall <marshall@zed.dev>
2024-11-18 11:40:40 -08:00
Max Brunsfeld
0b702273e3 Fix patch events
* Reuse patch id when patch end tag is appended (so range changes)
* Handle changes to patches' ranges in assistant panel
* Fix `DisplaySnapshot::intersects_fold` to account for replace blocks
2024-11-18 11:33:23 -08:00
Max Brunsfeld
ecf4dbad60 Emit patch events from PatchStore
This way, we can emit an event once a patch has been asynchronously
located.
2024-11-17 18:31:25 -08:00
Max Brunsfeld
91e4026fd6 🎨 2024-11-17 17:54:29 -08:00
Max Brunsfeld
6207c8a3e2 Associate patch view state w/ patch ids in the assistant panel 2024-11-17 17:35:00 -08:00
Max Brunsfeld
bb15bd76e6 Add logic for updating live diffs to reflect resolved patches 2024-11-15 19:38:51 -08:00
Max Brunsfeld
440967dae9 Make Context's project non-optional
Co-authored-by: Richard <richard@zed.dev>
2024-11-15 19:22:18 -08:00
Antonio Scandurra
8dfc3a38e4 Apply diff to resolved edits 2024-11-15 14:48:06 +01:00
Max Brunsfeld
863981bcbd Make incremental patch location more robust 2024-11-14 17:14:42 -08:00
Max Brunsfeld
0f85facb0e Start work on PatchStore struct, to store located patches
Co-authored-by: Marshall <marshall@zed.dev>
2024-11-14 16:02:41 -08:00
Max Brunsfeld
6f3b7158d0 When failing to parse an XML edit, just log the error 2024-11-14 13:10:22 -08:00
10 changed files with 1726 additions and 1127 deletions

View File

@@ -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()))

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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

View 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()
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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> {

View File

@@ -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| {