mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-16 00:47:39 +03:00
Introduce code generation (#2901)
![CleanShot 2023-08-28 at 12 24 36@2x](https://github.com/zed-industries/zed/assets/482957/f97cb399-1ac2-4fa9-94a7-137d1eec711c) Release Notes: - Added a new "Inline Assist" feature that lets you transform a selection or generate new code at the cursor location by hitting `ctrl-enter`.
This commit is contained in:
commit
ea17d1638e
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -102,14 +102,20 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.9.3",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"isahc",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"ordered-float",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search",
|
||||
@ -5649,6 +5655,7 @@ dependencies = [
|
||||
name = "quick_action_bar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai",
|
||||
"editor",
|
||||
"gpui",
|
||||
"search",
|
||||
|
@ -530,7 +530,8 @@
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"cmd-f8": "editor::GoToHunk",
|
||||
"cmd-shift-f8": "editor::GoToPrevHunk"
|
||||
"cmd-shift-f8": "editor::GoToPrevHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -24,7 +24,9 @@ workspace = { path = "../workspace" }
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
ordered-float.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
@ -35,3 +37,8 @@ tiktoken-rs = "0.4"
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
|
@ -1,28 +1,33 @@
|
||||
pub mod assistant;
|
||||
mod assistant_settings;
|
||||
mod streaming_diff;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use assistant::AssistantPanel;
|
||||
use assistant_settings::OpenAIModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::AppContext;
|
||||
use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use gpui::{executor::Background, AppContext};
|
||||
use isahc::{http::StatusCode, Request, RequestExt};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
|
||||
|
||||
// Data types for chat completion requests
|
||||
#[derive(Debug, Serialize)]
|
||||
struct OpenAIRequest {
|
||||
pub struct OpenAIRequest {
|
||||
model: String,
|
||||
messages: Vec<RequestMessage>,
|
||||
stream: bool,
|
||||
@ -116,7 +121,7 @@ struct RequestMessage {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct ResponseMessage {
|
||||
pub struct ResponseMessage {
|
||||
role: Option<Role>,
|
||||
content: Option<String>,
|
||||
}
|
||||
@ -150,7 +155,7 @@ impl Display for Role {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIResponseStreamEvent {
|
||||
pub struct OpenAIResponseStreamEvent {
|
||||
pub id: Option<String>,
|
||||
pub object: String,
|
||||
pub created: u32,
|
||||
@ -160,14 +165,14 @@ struct OpenAIResponseStreamEvent {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Usage {
|
||||
pub struct Usage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ChatChoiceDelta {
|
||||
pub struct ChatChoiceDelta {
|
||||
pub index: u32,
|
||||
pub delta: ResponseMessage,
|
||||
pub finish_reason: Option<String>,
|
||||
@ -191,3 +196,97 @@ struct OpenAIChoice {
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant::init(cx);
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
mut request: OpenAIRequest,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
request.stream = true;
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||
|
||||
let json_data = serde_json::to_string(&request)?;
|
||||
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(json_data)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::OK {
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||
|
||||
fn parse_line(
|
||||
line: Result<String, io::Error>,
|
||||
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||
if let Some(data) = line?.strip_prefix("data: ") {
|
||||
let event = serde_json::from_str(&data)?;
|
||||
Ok(Some(event))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next().await {
|
||||
if let Some(event) = parse_line(line).transpose() {
|
||||
let done = event.as_ref().map_or(false, |event| {
|
||||
event
|
||||
.choices
|
||||
.last()
|
||||
.map_or(false, |choice| choice.finish_reason.is_some())
|
||||
});
|
||||
if tx.unbounded_send(event).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(rx)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAIResponse {
|
||||
error: OpenAIError,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAIError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
match serde_json::from_str::<OpenAIResponse>(&body) {
|
||||
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {}",
|
||||
response.error.message,
|
||||
)),
|
||||
|
||||
_ => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
293
crates/ai/src/streaming_diff.rs
Normal file
293
crates/ai/src/streaming_diff.rs
Normal file
@ -0,0 +1,293 @@
|
||||
use collections::HashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
struct Matrix {
|
||||
cells: Vec<f64>,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cells: Vec::new(),
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, rows: usize, cols: usize) {
|
||||
self.cells.resize(rows * cols, 0.);
|
||||
self.rows = rows;
|
||||
self.cols = cols;
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> f64 {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
self.cells[col * self.rows + row]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
|
||||
self.cells[col * self.rows + row] = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Matrix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
for i in 0..self.rows {
|
||||
for j in 0..self.cols {
|
||||
write!(f, "{:5}", self.get(i, j))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Hunk {
|
||||
Insert { text: String },
|
||||
Remove { len: usize },
|
||||
Keep { len: usize },
|
||||
}
|
||||
|
||||
pub struct StreamingDiff {
|
||||
old: Vec<char>,
|
||||
new: Vec<char>,
|
||||
scores: Matrix,
|
||||
old_text_ix: usize,
|
||||
new_text_ix: usize,
|
||||
equal_runs: HashMap<(usize, usize), u32>,
|
||||
}
|
||||
|
||||
impl StreamingDiff {
|
||||
const INSERTION_SCORE: f64 = -1.;
|
||||
const DELETION_SCORE: f64 = -20.;
|
||||
const EQUALITY_BASE: f64 = 1.8;
|
||||
const MAX_EQUALITY_EXPONENT: i32 = 16;
|
||||
|
||||
pub fn new(old: String) -> Self {
|
||||
let old = old.chars().collect::<Vec<_>>();
|
||||
let mut scores = Matrix::new();
|
||||
scores.resize(old.len() + 1, 1);
|
||||
for i in 0..=old.len() {
|
||||
scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
|
||||
}
|
||||
Self {
|
||||
old,
|
||||
new: Vec::new(),
|
||||
scores,
|
||||
old_text_ix: 0,
|
||||
new_text_ix: 0,
|
||||
equal_runs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||
self.new.extend(text.chars());
|
||||
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||
|
||||
for j in self.new_text_ix + 1..=self.new.len() {
|
||||
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
|
||||
for i in 1..=self.old.len() {
|
||||
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
|
||||
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
|
||||
let equality_score = if self.old[i - 1] == self.new[j - 1] {
|
||||
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
|
||||
equal_run += 1;
|
||||
self.equal_runs.insert((i, j), equal_run);
|
||||
|
||||
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
|
||||
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
};
|
||||
|
||||
let score = insertion_score.max(deletion_score).max(equality_score);
|
||||
self.scores.set(i, j, score);
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_score = f64::NEG_INFINITY;
|
||||
let mut next_old_text_ix = self.old_text_ix;
|
||||
let next_new_text_ix = self.new.len();
|
||||
for i in self.old_text_ix..=self.old.len() {
|
||||
let score = self.scores.get(i, next_new_text_ix);
|
||||
if score > max_score {
|
||||
max_score = score;
|
||||
next_old_text_ix = i;
|
||||
}
|
||||
}
|
||||
|
||||
let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
|
||||
self.old_text_ix = next_old_text_ix;
|
||||
self.new_text_ix = next_new_text_ix;
|
||||
hunks
|
||||
}
|
||||
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||
let mut pending_insert: Option<Range<usize>> = None;
|
||||
let mut hunks = Vec::new();
|
||||
let mut i = old_text_ix;
|
||||
let mut j = new_text_ix;
|
||||
while (i, j) != (self.old_text_ix, self.new_text_ix) {
|
||||
let insertion_score = if j > self.new_text_ix {
|
||||
Some((i, j - 1))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deletion_score = if i > self.old_text_ix {
|
||||
Some((i - 1, j))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
|
||||
if self.old[i - 1] == self.new[j - 1] {
|
||||
Some((i - 1, j - 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
|
||||
.iter()
|
||||
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if prev_i == i && prev_j == j - 1 {
|
||||
if let Some(pending_insert) = pending_insert.as_mut() {
|
||||
pending_insert.start = prev_j;
|
||||
} else {
|
||||
pending_insert = Some(prev_j..j);
|
||||
}
|
||||
} else {
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let char_len = self.old[i - 1].len_utf8();
|
||||
if prev_i == i - 1 && prev_j == j {
|
||||
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else {
|
||||
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = prev_i;
|
||||
j = prev_j;
|
||||
}
|
||||
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
hunks.reverse();
|
||||
hunks
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<Hunk> {
|
||||
self.backtrack(self.old.len(), self.new.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_diffs(mut rng: StdRng) {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let old = util::RandomCharIter::new(&mut rng)
|
||||
.take(old_text_len)
|
||||
.collect::<String>();
|
||||
log::info!("old text: {:?}", old);
|
||||
|
||||
let mut diff = StreamingDiff::new(old.clone());
|
||||
let mut hunks = Vec::new();
|
||||
let mut new_len = 0;
|
||||
let mut new = String::new();
|
||||
while new_len < new_text_len {
|
||||
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||
.take(new_len)
|
||||
.collect::<String>();
|
||||
log::info!("new chunk: {:?}", new_chunk);
|
||||
new_len += new_chunk_len;
|
||||
new.push_str(&new_chunk);
|
||||
let new_hunks = diff.push_new(&new_chunk);
|
||||
log::info!("hunks: {:?}", new_hunks);
|
||||
hunks.extend(new_hunks);
|
||||
}
|
||||
let final_hunks = diff.finish();
|
||||
log::info!("final hunks: {:?}", final_hunks);
|
||||
hunks.extend(final_hunks);
|
||||
|
||||
log::info!("new text: {:?}", new);
|
||||
let mut old_ix = 0;
|
||||
let mut new_ix = 0;
|
||||
let mut patched = String::new();
|
||||
for hunk in hunks {
|
||||
match hunk {
|
||||
Hunk::Keep { len } => {
|
||||
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||
patched.push_str(&old[old_ix..old_ix + len]);
|
||||
old_ix += len;
|
||||
new_ix += len;
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
old_ix += len;
|
||||
}
|
||||
Hunk::Insert { text } => {
|
||||
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||
patched.push_str(&text);
|
||||
new_ix += text.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(patched, new);
|
||||
}
|
||||
}
|
@ -1635,6 +1635,15 @@ impl Editor {
|
||||
self.read_only = read_only;
|
||||
}
|
||||
|
||||
pub fn set_field_editor_style(
|
||||
&mut self,
|
||||
style: Option<Arc<GetFieldEditorTheme>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.get_field_editor_theme = style;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
|
||||
self.replica_id_mapping.as_ref()
|
||||
}
|
||||
@ -4989,6 +4998,9 @@ impl Editor {
|
||||
self.unmark_text(cx);
|
||||
self.refresh_copilot_suggestions(true, cx);
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(Event::TransactionUndone {
|
||||
transaction_id: tx_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -8428,6 +8440,9 @@ pub enum Event {
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
},
|
||||
TransactionUndone {
|
||||
transaction_id: TransactionId,
|
||||
},
|
||||
Closed,
|
||||
}
|
||||
|
||||
@ -8468,7 +8483,7 @@ impl View for Editor {
|
||||
"Editor"
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
let focused_event = EditorFocused(cx.handle());
|
||||
cx.emit(Event::Focused);
|
||||
@ -8476,7 +8491,7 @@ impl View for Editor {
|
||||
}
|
||||
if let Some(rename) = self.pending_rename.as_ref() {
|
||||
cx.focus(&rename.editor);
|
||||
} else {
|
||||
} else if cx.is_self_focused() || !focused.is::<Editor>() {
|
||||
if !self.focused {
|
||||
self.blink_manager.update(cx, BlinkManager::enable);
|
||||
}
|
||||
|
@ -617,6 +617,42 @@ impl MultiBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_transactions(
|
||||
&mut self,
|
||||
transaction: TransactionId,
|
||||
destination: TransactionId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.merge_transactions(transaction, destination)
|
||||
});
|
||||
} else {
|
||||
if let Some(transaction) = self.history.forget(transaction) {
|
||||
if let Some(destination) = self.history.transaction_mut(destination) {
|
||||
for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
|
||||
if let Some(destination_buffer_transaction_id) =
|
||||
destination.buffer_transactions.get(&buffer_id)
|
||||
{
|
||||
if let Some(state) = self.buffers.borrow().get(&buffer_id) {
|
||||
state.buffer.update(cx, |buffer, _| {
|
||||
buffer.merge_transactions(
|
||||
buffer_transaction_id,
|
||||
*destination_buffer_transaction_id,
|
||||
)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
destination
|
||||
.buffer_transactions
|
||||
.insert(buffer_id, buffer_transaction_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.history.finalize_last_transaction();
|
||||
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
||||
@ -788,6 +824,20 @@ impl MultiBuffer {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext<Self>) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
} else if let Some(transaction) = self.history.remove_from_undo(transaction_id) {
|
||||
for (buffer_id, transaction_id) in &transaction.buffer_transactions {
|
||||
if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.undo_transaction(*transaction_id, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stream_excerpts_with_context_lines(
|
||||
&mut self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
@ -2316,6 +2366,16 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_non_blank_row(&self, mut row: u32) -> Option<u32> {
|
||||
while row > 0 {
|
||||
row -= 1;
|
||||
if !self.is_line_blank(row) {
|
||||
return Some(row);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: u32) -> u32 {
|
||||
if let Some((_, range)) = self.buffer_line_for_row(row) {
|
||||
range.end.column - range.start.column
|
||||
@ -3347,6 +3407,35 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||
if let Some(ix) = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)
|
||||
{
|
||||
Some(self.undo_stack.remove(ix))
|
||||
} else if let Some(ix) = self
|
||||
.redo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)
|
||||
{
|
||||
Some(self.redo_stack.remove(ix))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
|
||||
self.undo_stack
|
||||
.iter_mut()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
.or_else(|| {
|
||||
self.redo_stack
|
||||
.iter_mut()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn pop_undo(&mut self) -> Option<&mut Transaction> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction) = self.undo_stack.pop() {
|
||||
@ -3367,6 +3456,16 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||
let ix = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)?;
|
||||
let transaction = self.undo_stack.remove(ix);
|
||||
self.redo_stack.push(transaction);
|
||||
self.redo_stack.last()
|
||||
}
|
||||
|
||||
fn group(&mut self) -> Option<TransactionId> {
|
||||
let mut count = 0;
|
||||
let mut transactions = self.undo_stack.iter();
|
||||
|
@ -1298,6 +1298,10 @@ impl Buffer {
|
||||
self.text.forget_transaction(transaction_id);
|
||||
}
|
||||
|
||||
pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||
self.text.merge_transactions(transaction, destination);
|
||||
}
|
||||
|
||||
pub fn wait_for_edits(
|
||||
&mut self,
|
||||
edit_ids: impl IntoIterator<Item = clock::Local>,
|
||||
@ -1664,6 +1668,22 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo_transaction(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> bool {
|
||||
let was_dirty = self.is_dirty();
|
||||
let old_version = self.version.clone();
|
||||
if let Some(operation) = self.text.undo_transaction(transaction_id) {
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo_to_transaction(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
|
@ -9,6 +9,7 @@ path = "src/quick_action_bar.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai = { path = "../ai" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
search = { path = "../search" }
|
||||
|
@ -1,25 +1,29 @@
|
||||
use ai::{assistant::InlineAssist, AssistantPanel};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
|
||||
use search::{buffer_search, BufferSearchBar};
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace};
|
||||
|
||||
pub struct QuickActionBar {
|
||||
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl QuickActionBar {
|
||||
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
|
||||
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>, workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
buffer_search_bar,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +92,21 @@ impl View for QuickActionBar {
|
||||
));
|
||||
}
|
||||
|
||||
bar.add_child(render_quick_action_bar_button(
|
||||
2,
|
||||
"icons/radix/magic-wand.svg",
|
||||
false,
|
||||
("Inline Assist".into(), Some(Box::new(InlineAssist))),
|
||||
cx,
|
||||
move |this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
AssistantPanel::inline_assist(workspace, &Default::default(), cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
bar.into_any()
|
||||
}
|
||||
}
|
||||
|
@ -384,6 +384,16 @@ impl<'a> From<&'a str> for Rope {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<&'a str> for Rope {
|
||||
fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
|
||||
let mut rope = Rope::new();
|
||||
for chunk in iter {
|
||||
rope.push(chunk);
|
||||
}
|
||||
rope
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Rope {
|
||||
fn from(text: String) -> Self {
|
||||
Rope::from(text.as_str())
|
||||
|
@ -22,6 +22,7 @@ use postage::{oneshot, prelude::*};
|
||||
|
||||
pub use rope::*;
|
||||
pub use selection::*;
|
||||
use util::ResultExt;
|
||||
|
||||
use std::{
|
||||
cmp::{self, Ordering, Reverse},
|
||||
@ -263,7 +264,19 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
|
||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
|
||||
let entry_ix = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|entry| entry.transaction.id == transaction_id)?;
|
||||
let entry = self.undo_stack.remove(entry_ix);
|
||||
self.redo_stack.push(entry);
|
||||
self.redo_stack.last()
|
||||
}
|
||||
|
||||
fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
|
||||
let redo_stack_start_len = self.redo_stack.len();
|
||||
@ -278,20 +291,43 @@ impl History {
|
||||
&self.redo_stack[redo_stack_start_len..]
|
||||
}
|
||||
|
||||
fn forget(&mut self, transaction_id: TransactionId) {
|
||||
fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(entry_ix) = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||
{
|
||||
self.undo_stack.remove(entry_ix);
|
||||
Some(self.undo_stack.remove(entry_ix).transaction)
|
||||
} else if let Some(entry_ix) = self
|
||||
.redo_stack
|
||||
.iter()
|
||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||
{
|
||||
self.undo_stack.remove(entry_ix);
|
||||
Some(self.redo_stack.remove(entry_ix).transaction)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
|
||||
let entry = self
|
||||
.undo_stack
|
||||
.iter_mut()
|
||||
.rfind(|entry| entry.transaction.id == transaction_id)
|
||||
.or_else(|| {
|
||||
self.redo_stack
|
||||
.iter_mut()
|
||||
.rfind(|entry| entry.transaction.id == transaction_id)
|
||||
})?;
|
||||
Some(&mut entry.transaction)
|
||||
}
|
||||
|
||||
fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||
if let Some(transaction) = self.forget(transaction) {
|
||||
if let Some(destination) = self.transaction_mut(destination) {
|
||||
destination.edit_ids.extend(transaction.edit_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1183,11 +1219,20 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
|
||||
let transaction = self
|
||||
.history
|
||||
.remove_from_undo(transaction_id)?
|
||||
.transaction
|
||||
.clone();
|
||||
self.undo_or_redo(transaction).log_err()
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
|
||||
let transactions = self
|
||||
.history
|
||||
.remove_from_undo(transaction_id)
|
||||
.remove_from_undo_until(transaction_id)
|
||||
.iter()
|
||||
.map(|entry| entry.transaction.clone())
|
||||
.collect::<Vec<_>>();
|
||||
@ -1202,6 +1247,10 @@ impl Buffer {
|
||||
self.history.forget(transaction_id);
|
||||
}
|
||||
|
||||
pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||
self.history.merge_transactions(transaction, destination);
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
|
||||
if let Some(entry) = self.history.pop_redo() {
|
||||
let transaction = entry.transaction.clone();
|
||||
|
@ -1150,6 +1150,17 @@ pub struct AssistantStyle {
|
||||
pub api_key_editor: FieldEditor,
|
||||
pub api_key_prompt: ContainedText,
|
||||
pub saved_conversation: SavedConversation,
|
||||
pub inline: InlineAssistantStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct InlineAssistantStyle {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub editor: FieldEditor,
|
||||
pub disabled_editor: FieldEditor,
|
||||
pub pending_edit_background: Color,
|
||||
pub include_conversation: ToggleIconButtonStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
|
@ -264,8 +264,9 @@ pub fn initialize_workspace(
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar.clone(), cx);
|
||||
let quick_action_bar =
|
||||
cx.add_view(|_| QuickActionBar::new(buffer_search_bar));
|
||||
let quick_action_bar = cx.add_view(|_| {
|
||||
QuickActionBar::new(buffer_search_bar, workspace)
|
||||
});
|
||||
toolbar.add_item(quick_action_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { text, border, background, foreground, TextStyle } from "./components"
|
||||
import { Interactive, interactive } from "../element"
|
||||
import { Interactive, interactive, toggleable } from "../element"
|
||||
import { tab_bar_button } from "../component/tab_bar_button"
|
||||
import { StyleSets, useTheme } from "../theme"
|
||||
|
||||
@ -59,6 +59,85 @@ export default function assistant(): any {
|
||||
background: background(theme.highest),
|
||||
padding: { left: 12 },
|
||||
},
|
||||
inline: {
|
||||
background: background(theme.highest),
|
||||
margin: { top: 3, bottom: 3 },
|
||||
border: border(theme.lowest, "on", {
|
||||
top: true,
|
||||
bottom: true,
|
||||
overlay: true,
|
||||
}),
|
||||
editor: {
|
||||
text: text(theme.highest, "mono", "default", { size: "sm" }),
|
||||
placeholder_text: text(theme.highest, "sans", "on", "disabled"),
|
||||
selection: theme.players[0],
|
||||
},
|
||||
disabled_editor: {
|
||||
text: text(theme.highest, "mono", "disabled", { size: "sm" }),
|
||||
placeholder_text: text(theme.highest, "sans", "on", "disabled"),
|
||||
selection: {
|
||||
cursor: text(theme.highest, "mono", "disabled").color,
|
||||
selection: theme.players[0].selection,
|
||||
},
|
||||
},
|
||||
pending_edit_background: background(theme.highest, "positive"),
|
||||
include_conversation: toggleable({
|
||||
base: interactive({
|
||||
base: {
|
||||
icon_size: 12,
|
||||
color: foreground(theme.highest, "variant"),
|
||||
|
||||
button_width: 12,
|
||||
background: background(theme.highest, "on"),
|
||||
corner_radius: 2,
|
||||
border: {
|
||||
width: 1., color: background(theme.highest, "on")
|
||||
},
|
||||
padding: {
|
||||
left: 4,
|
||||
right: 4,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(theme.highest, "mono", "variant", "hovered"),
|
||||
background: background(theme.highest, "on", "hovered"),
|
||||
border: {
|
||||
width: 1., color: background(theme.highest, "on", "hovered")
|
||||
},
|
||||
},
|
||||
clicked: {
|
||||
...text(theme.highest, "mono", "variant", "pressed"),
|
||||
background: background(theme.highest, "on", "pressed"),
|
||||
border: {
|
||||
width: 1., color: background(theme.highest, "on", "pressed")
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
state: {
|
||||
active: {
|
||||
default: {
|
||||
icon_size: 12,
|
||||
button_width: 12,
|
||||
color: foreground(theme.highest, "variant"),
|
||||
background: background(theme.highest, "accent"),
|
||||
border: border(theme.highest, "accent"),
|
||||
},
|
||||
hovered: {
|
||||
background: background(theme.highest, "accent", "hovered"),
|
||||
border: border(theme.highest, "accent", "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
background: background(theme.highest, "accent", "pressed"),
|
||||
border: border(theme.highest, "accent", "pressed"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
message_header: {
|
||||
margin: { bottom: 4, top: 4 },
|
||||
background: background(theme.highest),
|
||||
|
Loading…
Reference in New Issue
Block a user