This commit is contained in:
Antonio Scandurra 2023-08-22 08:16:22 +02:00
parent 5b9d48d723
commit 5453553cfa
3 changed files with 234 additions and 109 deletions

1
Cargo.lock generated
View file

@ -106,6 +106,7 @@ dependencies = [
"fs", "fs",
"futures 0.3.28", "futures 0.3.28",
"gpui", "gpui",
"indoc",
"isahc", "isahc",
"language", "language",
"menu", "menu",

View file

@ -24,6 +24,7 @@ workspace = { path = "../workspace" }
anyhow.workspace = true anyhow.workspace = true
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
futures.workspace = true futures.workspace = true
indoc.workspace = true
isahc.workspace = true isahc.workspace = true
regex.workspace = true regex.workspace = true
schemars.workspace = true schemars.workspace = true

View file

@ -1,14 +1,13 @@
use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; use crate::{stream_completion, OpenAIRequest, RequestMessage, Role};
use collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use collections::HashMap;
use editor::{Anchor, Editor, MultiBuffer, MultiBufferSnapshot, ToOffset}; use editor::{Editor, ToOffset};
use futures::{io::BufWriter, AsyncReadExt, AsyncWriteExt, StreamExt}; use futures::StreamExt;
use gpui::{ use gpui::{
actions, elements::*, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle, actions, elements::*, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle,
WeakViewHandle, WeakViewHandle,
}; };
use menu::Confirm; use menu::Confirm;
use serde::Deserialize; use similar::{Change, ChangeTag, TextDiff};
use similar::ChangeTag;
use std::{env, iter, ops::Range, sync::Arc}; use std::{env, iter, ops::Range, sync::Arc};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{Modal, Workspace}; use workspace::{Modal, Workspace};
@ -33,12 +32,12 @@ impl RefactoringAssistant {
} }
fn refactor(&mut self, editor: &ViewHandle<Editor>, prompt: &str, cx: &mut AppContext) { fn refactor(&mut self, editor: &ViewHandle<Editor>, prompt: &str, cx: &mut AppContext) {
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let selection = editor.read(cx).selections.newest_anchor().clone(); let selection = editor.read(cx).selections.newest_anchor().clone();
let selected_text = buffer let selected_text = snapshot
.text_for_range(selection.start..selection.end) .text_for_range(selection.start..selection.end)
.collect::<String>(); .collect::<String>();
let language_name = buffer let language_name = snapshot
.language_at(selection.start) .language_at(selection.start)
.map(|language| language.name()); .map(|language| language.name());
let language_name = language_name.as_deref().unwrap_or(""); let language_name = language_name.as_deref().unwrap_or("");
@ -48,7 +47,7 @@ impl RefactoringAssistant {
RequestMessage { RequestMessage {
role: Role::User, role: Role::User,
content: format!( content: format!(
"Given the following {language_name} snippet:\n{selected_text}\n{prompt}. Avoid making remarks and reply only with the new code." "Given the following {language_name} snippet:\n{selected_text}\n{prompt}. Avoid making remarks and reply only with the new code. Preserve indentation."
), ),
}], }],
stream: true, stream: true,
@ -60,86 +59,149 @@ impl RefactoringAssistant {
editor.id(), editor.id(),
cx.spawn(|mut cx| { cx.spawn(|mut cx| {
async move { async move {
let selection_start = selection.start.to_offset(&buffer); let selection_start = selection.start.to_offset(&snapshot);
// Find unique words in the selected text to use as diff boundaries.
let mut duplicate_words = HashSet::default();
let mut unique_old_words = HashMap::default();
for (range, word) in words(&selected_text) {
if !duplicate_words.contains(word) {
if unique_old_words.insert(word, range.end).is_some() {
unique_old_words.remove(word);
duplicate_words.insert(word);
}
}
}
let mut new_text = String::new(); let mut new_text = String::new();
let mut messages = response.await?; let mut messages = response.await?;
let mut new_word_search_start_ix = 0;
let mut last_old_word_end_ix = 0;
'outer: loop { let mut transaction = None;
const MIN_DIFF_LEN: usize = 50;
let start = new_word_search_start_ix; while let Some(message) = messages.next().await {
let mut words = words(&new_text[start..]); smol::future::yield_now().await;
while let Some((range, new_word)) = words.next() { let mut message = message?;
// We found a word in the new text that was unique in the old text. We can use if let Some(choice) = message.choices.pop() {
// it as a diff boundary, and start applying edits. if let Some(text) = choice.delta.content {
if let Some(old_word_end_ix) = unique_old_words.get(new_word).copied() { new_text.push_str(&text);
if old_word_end_ix.saturating_sub(last_old_word_end_ix)
> MIN_DIFF_LEN println!("-------------------------------------");
println!(
"{}",
similar::TextDiff::from_words(&selected_text, &new_text)
.unified_diff()
);
let mut changes =
similar::TextDiff::from_words(&selected_text, &new_text)
.iter_all_changes()
.collect::<Vec<_>>();
let mut ix = 0;
while ix < changes.len() {
let deletion_start_ix = ix;
let mut deletion_end_ix = ix;
while changes
.get(ix)
.map_or(false, |change| change.tag() == ChangeTag::Delete)
{
ix += 1;
deletion_end_ix += 1;
}
let insertion_start_ix = ix;
let mut insertion_end_ix = ix;
while changes
.get(ix)
.map_or(false, |change| change.tag() == ChangeTag::Insert)
{
ix += 1;
insertion_end_ix += 1;
}
if deletion_end_ix > deletion_start_ix
&& insertion_end_ix > insertion_start_ix
{
for _ in deletion_start_ix..deletion_end_ix {
let deletion = changes.remove(deletion_end_ix);
changes.insert(insertion_end_ix - 1, deletion);
}
}
ix += 1;
}
while changes
.last()
.map_or(false, |change| change.tag() != ChangeTag::Insert)
{ {
drop(words); changes.pop();
let remainder = new_text.split_off(start + range.end);
let edits = diff(
selection_start + last_old_word_end_ix,
&selected_text[last_old_word_end_ix..old_word_end_ix],
&new_text,
&buffer,
);
editor.update(&mut cx, |editor, cx| {
editor
.buffer()
.update(cx, |buffer, cx| buffer.edit(edits, None, cx))
})?;
new_text = remainder;
new_word_search_start_ix = 0;
last_old_word_end_ix = old_word_end_ix;
continue 'outer;
} }
}
new_word_search_start_ix = start + range.end; editor.update(&mut cx, |editor, cx| {
} editor.buffer().update(cx, |buffer, cx| {
drop(words); if let Some(transaction) = transaction.take() {
buffer.undo(cx); // TODO: Undo the transaction instead
}
// Buffer incoming text, stopping if the stream was exhausted. buffer.start_transaction(cx);
if let Some(message) = messages.next().await { let mut edit_start = selection_start;
let mut message = message?; dbg!(&changes);
if let Some(choice) = message.choices.pop() { for change in changes {
if let Some(text) = choice.delta.content { let value = change.value();
new_text.push_str(&text); let edit_end = edit_start + value.len();
} match change.tag() {
ChangeTag::Equal => {
edit_start = edit_end;
}
ChangeTag::Delete => {
let range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
buffer.edit([(range, "")], None, cx);
edit_start = edit_end;
}
ChangeTag::Insert => {
let insertion_start =
snapshot.anchor_after(edit_start);
buffer.edit(
[(insertion_start..insertion_start, value)],
None,
cx,
);
}
}
}
transaction = buffer.end_transaction(cx);
})
})?;
} }
} else {
break;
} }
} }
let edits = diff(
selection_start + last_old_word_end_ix,
&selected_text[last_old_word_end_ix..],
&new_text,
&buffer,
);
editor.update(&mut cx, |editor, cx| { editor.update(&mut cx, |editor, cx| {
editor editor.buffer().update(cx, |buffer, cx| {
.buffer() if let Some(transaction) = transaction.take() {
.update(cx, |buffer, cx| buffer.edit(edits, None, cx)) buffer.undo(cx); // TODO: Undo the transaction instead
}
buffer.start_transaction(cx);
let mut edit_start = selection_start;
for change in similar::TextDiff::from_words(&selected_text, &new_text)
.iter_all_changes()
{
let value = change.value();
let edit_end = edit_start + value.len();
match change.tag() {
ChangeTag::Equal => {
edit_start = edit_end;
}
ChangeTag::Delete => {
let range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
buffer.edit([(range, "")], None, cx);
edit_start = edit_end;
}
ChangeTag::Insert => {
let insertion_start = snapshot.anchor_after(edit_start);
buffer.edit(
[(insertion_start..insertion_start, value)],
None,
cx,
);
}
}
}
buffer.end_transaction(cx);
})
})?; })?;
anyhow::Ok(()) anyhow::Ok(())
@ -197,11 +259,13 @@ impl RefactoringModal {
{ {
workspace.toggle_modal(cx, |_, cx| { workspace.toggle_modal(cx, |_, cx| {
let prompt_editor = cx.add_view(|cx| { let prompt_editor = cx.add_view(|cx| {
Editor::auto_height( let mut editor = Editor::auto_height(
4, 4,
Some(Arc::new(|theme| theme.search.editor.input.clone())), Some(Arc::new(|theme| theme.search.editor.input.clone())),
cx, cx,
) );
editor.set_text("Replace with match statement.", cx);
editor
}); });
cx.add_view(|_| RefactoringModal { cx.add_view(|_| RefactoringModal {
editor, editor,
@ -242,38 +306,97 @@ fn words(text: &str) -> impl Iterator<Item = (Range<usize>, &str)> {
}) })
} }
fn diff<'a>( fn streaming_diff<'a>(old_text: &'a str, new_text: &'a str) -> Vec<Change<'a, str>> {
start_ix: usize, let changes = TextDiff::configure()
old_text: &'a str, .algorithm(similar::Algorithm::Patience)
new_text: &'a str, .diff_words(old_text, new_text);
old_buffer_snapshot: &MultiBufferSnapshot, let mut changes = changes.iter_all_changes().peekable();
) -> Vec<(Range<Anchor>, &'a str)> {
let mut edit_start = start_ix; let mut result = vec![];
let mut edits = Vec::new();
let diff = similar::TextDiff::from_words(old_text, &new_text); loop {
for change in diff.iter_all_changes() { let mut deletions = vec![];
let value = change.value(); let mut insertions = vec![];
let edit_end = edit_start + value.len();
match change.tag() { while changes
ChangeTag::Equal => { .peek()
edit_start = edit_end; .map_or(false, |change| change.tag() == ChangeTag::Delete)
} {
ChangeTag::Delete => { deletions.push(changes.next().unwrap());
edits.push(( }
old_buffer_snapshot.anchor_after(edit_start)
..old_buffer_snapshot.anchor_before(edit_end), while changes
"", .peek()
)); .map_or(false, |change| change.tag() == ChangeTag::Insert)
edit_start = edit_end; {
} insertions.push(changes.next().unwrap());
ChangeTag::Insert => { }
edits.push((
old_buffer_snapshot.anchor_after(edit_start) if !deletions.is_empty() && !insertions.is_empty() {
..old_buffer_snapshot.anchor_after(edit_start), result.append(&mut insertions);
value, result.append(&mut deletions);
)); } else {
} result.append(&mut deletions);
result.append(&mut insertions);
}
if let Some(change) = changes.next() {
result.push(change);
} else {
break;
} }
} }
edits
// Remove all non-inserts at the end.
while result
.last()
.map_or(false, |change| change.tag() != ChangeTag::Insert)
{
result.pop();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn test_streaming_diff() {
let old_text = indoc! {"
match (self.format, src_format) {
(Format::A8, Format::A8)
| (Format::Rgb24, Format::Rgb24)
| (Format::Rgba32, Format::Rgba32) => {
return self
.blit_from_with::<BlitMemcpy>(dst_rect, src_bytes, src_stride, src_format);
}
(Format::A8, Format::Rgb24) => {
return self
.blit_from_with::<BlitRgb24ToA8>(dst_rect, src_bytes, src_stride, src_format);
}
(Format::Rgb24, Format::A8) => {
return self
.blit_from_with::<BlitA8ToRgb24>(dst_rect, src_bytes, src_stride, src_format);
}
(Format::Rgb24, Format::Rgba32) => {
return self.blit_from_with::<BlitRgba32ToRgb24>(
dst_rect, src_bytes, src_stride, src_format,
);
}
(Format::Rgba32, Format::Rgb24)
| (Format::Rgba32, Format::A8)
| (Format::A8, Format::Rgba32) => {
unimplemented!()
}
_ => {}
}
"};
let new_text = indoc! {"
if self.format == src_format
"};
dbg!(streaming_diff(old_text, new_text));
}
} }