Introduce code generation (#2901)
 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
16 changed files with 2043 additions and 274 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -102,14 +102,20 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"collections",
|
"collections",
|
||||||
|
"ctor",
|
||||||
"editor",
|
"editor",
|
||||||
|
"env_logger 0.9.3",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
"isahc",
|
"isahc",
|
||||||
"language",
|
"language",
|
||||||
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
|
"ordered-float",
|
||||||
"project",
|
"project",
|
||||||
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"schemars",
|
"schemars",
|
||||||
"search",
|
"search",
|
||||||
|
@ -5649,6 +5655,7 @@ dependencies = [
|
||||||
name = "quick_action_bar"
|
name = "quick_action_bar"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ai",
|
||||||
"editor",
|
"editor",
|
||||||
"gpui",
|
"gpui",
|
||||||
"search",
|
"search",
|
||||||
|
|
|
@ -530,7 +530,8 @@
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-enter": "editor::OpenExcerpts",
|
"alt-enter": "editor::OpenExcerpts",
|
||||||
"cmd-f8": "editor::GoToHunk",
|
"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
|
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
|
||||||
|
ordered-float.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -35,3 +37,8 @@ tiktoken-rs = "0.4"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
project = { path = "../project", 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;
|
pub mod assistant;
|
||||||
mod assistant_settings;
|
mod assistant_settings;
|
||||||
|
mod streaming_diff;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
pub use assistant::AssistantPanel;
|
pub use assistant::AssistantPanel;
|
||||||
use assistant_settings::OpenAIModel;
|
use assistant_settings::OpenAIModel;
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::StreamExt;
|
use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||||
use gpui::AppContext;
|
use gpui::{executor::Background, AppContext};
|
||||||
|
use isahc::{http::StatusCode, Request, RequestExt};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Reverse,
|
cmp::Reverse,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
|
io,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use util::paths::CONVERSATIONS_DIR;
|
use util::paths::CONVERSATIONS_DIR;
|
||||||
|
|
||||||
|
const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
|
||||||
|
|
||||||
// Data types for chat completion requests
|
// Data types for chat completion requests
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct OpenAIRequest {
|
pub struct OpenAIRequest {
|
||||||
model: String,
|
model: String,
|
||||||
messages: Vec<RequestMessage>,
|
messages: Vec<RequestMessage>,
|
||||||
stream: bool,
|
stream: bool,
|
||||||
|
@ -116,7 +121,7 @@ struct RequestMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||||
struct ResponseMessage {
|
pub struct ResponseMessage {
|
||||||
role: Option<Role>,
|
role: Option<Role>,
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -150,7 +155,7 @@ impl Display for Role {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct OpenAIResponseStreamEvent {
|
pub struct OpenAIResponseStreamEvent {
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
pub object: String,
|
pub object: String,
|
||||||
pub created: u32,
|
pub created: u32,
|
||||||
|
@ -160,14 +165,14 @@ struct OpenAIResponseStreamEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Usage {
|
pub struct Usage {
|
||||||
pub prompt_tokens: u32,
|
pub prompt_tokens: u32,
|
||||||
pub completion_tokens: u32,
|
pub completion_tokens: u32,
|
||||||
pub total_tokens: u32,
|
pub total_tokens: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ChatChoiceDelta {
|
pub struct ChatChoiceDelta {
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
pub delta: ResponseMessage,
|
pub delta: ResponseMessage,
|
||||||
pub finish_reason: Option<String>,
|
pub finish_reason: Option<String>,
|
||||||
|
@ -191,3 +196,97 @@ struct OpenAIChoice {
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
assistant::init(cx);
|
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;
|
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>> {
|
pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
|
||||||
self.replica_id_mapping.as_ref()
|
self.replica_id_mapping.as_ref()
|
||||||
}
|
}
|
||||||
|
@ -4989,6 +4998,9 @@ impl Editor {
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(true, cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
|
cx.emit(Event::TransactionUndone {
|
||||||
|
transaction_id: tx_id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8428,6 +8440,9 @@ pub enum Event {
|
||||||
local: bool,
|
local: bool,
|
||||||
autoscroll: bool,
|
autoscroll: bool,
|
||||||
},
|
},
|
||||||
|
TransactionUndone {
|
||||||
|
transaction_id: TransactionId,
|
||||||
|
},
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8468,7 +8483,7 @@ impl View for Editor {
|
||||||
"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() {
|
if cx.is_self_focused() {
|
||||||
let focused_event = EditorFocused(cx.handle());
|
let focused_event = EditorFocused(cx.handle());
|
||||||
cx.emit(Event::Focused);
|
cx.emit(Event::Focused);
|
||||||
|
@ -8476,7 +8491,7 @@ impl View for Editor {
|
||||||
}
|
}
|
||||||
if let Some(rename) = self.pending_rename.as_ref() {
|
if let Some(rename) = self.pending_rename.as_ref() {
|
||||||
cx.focus(&rename.editor);
|
cx.focus(&rename.editor);
|
||||||
} else {
|
} else if cx.is_self_focused() || !focused.is::<Editor>() {
|
||||||
if !self.focused {
|
if !self.focused {
|
||||||
self.blink_manager.update(cx, BlinkManager::enable);
|
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>) {
|
pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
self.history.finalize_last_transaction();
|
self.history.finalize_last_transaction();
|
||||||
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
||||||
|
@ -788,6 +824,20 @@ impl MultiBuffer {
|
||||||
None
|
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(
|
pub fn stream_excerpts_with_context_lines(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: ModelHandle<Buffer>,
|
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 {
|
pub fn line_len(&self, row: u32) -> u32 {
|
||||||
if let Some((_, range)) = self.buffer_line_for_row(row) {
|
if let Some((_, range)) = self.buffer_line_for_row(row) {
|
||||||
range.end.column - range.start.column
|
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> {
|
fn pop_undo(&mut self) -> Option<&mut Transaction> {
|
||||||
assert_eq!(self.transaction_depth, 0);
|
assert_eq!(self.transaction_depth, 0);
|
||||||
if let Some(transaction) = self.undo_stack.pop() {
|
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> {
|
fn group(&mut self) -> Option<TransactionId> {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut transactions = self.undo_stack.iter();
|
let mut transactions = self.undo_stack.iter();
|
||||||
|
|
|
@ -1298,6 +1298,10 @@ impl Buffer {
|
||||||
self.text.forget_transaction(transaction_id);
|
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(
|
pub fn wait_for_edits(
|
||||||
&mut self,
|
&mut self,
|
||||||
edit_ids: impl IntoIterator<Item = clock::Local>,
|
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(
|
pub fn undo_to_transaction(
|
||||||
&mut self,
|
&mut self,
|
||||||
transaction_id: TransactionId,
|
transaction_id: TransactionId,
|
||||||
|
|
|
@ -9,6 +9,7 @@ path = "src/quick_action_bar.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ai = { path = "../ai" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
search = { path = "../search" }
|
search = { path = "../search" }
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
|
use ai::{assistant::InlineAssist, AssistantPanel};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
|
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
|
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
|
||||||
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use search::{buffer_search, BufferSearchBar};
|
use search::{buffer_search, BufferSearchBar};
|
||||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace};
|
||||||
|
|
||||||
pub struct QuickActionBar {
|
pub struct QuickActionBar {
|
||||||
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
||||||
active_item: Option<Box<dyn ItemHandle>>,
|
active_item: Option<Box<dyn ItemHandle>>,
|
||||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||||
|
workspace: WeakViewHandle<Workspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QuickActionBar {
|
impl QuickActionBar {
|
||||||
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
|
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>, workspace: &Workspace) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer_search_bar,
|
buffer_search_bar,
|
||||||
active_item: None,
|
active_item: None,
|
||||||
_inlay_hints_enabled_subscription: 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()
|
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 {
|
impl From<String> for Rope {
|
||||||
fn from(text: String) -> Self {
|
fn from(text: String) -> Self {
|
||||||
Rope::from(text.as_str())
|
Rope::from(text.as_str())
|
||||||
|
|
|
@ -22,6 +22,7 @@ use postage::{oneshot, prelude::*};
|
||||||
|
|
||||||
pub use rope::*;
|
pub use rope::*;
|
||||||
pub use selection::*;
|
pub use selection::*;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering, Reverse},
|
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);
|
assert_eq!(self.transaction_depth, 0);
|
||||||
|
|
||||||
let redo_stack_start_len = self.redo_stack.len();
|
let redo_stack_start_len = self.redo_stack.len();
|
||||||
|
@ -278,20 +291,43 @@ impl History {
|
||||||
&self.redo_stack[redo_stack_start_len..]
|
&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);
|
assert_eq!(self.transaction_depth, 0);
|
||||||
if let Some(entry_ix) = self
|
if let Some(entry_ix) = self
|
||||||
.undo_stack
|
.undo_stack
|
||||||
.iter()
|
.iter()
|
||||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
.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
|
} else if let Some(entry_ix) = self
|
||||||
.redo_stack
|
.redo_stack
|
||||||
.iter()
|
.iter()
|
||||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
.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)]
|
#[allow(clippy::needless_collect)]
|
||||||
pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
|
pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
|
||||||
let transactions = self
|
let transactions = self
|
||||||
.history
|
.history
|
||||||
.remove_from_undo(transaction_id)
|
.remove_from_undo_until(transaction_id)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| entry.transaction.clone())
|
.map(|entry| entry.transaction.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -1202,6 +1247,10 @@ impl Buffer {
|
||||||
self.history.forget(transaction_id);
|
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)> {
|
pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
|
||||||
if let Some(entry) = self.history.pop_redo() {
|
if let Some(entry) = self.history.pop_redo() {
|
||||||
let transaction = entry.transaction.clone();
|
let transaction = entry.transaction.clone();
|
||||||
|
|
|
@ -1150,6 +1150,17 @@ pub struct AssistantStyle {
|
||||||
pub api_key_editor: FieldEditor,
|
pub api_key_editor: FieldEditor,
|
||||||
pub api_key_prompt: ContainedText,
|
pub api_key_prompt: ContainedText,
|
||||||
pub saved_conversation: SavedConversation,
|
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)]
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
|
|
@ -264,8 +264,9 @@ pub fn initialize_workspace(
|
||||||
toolbar.add_item(breadcrumbs, cx);
|
toolbar.add_item(breadcrumbs, cx);
|
||||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||||
toolbar.add_item(buffer_search_bar.clone(), cx);
|
toolbar.add_item(buffer_search_bar.clone(), cx);
|
||||||
let quick_action_bar =
|
let quick_action_bar = cx.add_view(|_| {
|
||||||
cx.add_view(|_| QuickActionBar::new(buffer_search_bar));
|
QuickActionBar::new(buffer_search_bar, workspace)
|
||||||
|
});
|
||||||
toolbar.add_item(quick_action_bar, cx);
|
toolbar.add_item(quick_action_bar, cx);
|
||||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||||
toolbar.add_item(project_search_bar, cx);
|
toolbar.add_item(project_search_bar, cx);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { text, border, background, foreground, TextStyle } from "./components"
|
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 { tab_bar_button } from "../component/tab_bar_button"
|
||||||
import { StyleSets, useTheme } from "../theme"
|
import { StyleSets, useTheme } from "../theme"
|
||||||
|
|
||||||
|
@ -59,6 +59,85 @@ export default function assistant(): any {
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
padding: { left: 12 },
|
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: {
|
message_header: {
|
||||||
margin: { bottom: 4, top: 4 },
|
margin: { bottom: 4, top: 4 },
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue