Allow the assistant to suggest edits to files in the project (#11993)

### Todo

* [x] tuck the new system prompt away somehow
* for now, we're treating it as built-in, and not editable. once we have
a way to fold away default prompts, let's make it a default prompt.
* [x] when applying edits, re-parse the edit from the latest content of
the assistant buffer (to allow for manual editing of edits)
* [x] automatically adjust the indentation of edits suggested by the
assistant
* [x] fix edit row highlights persisting even when assistant messages
with edits are deleted
* ~adjust the fuzzy search to allow for small errors in the old text,
using some string similarity routine~

We decided to defer the fuzzy searching thing to a separate PR, since
it's a little bit involved, and the current functionality works well
enough to be worth landing. A couple of notes on the fuzzy searching:
* sometimes the assistant accidentally omits line breaks from the text
that it wants to replace
* when the old text has hallucinations, the new text often contains the
same hallucinations. so we'll probably need to use a more fine-grained
editing strategy where we perform a character-wise diff of the old and
new text as reported by the assistant, and then adjust that diff so that
it can be applied to the actual buffer text

Release Notes:

- Added the ability to request edits to project files using the
assistant panel.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-05-17 15:38:14 -07:00 committed by GitHub
parent 4386268a94
commit 84affa96ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 912 additions and 181 deletions

View file

@ -617,6 +617,14 @@ impl<'a> Chunks<'a> {
let end = self.range.end - chunk_start;
Some(&chunk.0[start..chunk.0.len().min(end)])
}
pub fn lines(self) -> Lines<'a> {
Lines {
chunks: self,
current_line: String::new(),
done: false,
}
}
}
impl<'a> Iterator for Chunks<'a> {
@ -714,6 +722,49 @@ impl<'a> io::Read for Bytes<'a> {
}
}
pub struct Lines<'a> {
chunks: Chunks<'a>,
current_line: String,
done: bool,
}
impl<'a> Lines<'a> {
pub fn next(&mut self) -> Option<&str> {
if self.done {
return None;
}
self.current_line.clear();
while let Some(chunk) = self.chunks.peek() {
let mut lines = chunk.split('\n').peekable();
while let Some(line) = lines.next() {
self.current_line.push_str(line);
if lines.peek().is_some() {
self.chunks
.seek(self.chunks.offset() + line.len() + "\n".len());
return Some(&self.current_line);
}
}
self.chunks.next();
}
self.done = true;
Some(&self.current_line)
}
pub fn seek(&mut self, offset: usize) {
self.chunks.seek(offset);
self.current_line.clear();
self.done = false;
}
pub fn offset(&self) -> usize {
self.chunks.offset()
}
}
#[derive(Clone, Debug, Default)]
struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>);
@ -1288,6 +1339,24 @@ mod tests {
);
}
#[test]
fn test_lines() {
let rope = Rope::from("abc\ndefg\nhi");
let mut lines = rope.chunks().lines();
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), Some("defg"));
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\ndefg\nhi\n");
let mut lines = rope.chunks().lines();
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), Some("defg"));
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), None);
}
#[gpui::test(iterations = 100)]
fn test_random_rope(mut rng: StdRng) {
let operations = env::var("OPERATIONS")