WIP
This commit is contained in:
parent
5b9d48d723
commit
5453553cfa
3 changed files with 234 additions and 109 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -106,6 +106,7 @@ dependencies = [
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
"isahc",
|
"isahc",
|
||||||
"language",
|
"language",
|
||||||
"menu",
|
"menu",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue