From 7a67ec57436eb048f10b2febb3b0220f3a7d1fa0 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 15 Aug 2023 12:48:30 -0400 Subject: [PATCH 001/114] Add support for querying multiple language servers for completions --- crates/project/src/project.rs | 84 ++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1aa2a2dd40..f85460770d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3861,7 +3861,7 @@ impl Project { let file = File::from_dyn(buffer.file())?; let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); let server = self - .primary_language_servers_for_buffer(buffer, cx) + .primary_language_server_for_buffer(buffer, cx) .map(|s| s.1.clone()); Some((buffer_handle, buffer_abs_path, server)) }) @@ -4166,7 +4166,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetDefinition { position }, cx) + self.request_primary_lsp(buffer.clone(), GetDefinition { position }, cx) } pub fn type_definition( @@ -4176,7 +4176,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetTypeDefinition { position }, cx) + self.request_primary_lsp(buffer.clone(), GetTypeDefinition { position }, cx) } pub fn references( @@ -4186,7 +4186,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetReferences { position }, cx) + self.request_primary_lsp(buffer.clone(), GetReferences { position }, cx) } pub fn document_highlights( @@ -4196,7 +4196,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetDocumentHighlights { position }, cx) + self.request_primary_lsp(buffer.clone(), GetDocumentHighlights { position }, cx) } pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { @@ -4424,7 +4424,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetHover { position }, cx) + self.request_primary_lsp(buffer.clone(), GetHover { position }, cx) } pub fn completions( @@ -4434,7 +4434,29 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetCompletions { position }, cx) + let server_ids: Vec<_> = self + .language_servers_for_buffer(buffer.read(cx), cx) + .map(|(_, server)| server.server_id()) + .collect(); + + let buffer = buffer.clone(); + cx.spawn(|this, mut cx| async move { + let mut completions = Vec::new(); + + for server_id in server_ids { + let new_completions = this + .update(&mut cx, |this, cx| { + this.request_lsp(buffer.clone(), server_id, GetCompletions { position }, cx) + }) + .await; + + if let Ok(new_completions) = new_completions { + completions.extend_from_slice(&new_completions); + } + } + + Ok(completions) + }) } pub fn apply_additional_edits_for_completion( @@ -4448,7 +4470,7 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = match self.primary_language_servers_for_buffer(buffer, cx) { + let lang_server = match self.primary_language_server_for_buffer(buffer, cx) { Some((_, server)) => server.clone(), _ => return Task::ready(Ok(Default::default())), }; @@ -4545,7 +4567,7 @@ impl Project { ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); - self.request_lsp(buffer_handle.clone(), GetCodeActions { range }, cx) + self.request_primary_lsp(buffer_handle.clone(), GetCodeActions { range }, cx) } pub fn apply_code_action( @@ -4901,7 +4923,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer, PrepareRename { position }, cx) + self.request_primary_lsp(buffer, PrepareRename { position }, cx) } pub fn perform_rename( @@ -4913,7 +4935,7 @@ impl Project { cx: &mut ModelContext, ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp( + self.request_primary_lsp( buffer, PerformRename { position, @@ -4940,7 +4962,7 @@ impl Project { .tab_size, ) }); - self.request_lsp( + self.request_primary_lsp( buffer.clone(), OnTypeFormatting { position, @@ -4967,7 +4989,7 @@ impl Project { let lsp_request = InlayHints { range }; if self.is_local() { - let lsp_request_task = self.request_lsp(buffer_handle.clone(), lsp_request, cx); + let lsp_request_task = self.request_primary_lsp(buffer_handle.clone(), lsp_request, cx); cx.spawn(|_, mut cx| async move { buffer_handle .update(&mut cx, |buffer, _| { @@ -5223,23 +5245,42 @@ impl Project { } } - // TODO: Wire this up to allow selecting a server? - fn request_lsp( + fn request_primary_lsp( &self, buffer_handle: ModelHandle, request: R, cx: &mut ModelContext, ) -> Task> + where + ::Result: Send, + { + let buffer = buffer_handle.read(cx); + let server_id = match self.primary_language_server_for_buffer(buffer, cx) { + Some((_, server)) => server.server_id(), + None => return Task::ready(Ok(Default::default())), + }; + + self.request_lsp(buffer_handle, server_id, request, cx) + } + + fn request_lsp( + &self, + buffer_handle: ModelHandle, + server_id: LanguageServerId, + request: R, + cx: &mut ModelContext, + ) -> Task> where ::Result: Send, { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, language_server)) = file.zip( - self.primary_language_servers_for_buffer(buffer, cx) - .map(|(_, server)| server.clone()), - ) { + let language_server = self + .language_server_for_buffer(buffer, server_id, cx) + .map(|(_, server)| server.clone()); + + if let (Some(file), Some(language_server)) = (file, language_server) { let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx); return cx.spawn(|this, cx| async move { if !request.check_capabilities(language_server.capabilities()) { @@ -5294,6 +5335,7 @@ impl Project { } }); } + Task::ready(Ok(Default::default())) } @@ -6842,7 +6884,7 @@ impl Project { let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version()); let response = this .update(&mut cx, |this, cx| { - this.request_lsp(buffer_handle, request, cx) + this.request_primary_lsp(buffer_handle, request, cx) }) .await?; this.update(&mut cx, |this, cx| { @@ -7558,7 +7600,7 @@ impl Project { }) } - fn primary_language_servers_for_buffer( + fn primary_language_server_for_buffer( &self, buffer: &Buffer, cx: &AppContext, From 40ce099780f51b7863bb45d8237f754b8195e40b Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 15 Aug 2023 16:34:15 -0400 Subject: [PATCH 002/114] Use originating language server to resolve additional completion edits --- crates/language/src/buffer.rs | 1 + crates/language/src/proto.rs | 2 ++ crates/project/src/lsp_command.rs | 3 ++- crates/project/src/project.rs | 3 ++- crates/rpc/proto/zed.proto | 3 ++- crates/rpc/src/rpc.rs | 2 +- 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0b10432a9f..ec5f9541f5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -149,6 +149,7 @@ pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, + pub server_id: LanguageServerId, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 0de3f704c7..c463c7a01c 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -433,6 +433,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion { old_start: Some(serialize_anchor(&completion.old_range.start)), old_end: Some(serialize_anchor(&completion.old_range.end)), new_text: completion.new_text.clone(), + server_id: completion.server_id.0 as u64, lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), } } @@ -465,6 +466,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), + server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a8692257d8..ad5b63ae2a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1338,7 +1338,7 @@ impl LspCommand for GetCompletions { completions: Option, _: ModelHandle, buffer: ModelHandle, - _: LanguageServerId, + server_id: LanguageServerId, cx: AsyncAppContext, ) -> Result> { let completions = if let Some(completions) = completions { @@ -1425,6 +1425,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), + server_id, lsp_completion, } }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f85460770d..7986d6faec 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4470,7 +4470,8 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = match self.primary_language_server_for_buffer(buffer, cx) { + let server_id = completion.server_id; + let lang_server = match self.language_server_for_buffer(buffer, server_id, cx) { Some((_, server)) => server.clone(), _ => return Task::ready(Ok(Default::default())), }; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a0b98372b1..2bc31dccc3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -630,7 +630,8 @@ message Completion { Anchor old_start = 1; Anchor old_end = 2; string new_text = 3; - bytes lsp_completion = 4; + uint64 server_id = 4; + bytes lsp_completion = 5; } message GetCodeActions { diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 6b430d90e4..3cb8b6bffa 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 59; +pub const PROTOCOL_VERSION: u32 = 60; From 8839b07a25911197763a63f5ff32af66f30d6daf Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 16 Aug 2023 11:53:05 -0400 Subject: [PATCH 003/114] Add broken Tailwind language server --- crates/zed/src/languages.rs | 7 +- crates/zed/src/languages/tailwind.rs | 126 +++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 crates/zed/src/languages/tailwind.rs diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index eb31c08dd2..0f8699aff2 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -18,6 +18,7 @@ mod python; mod ruby; mod rust; mod svelte; +mod tailwind; mod typescript; mod yaml; @@ -116,7 +117,11 @@ pub fn init(languages: Arc, node_runtime: Arc) { language( "html", tree_sitter_html::language(), - vec![Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))], + vec![ + // Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), + // Arc::new(emmet::EmmetLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], ); language( "ruby", diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs new file mode 100644 index 0000000000..79f65eb578 --- /dev/null +++ b/crates/zed/src/languages/tailwind.rs @@ -0,0 +1,126 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::json; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct TailwindLspAdapter { + node: Arc, +} + +impl TailwindLspAdapter { + pub fn new(node: Arc) -> Self { + TailwindLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for TailwindLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("tailwindcss-language-server".into()) + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("@tailwindcss/language-server") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + dbg!(&container_dir, version.as_str()); + self.node + .npm_install_packages( + &container_dir, + [("@tailwindcss/language-server", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} From e54f16f37205a75b6b9ff37eda63c8315eece50c Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 16 Aug 2023 21:25:17 -0400 Subject: [PATCH 004/114] Register initial request handlers before launching server --- crates/lsp/src/lsp.rs | 2 +- crates/project/src/project.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d806..66ef33418b 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -282,7 +282,7 @@ impl LanguageServer { stdout.read_exact(&mut buffer).await?; if let Ok(message) = str::from_utf8(&buffer) { - log::trace!("incoming message:{}", message); + log::trace!("incoming message: {}", message); for handler in io_handlers.lock().values_mut() { handler(true, message); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7986d6faec..4e16ee3da2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2748,10 +2748,8 @@ impl Project { ) -> Result>> { let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; let language_server = match pending_server.task.await? { - Some(server) => server.initialize(initialization_options).await?, - None => { - return Ok(None); - } + Some(server) => server, + None => return Ok(None), }; language_server @@ -2909,7 +2907,9 @@ impl Project { ) .ok(); - Ok(Some(language_server)) + Ok(Some( + language_server.initialize(initialization_options).await?, + )) } fn insert_newly_running_language_server( From 4f0fa21c04db0aa61074732bce1789c2c7e3955e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 17 Aug 2023 15:02:33 +0300 Subject: [PATCH 005/114] Provide more data to tailwind langserver Tailwind needs user languages and language-to-language-id mappings to start providing completions for those languages. And also it has emmet completions disabled by default, enable them. --- crates/zed/src/languages/tailwind.rs | 35 +++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 79f65eb578..0290bf3334 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -1,10 +1,15 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use futures::StreamExt; +use collections::HashMap; +use futures::{ + future::{self, BoxFuture}, + FutureExt, StreamExt, +}; +use gpui::AppContext; use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; -use serde_json::json; +use serde_json::{json, Value}; use smol::fs; use std::{ any::Any, @@ -89,9 +94,33 @@ impl LspAdapter for TailwindLspAdapter { async fn initialization_options(&self) -> Option { Some(json!({ - "provideFormatter": true + "provideFormatter": true, + "userLanguages": { + "html": "html", + "css": "css", + "javascript": "javascript", + }, })) } + + fn workspace_configuration(&self, _: &mut AppContext) -> Option> { + Some( + future::ready(json!({ + "tailwindCSS": { + "emmetCompletions": true, + } + })) + .boxed(), + ) + } + + async fn language_ids(&self) -> HashMap { + HashMap::from([ + ("HTML".to_string(), "html".to_string()), + ("CSS".to_string(), "css".to_string()), + ("JavaScript".to_string(), "javascript".to_string()), + ]) + } } async fn get_cached_server_binary( From a979e3212762a9ed67a43cee3ef281361cf83ab0 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 17 Aug 2023 21:57:39 -0400 Subject: [PATCH 006/114] Utilize LSP completion `itemDefaults` a bit Tailwind likes to throw a lot of completion data at us, this gets it to send less. Previously it would respond to a completion with 2.5 MB JSON blob, now it is more like 0.8 MB. Relies on a local copy of lsp-types with the `itemDefaults` field added. I don't have write perms to push to our fork of the crate atm, sorry :) --- Cargo.lock | 4 +- crates/lsp/Cargo.toml | 2 +- crates/lsp/src/lsp.rs | 8 ++++ crates/project/src/lsp_command.rs | 69 +++++++++++++++++++++++-------- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ff9981a6a..4e1d0b53a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4185,9 +4185,7 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.94.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" +version = "0.94.1" dependencies = [ "bitflags 1.3.2", "serde", diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 47e0995c85..c3ff37dc61 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -20,7 +20,7 @@ anyhow.workspace = true async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true } futures.workspace = true log.workspace = true -lsp-types = "0.94" +lsp-types = { path = "../../../lsp-types" } parking_lot.workspace = true postage.workspace = true serde.workspace = true diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 66ef33418b..f39d97aeb5 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -423,6 +423,14 @@ impl LanguageServer { }), ..Default::default() }), + completion_list: Some(CompletionListCapability { + item_defaults: Some(vec![ + "commitCharacters".to_owned(), + "editRange".to_owned(), + "insertTextMode".to_owned(), + "data".to_owned(), + ]), + }), ..Default::default() }), rename: Some(RenameClientCapabilities { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ad5b63ae2a..c1bb890d49 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -15,7 +15,10 @@ use language::{ range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; -use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; +use lsp::{ + CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId, + ServerCapabilities, +}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { @@ -1341,10 +1344,16 @@ impl LspCommand for GetCompletions { server_id: LanguageServerId, cx: AsyncAppContext, ) -> Result> { + let mut response_list = None; let completions = if let Some(completions) = completions { match completions { lsp::CompletionResponse::Array(completions) => completions, - lsp::CompletionResponse::List(list) => list.items, + + lsp::CompletionResponse::List(mut list) => { + let items = std::mem::take(&mut list.items); + response_list = Some(list); + items + } } } else { Default::default() @@ -1354,6 +1363,7 @@ impl LspCommand for GetCompletions { let language = buffer.language().cloned(); let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); + let mut range_for_token = None; completions .into_iter() @@ -1374,6 +1384,7 @@ impl LspCommand for GetCompletions { edit.new_text.clone(), ) } + // If the language server does not provide a range, then infer // the range based on the syntax tree. None => { @@ -1381,27 +1392,51 @@ impl LspCommand for GetCompletions { log::info!("completion out of expected range"); return None; } - let Range { start, end } = range_for_token - .get_or_insert_with(|| { - let offset = self.position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset); - if kind == Some(CharKind::Word) { - range - } else { - offset..offset - } - }) - .clone(); + + let default_edit_range = response_list + .as_ref() + .and_then(|list| list.item_defaults.as_ref()) + .and_then(|defaults| defaults.edit_range.as_ref()) + .and_then(|range| match range { + CompletionListItemDefaultsEditRange::Range(r) => Some(r), + _ => None, + }); + + let range = if let Some(range) = default_edit_range { + let range = range_from_lsp(range.clone()); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return None; + } + + snapshot.anchor_before(start)..snapshot.anchor_after(end) + } else { + range_for_token + .get_or_insert_with(|| { + let offset = self.position.to_offset(&snapshot); + let (range, kind) = snapshot.surrounding_word(offset); + let range = if kind == Some(CharKind::Word) { + range + } else { + offset..offset + }; + + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .clone() + }; + let text = lsp_completion .insert_text .as_ref() .unwrap_or(&lsp_completion.label) .clone(); - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - text, - ) + (range, text) } + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; From c842e8707971e0f3f77a846a0fe04be7a6fe6fea Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 18 Aug 2023 11:57:19 -0400 Subject: [PATCH 007/114] Use updated lsp-types fork branch --- Cargo.lock | 1 + crates/lsp/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4e1d0b53a6..e27b5756aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4186,6 +4186,7 @@ dependencies = [ [[package]] name = "lsp-types" version = "0.94.1" +source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a" dependencies = [ "bitflags 1.3.2", "serde", diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index c3ff37dc61..653c25b7bb 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -20,7 +20,7 @@ anyhow.workspace = true async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true } futures.workspace = true log.workspace = true -lsp-types = { path = "../../../lsp-types" } +lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" } parking_lot.workspace = true postage.workspace = true serde.workspace = true From 3ad7f528cb0eb2f953d138d3d499163d8993f8ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Aug 2023 17:58:22 +0200 Subject: [PATCH 008/114] Start on a refactoring assistant --- crates/ai/src/ai.rs | 1 + crates/ai/src/refactor.rs | 88 +++++++++++++++++++++++++++++++++++++++ prompt.md | 11 +++++ 3 files changed, 100 insertions(+) create mode 100644 crates/ai/src/refactor.rs create mode 100644 prompt.md diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 7cc5f08f7c..7874bb46a5 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,5 +1,6 @@ pub mod assistant; mod assistant_settings; +mod refactor; use anyhow::Result; pub use assistant::AssistantPanel; diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs new file mode 100644 index 0000000000..e1b57680ee --- /dev/null +++ b/crates/ai/src/refactor.rs @@ -0,0 +1,88 @@ +use collections::HashMap; +use editor::Editor; +use gpui::{ + actions, elements::*, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle, +}; +use std::sync::Arc; +use workspace::{Modal, Workspace}; + +actions!(assistant, [Refactor]); + +fn init(cx: &mut AppContext) { + cx.set_global(RefactoringAssistant::new()); + cx.add_action(RefactoringModal::deploy); +} + +pub struct RefactoringAssistant { + pending_edits_by_editor: HashMap>>, +} + +impl RefactoringAssistant { + fn new() -> Self { + Self { + pending_edits_by_editor: Default::default(), + } + } + + fn refactor(&mut self, editor: &ViewHandle, prompt: &str, cx: &mut AppContext) {} +} + +struct RefactoringModal { + prompt_editor: ViewHandle, + has_focus: bool, +} + +impl Entity for RefactoringModal { + type Event = (); +} + +impl View for RefactoringModal { + fn ui_name() -> &'static str { + "RefactoringModal" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + todo!() + } + + fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = true; + } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl Modal for RefactoringModal { + fn has_focus(&self) -> bool { + self.has_focus + } + + fn dismiss_on_event(event: &Self::Event) -> bool { + todo!() + } +} + +impl RefactoringModal { + fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { + workspace.toggle_modal(cx, |_, cx| { + let prompt_editor = cx.add_view(|cx| { + Editor::auto_height( + 4, + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ) + }); + cx.add_view(|_| RefactoringModal { + prompt_editor, + has_focus: false, + }) + }); + } +} + +// ABCDEFG +// XCDEFG +// +// diff --git a/prompt.md b/prompt.md new file mode 100644 index 0000000000..33213a5859 --- /dev/null +++ b/prompt.md @@ -0,0 +1,11 @@ +Given a snippet as the input, you must produce an array of edits. An edit has the following structure: + +{ skip: "skip", delete: "delete", insert: "insert" } + +`skip` is a string in the input that should be left unchanged. `delete` is a string in the input located right after the skipped text that should be deleted. `insert` is a new string that should be inserted after the end of the text in `skip`. It's crucial that a string in the input can only be skipped or deleted once and only once. + +Your task is to produce an array of edits. `delete` and `insert` can be empty if nothing changed. When `skip`, `delete` or `insert` are longer than 20 characters, split them into multiple edits. + +Check your reasoning by concatenating all the strings in `skip` and `delete`. If the text is the same as the input snippet then the edits are valid. + +It's crucial that you reply only with edits. No prose or remarks. From 42f02eb4e7adafe27d444b8b9ffbe68ddce9e714 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Aug 2023 15:11:06 +0200 Subject: [PATCH 009/114] Incrementally diff input coming from GPT --- Cargo.lock | 1 + crates/ai/Cargo.toml | 1 + crates/ai/src/ai.rs | 107 +++++++++++++++-- crates/ai/src/assistant.rs | 99 +--------------- crates/ai/src/refactor.rs | 233 +++++++++++++++++++++++++++++++++---- prompt.md | 11 -- 6 files changed, 315 insertions(+), 137 deletions(-) delete mode 100644 prompt.md diff --git a/Cargo.lock b/Cargo.lock index 69285a1abf..f802d90739 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,7 @@ dependencies = [ "serde", "serde_json", "settings", + "similar", "smol", "theme", "tiktoken-rs 0.4.5", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 013565e14f..bae20f7537 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -29,6 +29,7 @@ regex.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true +similar = "1.3" smol.workspace = true tiktoken-rs = "0.4" diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 7874bb46a5..511e7fddd7 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -2,27 +2,31 @@ pub mod assistant; mod assistant_settings; mod refactor; -use anyhow::Result; +use anyhow::{anyhow, Result}; pub use assistant::AssistantPanel; 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, stream: bool, @@ -116,7 +120,7 @@ struct RequestMessage { } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -struct ResponseMessage { +pub struct ResponseMessage { role: Option, content: Option, } @@ -150,7 +154,7 @@ impl Display for Role { } #[derive(Deserialize, Debug)] -struct OpenAIResponseStreamEvent { +pub struct OpenAIResponseStreamEvent { pub id: Option, pub object: String, pub created: u32, @@ -160,14 +164,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, @@ -190,4 +194,91 @@ struct OpenAIChoice { pub fn init(cx: &mut AppContext) { assistant::init(cx); + refactor::init(cx); +} + +pub async fn stream_completion( + api_key: String, + executor: Arc, + mut request: OpenAIRequest, +) -> Result>> { + request.stream = true; + + let (tx, rx) = futures::channel::mpsc::unbounded::>(); + + 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, + ) -> Result> { + 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::(&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, + )), + } + } } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index e5026182ed..f134eeeeb6 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,7 +1,7 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, - MessageId, MessageMetadata, MessageStatus, OpenAIRequest, OpenAIResponseStreamEvent, - RequestMessage, Role, SavedConversation, SavedConversationMetadata, SavedMessage, + stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, + Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; @@ -12,26 +12,23 @@ use editor::{ Anchor, Editor, ToOffset, }; use fs::Fs; -use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; +use futures::StreamExt; use gpui::{ actions, elements::*, - executor::Background, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton}, Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use search::BufferSearchBar; -use serde::Deserialize; use settings::SettingsStore; use std::{ cell::RefCell, cmp, env, fmt::Write, - io, iter, + iter, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -46,8 +43,6 @@ use workspace::{ Save, ToggleZoom, Toolbar, Workspace, }; -const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; - actions!( assistant, [ @@ -2144,92 +2139,6 @@ impl Message { } } -async fn stream_completion( - api_key: String, - executor: Arc, - mut request: OpenAIRequest, -) -> Result>> { - request.stream = true; - - let (tx, rx) = futures::channel::mpsc::unbounded::>(); - - 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, - ) -> Result> { - 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::(&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)] mod tests { use super::*; diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index e1b57680ee..fc6cbdb8c4 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -1,16 +1,24 @@ -use collections::HashMap; -use editor::Editor; +use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; +use collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use editor::{Anchor, Editor, MultiBuffer, MultiBufferSnapshot, ToOffset}; +use futures::{io::BufWriter, AsyncReadExt, AsyncWriteExt, StreamExt}; use gpui::{ actions, elements::*, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle, + WeakViewHandle, }; -use std::sync::Arc; +use menu::Confirm; +use serde::Deserialize; +use similar::ChangeTag; +use std::{env, iter, ops::Range, sync::Arc}; +use util::TryFutureExt; use workspace::{Modal, Workspace}; actions!(assistant, [Refactor]); -fn init(cx: &mut AppContext) { +pub fn init(cx: &mut AppContext) { cx.set_global(RefactoringAssistant::new()); cx.add_action(RefactoringModal::deploy); + cx.add_action(RefactoringModal::confirm); } pub struct RefactoringAssistant { @@ -24,10 +32,122 @@ impl RefactoringAssistant { } } - fn refactor(&mut self, editor: &ViewHandle, prompt: &str, cx: &mut AppContext) {} + fn refactor(&mut self, editor: &ViewHandle, prompt: &str, cx: &mut AppContext) { + let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); + let selection = editor.read(cx).selections.newest_anchor().clone(); + let selected_text = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let language_name = buffer + .language_at(selection.start) + .map(|language| language.name()); + let language_name = language_name.as_deref().unwrap_or(""); + let request = OpenAIRequest { + model: "gpt-4".into(), + messages: vec![ + RequestMessage { + role: Role::User, + content: format!( + "Given the following {language_name} snippet:\n{selected_text}\n{prompt}. Avoid making remarks and reply only with the new code." + ), + }], + stream: true, + }; + let api_key = env::var("OPENAI_API_KEY").unwrap(); + let response = stream_completion(api_key, cx.background().clone(), request); + let editor = editor.downgrade(); + self.pending_edits_by_editor.insert( + editor.id(), + cx.spawn(|mut cx| { + async move { + let selection_start = selection.start.to_offset(&buffer); + + // 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 messages = response.await?; + let mut new_word_search_start_ix = 0; + let mut last_old_word_end_ix = 0; + + 'outer: loop { + let start = new_word_search_start_ix; + let mut words = words(&new_text[start..]); + while let Some((range, new_word)) = words.next() { + // We found a word in the new text that was unique in the old text. We can use + // it as a diff boundary, and start applying edits. + if let Some(old_word_end_ix) = unique_old_words.remove(new_word) { + if old_word_end_ix > last_old_word_end_ix { + drop(words); + + 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; + } + drop(words); + + // Buffer incoming text, stopping if the stream was exhausted. + if let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + if let Some(text) = choice.delta.content { + new_text.push_str(&text); + } + } + } 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 + .buffer() + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)) + })?; + + anyhow::Ok(()) + } + .log_err() + }), + ); + } } struct RefactoringModal { + editor: WeakViewHandle, prompt_editor: ViewHandle, has_focus: bool, } @@ -42,7 +162,7 @@ impl View for RefactoringModal { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - todo!() + ChildView::new(&self.prompt_editor, cx).into_any() } fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext) { @@ -60,29 +180,96 @@ impl Modal for RefactoringModal { } fn dismiss_on_event(event: &Self::Event) -> bool { - todo!() + // TODO + false } } impl RefactoringModal { fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |_, cx| { - let prompt_editor = cx.add_view(|cx| { - Editor::auto_height( - 4, - Some(Arc::new(|theme| theme.search.editor.input.clone())), - cx, - ) + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| Some(item.downcast::()?.downgrade())) + { + workspace.toggle_modal(cx, |_, cx| { + let prompt_editor = cx.add_view(|cx| { + Editor::auto_height( + 4, + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ) + }); + cx.add_view(|_| RefactoringModal { + editor, + prompt_editor, + has_focus: false, + }) }); - cx.add_view(|_| RefactoringModal { - prompt_editor, - has_focus: false, - }) - }); + } + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some(editor) = self.editor.upgrade(cx) { + let prompt = self.prompt_editor.read(cx).text(cx); + cx.update_global(|assistant: &mut RefactoringAssistant, cx| { + assistant.refactor(&editor, &prompt, cx); + }); + } } } +fn words(text: &str) -> impl Iterator, &str)> { + let mut word_start_ix = None; + let mut chars = text.char_indices(); + iter::from_fn(move || { + while let Some((ix, ch)) = chars.next() { + if let Some(start_ix) = word_start_ix { + if !ch.is_alphanumeric() { + let word = &text[start_ix..ix]; + word_start_ix.take(); + return Some((start_ix..ix, word)); + } + } else { + if ch.is_alphanumeric() { + word_start_ix = Some(ix); + } + } + } + None + }) +} -// ABCDEFG -// XCDEFG -// -// +fn diff<'a>( + start_ix: usize, + old_text: &'a str, + new_text: &'a str, + old_buffer_snapshot: &MultiBufferSnapshot, +) -> Vec<(Range, &'a str)> { + let mut edit_start = start_ix; + let mut edits = Vec::new(); + let diff = similar::TextDiff::from_words(old_text, &new_text); + for change in diff.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 => { + edits.push(( + old_buffer_snapshot.anchor_after(edit_start) + ..old_buffer_snapshot.anchor_before(edit_end), + "", + )); + edit_start = edit_end; + } + ChangeTag::Insert => { + edits.push(( + old_buffer_snapshot.anchor_after(edit_start) + ..old_buffer_snapshot.anchor_after(edit_start), + value, + )); + } + } + } + edits +} diff --git a/prompt.md b/prompt.md deleted file mode 100644 index 33213a5859..0000000000 --- a/prompt.md +++ /dev/null @@ -1,11 +0,0 @@ -Given a snippet as the input, you must produce an array of edits. An edit has the following structure: - -{ skip: "skip", delete: "delete", insert: "insert" } - -`skip` is a string in the input that should be left unchanged. `delete` is a string in the input located right after the skipped text that should be deleted. `insert` is a new string that should be inserted after the end of the text in `skip`. It's crucial that a string in the input can only be skipped or deleted once and only once. - -Your task is to produce an array of edits. `delete` and `insert` can be empty if nothing changed. When `skip`, `delete` or `insert` are longer than 20 characters, split them into multiple edits. - -Check your reasoning by concatenating all the strings in `skip` and `delete`. If the text is the same as the input snippet then the edits are valid. - -It's crucial that you reply only with edits. No prose or remarks. From 5b9d48d723eff66eb99afc72208ab90ba2015bb1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Aug 2023 15:53:43 +0200 Subject: [PATCH 010/114] Avoid diffing when the length is too small --- crates/ai/src/refactor.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index fc6cbdb8c4..1a1d02cf1f 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -80,13 +80,17 @@ impl RefactoringAssistant { let mut last_old_word_end_ix = 0; 'outer: loop { + const MIN_DIFF_LEN: usize = 50; + let start = new_word_search_start_ix; let mut words = words(&new_text[start..]); while let Some((range, new_word)) = words.next() { // We found a word in the new text that was unique in the old text. We can use // it as a diff boundary, and start applying edits. - if let Some(old_word_end_ix) = unique_old_words.remove(new_word) { - if old_word_end_ix > last_old_word_end_ix { + if let Some(old_word_end_ix) = unique_old_words.get(new_word).copied() { + if old_word_end_ix.saturating_sub(last_old_word_end_ix) + > MIN_DIFF_LEN + { drop(words); let remainder = new_text.split_off(start + range.end); From 5453553cfa4eb8b1a21a066402ed3ba82067a240 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Aug 2023 08:16:22 +0200 Subject: [PATCH 011/114] WIP --- Cargo.lock | 1 + crates/ai/Cargo.toml | 1 + crates/ai/src/refactor.rs | 341 ++++++++++++++++++++++++++------------ 3 files changed, 234 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f802d90739..af16a88596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,7 @@ dependencies = [ "fs", "futures 0.3.28", "gpui", + "indoc", "isahc", "language", "menu", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index bae20f7537..5ef371e342 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -24,6 +24,7 @@ workspace = { path = "../workspace" } anyhow.workspace = true chrono = { version = "0.4", features = ["serde"] } futures.workspace = true +indoc.workspace = true isahc.workspace = true regex.workspace = true schemars.workspace = true diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 1a1d02cf1f..1923ef7845 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -1,14 +1,13 @@ use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; -use collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use editor::{Anchor, Editor, MultiBuffer, MultiBufferSnapshot, ToOffset}; -use futures::{io::BufWriter, AsyncReadExt, AsyncWriteExt, StreamExt}; +use collections::HashMap; +use editor::{Editor, ToOffset}; +use futures::StreamExt; use gpui::{ actions, elements::*, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::Confirm; -use serde::Deserialize; -use similar::ChangeTag; +use similar::{Change, ChangeTag, TextDiff}; use std::{env, iter, ops::Range, sync::Arc}; use util::TryFutureExt; use workspace::{Modal, Workspace}; @@ -33,12 +32,12 @@ impl RefactoringAssistant { } fn refactor(&mut self, editor: &ViewHandle, 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 selected_text = buffer + let selected_text = snapshot .text_for_range(selection.start..selection.end) .collect::(); - let language_name = buffer + let language_name = snapshot .language_at(selection.start) .map(|language| language.name()); let language_name = language_name.as_deref().unwrap_or(""); @@ -48,7 +47,7 @@ impl RefactoringAssistant { RequestMessage { role: Role::User, 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, @@ -60,86 +59,149 @@ impl RefactoringAssistant { editor.id(), cx.spawn(|mut cx| { async move { - let selection_start = selection.start.to_offset(&buffer); - - // 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 selection_start = selection.start.to_offset(&snapshot); let mut new_text = String::new(); let mut messages = response.await?; - let mut new_word_search_start_ix = 0; - let mut last_old_word_end_ix = 0; - 'outer: loop { - const MIN_DIFF_LEN: usize = 50; + let mut transaction = None; - let start = new_word_search_start_ix; - let mut words = words(&new_text[start..]); - while let Some((range, new_word)) = words.next() { - // We found a word in the new text that was unique in the old text. We can use - // it as a diff boundary, and start applying edits. - if let Some(old_word_end_ix) = unique_old_words.get(new_word).copied() { - if old_word_end_ix.saturating_sub(last_old_word_end_ix) - > MIN_DIFF_LEN + while let Some(message) = messages.next().await { + smol::future::yield_now().await; + let mut message = message?; + if let Some(choice) = message.choices.pop() { + if let Some(text) = choice.delta.content { + new_text.push_str(&text); + + 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::>(); + + 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); - - 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; + changes.pop(); } - } - new_word_search_start_ix = start + range.end; - } - drop(words); + editor.update(&mut cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + if let Some(transaction) = transaction.take() { + buffer.undo(cx); // TODO: Undo the transaction instead + } - // Buffer incoming text, stopping if the stream was exhausted. - if let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - if let Some(text) = choice.delta.content { - new_text.push_str(&text); - } + buffer.start_transaction(cx); + let mut edit_start = selection_start; + dbg!(&changes); + for change in 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, + ); + } + } + } + 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 - .buffer() - .update(cx, |buffer, cx| buffer.edit(edits, None, cx)) + editor.buffer().update(cx, |buffer, cx| { + if let Some(transaction) = transaction.take() { + 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(()) @@ -197,11 +259,13 @@ impl RefactoringModal { { workspace.toggle_modal(cx, |_, cx| { let prompt_editor = cx.add_view(|cx| { - Editor::auto_height( + let mut editor = Editor::auto_height( 4, Some(Arc::new(|theme| theme.search.editor.input.clone())), cx, - ) + ); + editor.set_text("Replace with match statement.", cx); + editor }); cx.add_view(|_| RefactoringModal { editor, @@ -242,38 +306,97 @@ fn words(text: &str) -> impl Iterator, &str)> { }) } -fn diff<'a>( - start_ix: usize, - old_text: &'a str, - new_text: &'a str, - old_buffer_snapshot: &MultiBufferSnapshot, -) -> Vec<(Range, &'a str)> { - let mut edit_start = start_ix; - let mut edits = Vec::new(); - let diff = similar::TextDiff::from_words(old_text, &new_text); - for change in diff.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 => { - edits.push(( - old_buffer_snapshot.anchor_after(edit_start) - ..old_buffer_snapshot.anchor_before(edit_end), - "", - )); - edit_start = edit_end; - } - ChangeTag::Insert => { - edits.push(( - old_buffer_snapshot.anchor_after(edit_start) - ..old_buffer_snapshot.anchor_after(edit_start), - value, - )); - } +fn streaming_diff<'a>(old_text: &'a str, new_text: &'a str) -> Vec> { + let changes = TextDiff::configure() + .algorithm(similar::Algorithm::Patience) + .diff_words(old_text, new_text); + let mut changes = changes.iter_all_changes().peekable(); + + let mut result = vec![]; + + loop { + let mut deletions = vec![]; + let mut insertions = vec![]; + + while changes + .peek() + .map_or(false, |change| change.tag() == ChangeTag::Delete) + { + deletions.push(changes.next().unwrap()); + } + + while changes + .peek() + .map_or(false, |change| change.tag() == ChangeTag::Insert) + { + insertions.push(changes.next().unwrap()); + } + + if !deletions.is_empty() && !insertions.is_empty() { + result.append(&mut insertions); + 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::(dst_rect, src_bytes, src_stride, src_format); + } + (Format::A8, Format::Rgb24) => { + return self + .blit_from_with::(dst_rect, src_bytes, src_stride, src_format); + } + (Format::Rgb24, Format::A8) => { + return self + .blit_from_with::(dst_rect, src_bytes, src_stride, src_format); + } + (Format::Rgb24, Format::Rgba32) => { + return self.blit_from_with::( + 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)); + } } From 007d1b09ac970cfc27736dd3d798a96041d84387 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:35:20 +0200 Subject: [PATCH 012/114] Z 2819 (#2872) This PR adds new config option to language config called `word_boundaries` that controls which characters should be recognised as word boundary for a given language. This will improve our UX for languages such as PHP and Tailwind. Release Notes: - Improved completions for PHP [#1820](https://github.com/zed-industries/community/issues/1820) --------- Co-authored-by: Julia Risley --- crates/editor/src/editor.rs | 1 - crates/editor/src/items.rs | 29 +++++++++++++---------- crates/editor/src/movement.rs | 30 +++++++++++++++++++----- crates/editor/src/multi_buffer.rs | 11 +++++---- crates/language/src/buffer.rs | 25 +++++++++++++------- crates/language/src/language.rs | 5 +++- crates/project/src/project.rs | 2 +- crates/project/src/search.rs | 27 ++++++++++++++++----- crates/vim/src/motion.rs | 18 ++++++++------ crates/vim/src/normal/change.rs | 9 ++++--- crates/vim/src/object.rs | 21 ++++++++++------- crates/zed/src/languages/php/config.toml | 1 + 12 files changed, 120 insertions(+), 59 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 256ef2284c..9e24e56efe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2654,7 +2654,6 @@ impl Editor { false }); } - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { let offset = position.to_offset(buffer); let (word_range, kind) = buffer.surrounding_word(offset); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b99977a60e..4a2b03bbdf 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1028,7 +1028,7 @@ impl SearchableItem for Editor { if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { ranges.extend( query - .search(excerpt_buffer.as_rope()) + .search(excerpt_buffer, None) .await .into_iter() .map(|range| { @@ -1038,17 +1038,22 @@ impl SearchableItem for Editor { } else { for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer); - let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone()); - ranges.extend(query.search(&rope).await.into_iter().map(|range| { - let start = excerpt - .buffer - .anchor_after(excerpt_range.start + range.start); - let end = excerpt - .buffer - .anchor_before(excerpt_range.start + range.end); - buffer.anchor_in_excerpt(excerpt.id.clone(), start) - ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) - })); + ranges.extend( + query + .search(&excerpt.buffer, Some(excerpt_range.clone())) + .await + .into_iter() + .map(|range| { + let start = excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(excerpt.id.clone(), start) + ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + }), + ); } } ranges diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index f70436abeb..5917b8b3bd 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -176,14 +176,21 @@ pub fn line_end( } pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); + find_preceding_boundary(map, point, |left, right| { - (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n' + (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) + || left == '\n' }) } pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_preceding_boundary(map, point, |left, right| { - let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace(); + let is_word_start = + char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); let is_subword_start = left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start || left == '\n' @@ -191,14 +198,20 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis } pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_boundary(map, point, |left, right| { - (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n' + (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) + || right == '\n' }) } pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_boundary(map, point, |left, right| { - let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace(); + let is_word_end = + (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); let is_subword_end = left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_end || is_subword_end || right == '\n' @@ -385,10 +398,15 @@ pub fn find_boundary_in_line( } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; - let next_char_kind = text.chars_at(ix).next().map(char_kind); - let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind); + let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c)); + let prev_char_kind = text + .reversed_chars_at(ix) + .next() + .map(|c| char_kind(language, c)); prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8417c411f2..d4061f25dc 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1865,13 +1865,16 @@ impl MultiBufferSnapshot { let mut end = start; let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); + + let language = self.language_at(start); + let kind = |c| char_kind(language, c); let word_kind = cmp::max( - prev_chars.peek().copied().map(char_kind), - next_chars.peek().copied().map(char_kind), + prev_chars.peek().copied().map(kind), + next_chars.peek().copied().map(kind), ); for ch in prev_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { start -= ch.len_utf8(); } else { break; @@ -1879,7 +1882,7 @@ impl MultiBufferSnapshot { } for ch in next_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { end += ch.len_utf8(); } else { break; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ec5f9541f5..eff95460c4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2174,13 +2174,16 @@ impl BufferSnapshot { let mut end = start; let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); + + let language = self.language_at(start); + let kind = |c| char_kind(language, c); let word_kind = cmp::max( - prev_chars.peek().copied().map(char_kind), - next_chars.peek().copied().map(char_kind), + prev_chars.peek().copied().map(kind), + next_chars.peek().copied().map(kind), ); for ch in prev_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { start -= ch.len_utf8(); } else { break; @@ -2188,7 +2191,7 @@ impl BufferSnapshot { } for ch in next_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { end += ch.len_utf8(); } else { break; @@ -2985,14 +2988,18 @@ pub fn contiguous_ranges( }) } -pub fn char_kind(c: char) -> CharKind { +pub fn char_kind(language: Option<&Arc>, c: char) -> CharKind { if c.is_whitespace() { - CharKind::Whitespace + return CharKind::Whitespace; } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation + return CharKind::Word; } + if let Some(language) = language { + if language.config.word_characters.contains(&c) { + return CharKind::Word; + } + } + CharKind::Punctuation } /// Find all of the ranges of whitespace that occur at the ends of lines diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 223f5679ae..82245d67ca 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -11,7 +11,7 @@ mod buffer_tests; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use collections::HashMap; +use collections::{HashMap, HashSet}; use futures::{ channel::oneshot, future::{BoxFuture, Shared}, @@ -344,6 +344,8 @@ pub struct LanguageConfig { pub block_comment: Option<(Arc, Arc)>, #[serde(default)] pub overrides: HashMap, + #[serde(default)] + pub word_characters: HashSet, } #[derive(Debug, Default)] @@ -411,6 +413,7 @@ impl Default for LanguageConfig { block_comment: Default::default(), overrides: Default::default(), collapsed_placeholder: Default::default(), + word_characters: Default::default(), } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4e16ee3da2..4b3c80c08a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5193,7 +5193,7 @@ impl Project { snapshot.file().map(|file| file.path().as_ref()), ) { query - .search(snapshot.as_rope()) + .search(&snapshot, None) .await .iter() .map(|range| { diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 71a0b70b81..bfa34c0422 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -3,7 +3,7 @@ use anyhow::{Context, Result}; use client::proto; use globset::{Glob, GlobMatcher}; use itertools::Itertools; -use language::{char_kind, Rope}; +use language::{char_kind, BufferSnapshot}; use regex::{Regex, RegexBuilder}; use smol::future::yield_now; use std::{ @@ -23,6 +23,7 @@ pub enum SearchQuery { files_to_include: Vec, files_to_exclude: Vec, }, + Regex { regex: Regex, query: Arc, @@ -193,12 +194,24 @@ impl SearchQuery { } } - pub async fn search(&self, rope: &Rope) -> Vec> { + pub async fn search( + &self, + buffer: &BufferSnapshot, + subrange: Option>, + ) -> Vec> { const YIELD_INTERVAL: usize = 20000; if self.as_str().is_empty() { return Default::default(); } + let language = buffer.language_at(0); + let rope = if let Some(range) = subrange { + buffer.as_rope().slice(range) + } else { + buffer.as_rope().clone() + }; + + let kind = |c| char_kind(language, c); let mut matches = Vec::new(); match self { @@ -215,10 +228,10 @@ impl SearchQuery { let mat = mat.unwrap(); if *whole_word { - let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); - let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = rope.chars_at(mat.end()).next().map(char_kind); + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind); + let start_kind = kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(kind); if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { continue; } @@ -226,6 +239,7 @@ impl SearchQuery { matches.push(mat.start()..mat.end()) } } + Self::Regex { regex, multiline, .. } => { @@ -263,6 +277,7 @@ impl SearchQuery { } } } + matches } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index acf9d46ad3..1defee70da 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -439,11 +439,12 @@ pub(crate) fn next_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { + let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { let mut crossed_newline = false; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); let at_newline = right == '\n'; let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) @@ -463,11 +464,12 @@ fn next_word_end( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { + let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { *point.column_mut() += 1; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); @@ -493,12 +495,13 @@ fn previous_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { + let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { // This works even though find_preceding_boundary is called for every character in the line containing // cursor because the newline is checked only once. point = movement::find_preceding_boundary(map, point, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); (left_kind != right_kind && !right.is_whitespace()) || left == '\n' }); @@ -508,6 +511,7 @@ fn previous_word_start( fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { let mut last_point = DisplayPoint::new(from.row(), 0); + let language = map.buffer_snapshot.language_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { return from; @@ -515,7 +519,7 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi last_point = point; - if char_kind(ch) != CharKind::Whitespace { + if char_kind(language, ch) != CharKind::Whitespace { break; } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index d226c70410..50bc049a3a 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -82,16 +82,19 @@ fn expand_changed_word_selection( ignore_punctuation: bool, ) -> bool { if times.is_none() || times.unwrap() == 1 { + let language = map + .buffer_snapshot + .language_at(selection.start.to_point(map)); let in_word = map .chars_at(selection.head()) .next() - .map(|(c, _)| char_kind(c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) .unwrap_or_default(); if in_word { selection.end = movement::find_boundary(map, selection.end, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 85e6eab692..d0bcad36c2 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -122,17 +122,18 @@ fn in_word( ignore_punctuation: bool, ) -> Option> { // Use motion::right so that we consider the character under the cursor when looking for the start + let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(left).coerce_punctuation(ignore_punctuation) - != char_kind(right).coerce_punctuation(ignore_punctuation) + char_kind(language, left).coerce_punctuation(ignore_punctuation) + != char_kind(language, right).coerce_punctuation(ignore_punctuation) }, ); let end = movement::find_boundary_in_line(map, relative_to, |left, right| { - char_kind(left).coerce_punctuation(ignore_punctuation) - != char_kind(right).coerce_punctuation(ignore_punctuation) + char_kind(language, left).coerce_punctuation(ignore_punctuation) + != char_kind(language, right).coerce_punctuation(ignore_punctuation) }); Some(start..end) @@ -155,10 +156,11 @@ fn around_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { + let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); let in_word = map .chars_at(relative_to) .next() - .map(|(c, _)| char_kind(c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) .unwrap_or(false); if in_word { @@ -182,20 +184,21 @@ fn around_next_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { + let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); // Get the start of the word let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(left).coerce_punctuation(ignore_punctuation) - != char_kind(right).coerce_punctuation(ignore_punctuation) + char_kind(language, left).coerce_punctuation(ignore_punctuation) + != char_kind(language, right).coerce_punctuation(ignore_punctuation) }, ); let mut word_found = false; let end = movement::find_boundary(map, relative_to, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; diff --git a/crates/zed/src/languages/php/config.toml b/crates/zed/src/languages/php/config.toml index 19acb949e2..60dd233555 100644 --- a/crates/zed/src/languages/php/config.toml +++ b/crates/zed/src/languages/php/config.toml @@ -10,3 +10,4 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, ] collapsed_placeholder = "/* ... */" +word_characters = ["$"] From a35b3f39c5c692a5d6ab1df7c1f26d933f3fb601 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 12:41:59 +0300 Subject: [PATCH 013/114] Expand word characters for html and css --- crates/zed/src/languages/css/config.toml | 1 + crates/zed/src/languages/html/config.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/zed/src/languages/css/config.toml b/crates/zed/src/languages/css/config.toml index ba9660c4ed..05de4be8a3 100644 --- a/crates/zed/src/languages/css/config.toml +++ b/crates/zed/src/languages/css/config.toml @@ -8,3 +8,4 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, ] +word_characters = ["-"] diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml index 077a421ce1..164e095cee 100644 --- a/crates/zed/src/languages/html/config.toml +++ b/crates/zed/src/languages/html/config.toml @@ -10,3 +10,4 @@ brackets = [ { start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] }, { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, ] +word_characters = ["-"] From 814896de3f5655eb9c529e1e39b497be73cf9a0b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 12:51:14 +0300 Subject: [PATCH 014/114] Reenable html, remove emmet due to the lack of the code --- crates/zed/src/languages.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 0f8699aff2..2cdd540e8a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -118,8 +118,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { "html", tree_sitter_html::language(), vec![ - // Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), - // Arc::new(emmet::EmmetLspAdapter::new(node_runtime.clone())), + Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); From 1ae5a909cdc3f4a0db34951b26555602626736e9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Aug 2023 12:07:41 +0200 Subject: [PATCH 015/114] Start on a custom diff implementation --- crates/ai/src/ai.rs | 1 + crates/ai/src/diff.rs | 180 ++++++++++++++++++++++++++++++++++++++ crates/ai/src/refactor.rs | 43 --------- 3 files changed, 181 insertions(+), 43 deletions(-) create mode 100644 crates/ai/src/diff.rs diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 511e7fddd7..52f31d2f56 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,5 +1,6 @@ pub mod assistant; mod assistant_settings; +mod diff; mod refactor; use anyhow::{anyhow, Result}; diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs new file mode 100644 index 0000000000..b70aa40b62 --- /dev/null +++ b/crates/ai/src/diff.rs @@ -0,0 +1,180 @@ +use std::{ + cmp, + fmt::{self, Debug}, +}; + +use collections::BinaryHeap; + +struct Matrix { + cells: Vec, + 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) -> isize { + 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: isize) { + 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)] +enum Hunk { + Insert(char), + Remove(char), + Keep(char), +} + +struct Diff { + old: String, + new: String, + scores: Matrix, + last_diff_row: usize, +} + +impl Diff { + fn new(old: String) -> Self { + let mut scores = Matrix::new(); + scores.resize(old.len() + 1, 1); + for i in 0..=old.len() { + scores.set(i, 0, -(i as isize)); + } + dbg!(&scores); + Self { + old, + new: String::new(), + scores, + last_diff_row: 0, + } + } + + fn push_new(&mut self, text: &str) -> Vec { + let last_diff_column = self.new.len(); + self.new.push_str(text); + self.scores.resize(self.old.len() + 1, self.new.len() + 1); + + for j in last_diff_column + 1..=self.new.len() { + self.scores.set(0, j, -(j as isize)); + for i in 1..=self.old.len() { + let insertion_score = self.scores.get(i, j - 1) - 1; + let deletion_score = self.scores.get(i - 1, j) - 10; + let equality_score = if self.old.as_bytes()[i - 1] == self.new.as_bytes()[j - 1] { + self.scores.get(i - 1, j - 1) + 5 + } else { + self.scores.get(i - 1, j - 1) - 20 + }; + let score = insertion_score.max(deletion_score).max(equality_score); + self.scores.set(i, j, score); + } + } + + let mut max_score = isize::MIN; + let mut best_row = self.last_diff_row; + for i in self.last_diff_row..=self.old.len() { + let score = self.scores.get(i, self.new.len()); + if score > max_score { + max_score = score; + best_row = i; + } + } + + let mut hunks = Vec::new(); + let mut i = best_row; + let mut j = self.new.len(); + while (i, j) != (self.last_diff_row, last_diff_column) { + let insertion_score = if j > last_diff_column { + Some((i, j - 1)) + } else { + None + }; + let deletion_score = if i > self.last_diff_row { + Some((i - 1, j)) + } else { + None + }; + let equality_score = if i > self.last_diff_row && j > last_diff_column { + Some((i - 1, j - 1)) + } else { + None + }; + + let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] + .iter() + .max_by_key(|cell| cell.map(|(i, j)| self.scores.get(i, j))) + .unwrap() + .unwrap(); + + if prev_i == i && prev_j == j - 1 { + hunks.push(Hunk::Insert(self.new.chars().skip(j - 1).next().unwrap())); + } else if prev_i == i - 1 && prev_j == j { + hunks.push(Hunk::Remove(self.old.chars().skip(i - 1).next().unwrap())); + } else { + hunks.push(Hunk::Keep(self.old.chars().skip(i - 1).next().unwrap())); + } + + i = prev_i; + j = prev_j; + } + self.last_diff_row = best_row; + hunks.reverse(); + hunks + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_diff() { + let mut diff = Diff::new("hello world".to_string()); + dbg!(diff.push_new("hello")); + dbg!(diff.push_new(" ciaone")); + dbg!(diff.push_new(" world")); + } +} diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 1923ef7845..5bd1b5dcca 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -357,46 +357,3 @@ fn streaming_diff<'a>(old_text: &'a str, new_text: &'a str) -> Vec { - return self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format); - } - (Format::A8, Format::Rgb24) => { - return self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format); - } - (Format::Rgb24, Format::A8) => { - return self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format); - } - (Format::Rgb24, Format::Rgba32) => { - return self.blit_from_with::( - 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)); - } -} From 69b69678381cdd3d5ec0f0cb05941c13362af45f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Aug 2023 13:59:45 +0200 Subject: [PATCH 016/114] Integrate the new diff algorithm into the modal assistant --- crates/ai/src/diff.rs | 72 +++++++---- crates/ai/src/refactor.rs | 264 ++++++++------------------------------ 2 files changed, 101 insertions(+), 235 deletions(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index b70aa40b62..5e73c94ff8 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -64,41 +64,40 @@ impl Debug for Matrix { } #[derive(Debug)] -enum Hunk { - Insert(char), - Remove(char), - Keep(char), +pub enum Hunk { + Insert { len: usize }, + Remove { len: usize }, + Keep { len: usize }, } -struct Diff { +pub struct Diff { old: String, new: String, scores: Matrix, - last_diff_row: usize, + old_text_ix: usize, } impl Diff { - fn new(old: String) -> Self { + pub fn new(old: String) -> Self { let mut scores = Matrix::new(); scores.resize(old.len() + 1, 1); for i in 0..=old.len() { scores.set(i, 0, -(i as isize)); } - dbg!(&scores); Self { old, new: String::new(), scores, - last_diff_row: 0, + old_text_ix: 0, } } - fn push_new(&mut self, text: &str) -> Vec { - let last_diff_column = self.new.len(); + pub fn push_new(&mut self, text: &str) -> Vec { + let new_text_ix = self.new.len(); self.new.push_str(text); self.scores.resize(self.old.len() + 1, self.new.len() + 1); - for j in last_diff_column + 1..=self.new.len() { + for j in new_text_ix + 1..=self.new.len() { self.scores.set(0, j, -(j as isize)); for i in 1..=self.old.len() { let insertion_score = self.scores.get(i, j - 1) - 1; @@ -114,8 +113,8 @@ impl Diff { } let mut max_score = isize::MIN; - let mut best_row = self.last_diff_row; - for i in self.last_diff_row..=self.old.len() { + let mut best_row = self.old_text_ix; + for i in self.old_text_ix..=self.old.len() { let score = self.scores.get(i, self.new.len()); if score > max_score { max_score = score; @@ -126,18 +125,18 @@ impl Diff { let mut hunks = Vec::new(); let mut i = best_row; let mut j = self.new.len(); - while (i, j) != (self.last_diff_row, last_diff_column) { - let insertion_score = if j > last_diff_column { + while (i, j) != (self.old_text_ix, new_text_ix) { + let insertion_score = if j > new_text_ix { Some((i, j - 1)) } else { None }; - let deletion_score = if i > self.last_diff_row { + let deletion_score = if i > self.old_text_ix { Some((i - 1, j)) } else { None }; - let equality_score = if i > self.last_diff_row && j > last_diff_column { + let equality_score = if i > self.old_text_ix && j > new_text_ix { Some((i - 1, j - 1)) } else { None @@ -150,20 +149,42 @@ impl Diff { .unwrap(); if prev_i == i && prev_j == j - 1 { - hunks.push(Hunk::Insert(self.new.chars().skip(j - 1).next().unwrap())); + if let Some(Hunk::Insert { len }) = hunks.last_mut() { + *len += 1; + } else { + hunks.push(Hunk::Insert { len: 1 }) + } } else if prev_i == i - 1 && prev_j == j { - hunks.push(Hunk::Remove(self.old.chars().skip(i - 1).next().unwrap())); + if let Some(Hunk::Remove { len }) = hunks.last_mut() { + *len += 1; + } else { + hunks.push(Hunk::Remove { len: 1 }) + } } else { - hunks.push(Hunk::Keep(self.old.chars().skip(i - 1).next().unwrap())); + if let Some(Hunk::Keep { len }) = hunks.last_mut() { + *len += 1; + } else { + hunks.push(Hunk::Keep { len: 1 }) + } } i = prev_i; j = prev_j; } - self.last_diff_row = best_row; + self.old_text_ix = best_row; hunks.reverse(); hunks } + + pub fn finish(self) -> Option { + if self.old_text_ix < self.old.len() { + Some(Hunk::Remove { + len: self.old.len() - self.old_text_ix, + }) + } else { + None + } + } } #[cfg(test)] @@ -173,8 +194,9 @@ mod tests { #[test] fn test_diff() { let mut diff = Diff::new("hello world".to_string()); - dbg!(diff.push_new("hello")); - dbg!(diff.push_new(" ciaone")); - dbg!(diff.push_new(" world")); + diff.push_new("hello"); + diff.push_new(" ciaone"); + diff.push_new(" world"); + diff.finish(); } } diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 5bd1b5dcca..dcec04deef 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -1,7 +1,7 @@ use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; use collections::HashMap; use editor::{Editor, ToOffset}; -use futures::StreamExt; +use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ actions, elements::*, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -59,151 +59,67 @@ impl RefactoringAssistant { editor.id(), cx.spawn(|mut cx| { async move { - let selection_start = selection.start.to_offset(&snapshot); + let mut edit_start = selection.start.to_offset(&snapshot); - let mut new_text = String::new(); - let mut messages = response.await?; + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let mut messages = response.await?.ready_chunks(4); + let mut diff = crate::diff::Diff::new(selected_text); - let mut transaction = None; - - while let Some(message) = messages.next().await { - smol::future::yield_now().await; - let mut message = message?; - if let Some(choice) = message.choices.pop() { - if let Some(text) = choice.delta.content { - new_text.push_str(&text); - - 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::>(); - - 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; + while let Some(messages) = messages.next().await { + let mut new_text = String::new(); + for message in messages { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + if let Some(text) = choice.delta.content { + new_text.push_str(&text); } - - 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) - { - changes.pop(); - } - - editor.update(&mut cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - if let Some(transaction) = transaction.take() { - buffer.undo(cx); // TODO: Undo the transaction instead - } - - buffer.start_transaction(cx); - let mut edit_start = selection_start; - dbg!(&changes); - for change in 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, - ); - } - } - } - transaction = buffer.end_transaction(cx); - }) - })?; } + + let hunks = diff.push_new(&new_text); + hunks_tx.send((hunks, new_text)).await?; } + + if let Some(hunk) = diff.finish() { + hunks_tx.send((vec![hunk], String::new())).await?; + } + + anyhow::Ok(()) + }); + + while let Some((hunks, new_text)) = hunks_rx.next().await { + editor.update(&mut cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + buffer.start_transaction(cx); + let mut new_text_ix = 0; + for hunk in hunks { + match hunk { + crate::diff::Hunk::Insert { len } => { + let text = &new_text[new_text_ix..new_text_ix + len]; + let edit_start = snapshot.anchor_after(edit_start); + buffer.edit([(edit_start..edit_start, text)], None, cx); + new_text_ix += len; + } + crate::diff::Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + buffer.edit([(edit_range, "")], None, cx); + edit_start = edit_end; + } + crate::diff::Hunk::Keep { len } => { + edit_start += len; + new_text_ix += len; + } + } + } + buffer.end_transaction(cx); + }) + })?; } - editor.update(&mut cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - if let Some(transaction) = transaction.take() { - 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); - }) - })?; - + diff.await?; anyhow::Ok(()) } .log_err() @@ -285,75 +201,3 @@ impl RefactoringModal { } } } -fn words(text: &str) -> impl Iterator, &str)> { - let mut word_start_ix = None; - let mut chars = text.char_indices(); - iter::from_fn(move || { - while let Some((ix, ch)) = chars.next() { - if let Some(start_ix) = word_start_ix { - if !ch.is_alphanumeric() { - let word = &text[start_ix..ix]; - word_start_ix.take(); - return Some((start_ix..ix, word)); - } - } else { - if ch.is_alphanumeric() { - word_start_ix = Some(ix); - } - } - } - None - }) -} - -fn streaming_diff<'a>(old_text: &'a str, new_text: &'a str) -> Vec> { - let changes = TextDiff::configure() - .algorithm(similar::Algorithm::Patience) - .diff_words(old_text, new_text); - let mut changes = changes.iter_all_changes().peekable(); - - let mut result = vec![]; - - loop { - let mut deletions = vec![]; - let mut insertions = vec![]; - - while changes - .peek() - .map_or(false, |change| change.tag() == ChangeTag::Delete) - { - deletions.push(changes.next().unwrap()); - } - - while changes - .peek() - .map_or(false, |change| change.tag() == ChangeTag::Insert) - { - insertions.push(changes.next().unwrap()); - } - - if !deletions.is_empty() && !insertions.is_empty() { - result.append(&mut insertions); - result.append(&mut deletions); - } else { - result.append(&mut deletions); - result.append(&mut insertions); - } - - if let Some(change) = changes.next() { - result.push(change); - } else { - break; - } - } - - // Remove all non-inserts at the end. - while result - .last() - .map_or(false, |change| change.tag() != ChangeTag::Insert) - { - result.pop(); - } - - result -} From 3a511db5c985c0b7bb129f9b15347a4a93dd95ca Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Aug 2023 18:41:22 +0200 Subject: [PATCH 017/114] :art: --- crates/ai/src/diff.rs | 85 +++++++++++++++++++++++---------------- crates/ai/src/refactor.rs | 12 ++---- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index 5e73c94ff8..1b5b4cbd20 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -65,7 +65,7 @@ impl Debug for Matrix { #[derive(Debug)] pub enum Hunk { - Insert { len: usize }, + Insert { text: String }, Remove { len: usize }, Keep { len: usize }, } @@ -75,37 +75,42 @@ pub struct Diff { new: String, scores: Matrix, old_text_ix: usize, + new_text_ix: usize, } impl Diff { + const INSERTION_SCORE: isize = -1; + const DELETION_SCORE: isize = -4; + const EQUALITY_SCORE: isize = 5; + pub fn new(old: String) -> Self { let mut scores = Matrix::new(); scores.resize(old.len() + 1, 1); for i in 0..=old.len() { - scores.set(i, 0, -(i as isize)); + scores.set(i, 0, i as isize * Self::DELETION_SCORE); } Self { old, new: String::new(), scores, old_text_ix: 0, + new_text_ix: 0, } } pub fn push_new(&mut self, text: &str) -> Vec { - let new_text_ix = self.new.len(); self.new.push_str(text); self.scores.resize(self.old.len() + 1, self.new.len() + 1); - for j in new_text_ix + 1..=self.new.len() { - self.scores.set(0, j, -(j as isize)); + for j in self.new_text_ix + 1..=self.new.len() { + self.scores.set(0, j, j as isize * Self::INSERTION_SCORE); for i in 1..=self.old.len() { - let insertion_score = self.scores.get(i, j - 1) - 1; - let deletion_score = self.scores.get(i - 1, j) - 10; + 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.as_bytes()[i - 1] == self.new.as_bytes()[j - 1] { - self.scores.get(i - 1, j - 1) + 5 + self.scores.get(i - 1, j - 1) + Self::EQUALITY_SCORE } else { - self.scores.get(i - 1, j - 1) - 20 + isize::MIN }; let score = insertion_score.max(deletion_score).max(equality_score); self.scores.set(i, j, score); @@ -114,19 +119,30 @@ impl Diff { let mut max_score = isize::MIN; let mut best_row = self.old_text_ix; + let mut best_col = self.new_text_ix; for i in self.old_text_ix..=self.old.len() { - let score = self.scores.get(i, self.new.len()); - if score > max_score { - max_score = score; - best_row = i; + for j in self.new_text_ix..=self.new.len() { + let score = self.scores.get(i, j); + if score > max_score { + max_score = score; + best_row = i; + best_col = j; + } } } + let hunks = self.backtrack(best_row, best_col); + self.old_text_ix = best_row; + self.new_text_ix = best_col; + hunks + } + + fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { let mut hunks = Vec::new(); - let mut i = best_row; - let mut j = self.new.len(); - while (i, j) != (self.old_text_ix, new_text_ix) { - let insertion_score = if j > new_text_ix { + 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 @@ -136,8 +152,12 @@ impl Diff { } else { None }; - let equality_score = if i > self.old_text_ix && j > new_text_ix { - Some((i - 1, j - 1)) + let equality_score = if i > self.old_text_ix && j > self.new_text_ix { + if self.old.as_bytes()[i - 1] == self.new.as_bytes()[j - 1] { + Some((i - 1, j - 1)) + } else { + None + } } else { None }; @@ -149,10 +169,12 @@ impl Diff { .unwrap(); if prev_i == i && prev_j == j - 1 { - if let Some(Hunk::Insert { len }) = hunks.last_mut() { - *len += 1; + if let Some(Hunk::Insert { text }) = hunks.last_mut() { + text.insert_str(0, &self.new[prev_j..j]); } else { - hunks.push(Hunk::Insert { len: 1 }) + hunks.push(Hunk::Insert { + text: self.new[prev_j..j].to_string(), + }) } } else if prev_i == i - 1 && prev_j == j { if let Some(Hunk::Remove { len }) = hunks.last_mut() { @@ -171,19 +193,12 @@ impl Diff { i = prev_i; j = prev_j; } - self.old_text_ix = best_row; hunks.reverse(); hunks } - pub fn finish(self) -> Option { - if self.old_text_ix < self.old.len() { - Some(Hunk::Remove { - len: self.old.len() - self.old_text_ix, - }) - } else { - None - } + pub fn finish(self) -> Vec { + self.backtrack(self.old.len(), self.new.len()) } } @@ -194,9 +209,9 @@ mod tests { #[test] fn test_diff() { let mut diff = Diff::new("hello world".to_string()); - diff.push_new("hello"); - diff.push_new(" ciaone"); - diff.push_new(" world"); - diff.finish(); + dbg!(diff.push_new("hello")); + dbg!(diff.push_new(" ciaone")); + // dbg!(diff.push_new(" world")); + dbg!(diff.finish()); } } diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index dcec04deef..87f7495fcf 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -47,7 +47,7 @@ impl RefactoringAssistant { RequestMessage { role: Role::User, content: format!( - "Given the following {language_name} snippet:\n{selected_text}\n{prompt}. Avoid making remarks and reply only with the new code. Preserve indentation." + "Given the following {language_name} snippet:\n{selected_text}\n{prompt}. Never make remarks and reply only with the new code. Never change the leading whitespace on each line." ), }], stream: true, @@ -81,9 +81,7 @@ impl RefactoringAssistant { hunks_tx.send((hunks, new_text)).await?; } - if let Some(hunk) = diff.finish() { - hunks_tx.send((vec![hunk], String::new())).await?; - } + hunks_tx.send((diff.finish(), String::new())).await?; anyhow::Ok(()) }); @@ -92,14 +90,11 @@ impl RefactoringAssistant { editor.update(&mut cx, |editor, cx| { editor.buffer().update(cx, |buffer, cx| { buffer.start_transaction(cx); - let mut new_text_ix = 0; for hunk in hunks { match hunk { - crate::diff::Hunk::Insert { len } => { - let text = &new_text[new_text_ix..new_text_ix + len]; + crate::diff::Hunk::Insert { text } => { let edit_start = snapshot.anchor_after(edit_start); buffer.edit([(edit_start..edit_start, text)], None, cx); - new_text_ix += len; } crate::diff::Hunk::Remove { len } => { let edit_end = edit_start + len; @@ -110,7 +105,6 @@ impl RefactoringAssistant { } crate::diff::Hunk::Keep { len } => { edit_start += len; - new_text_ix += len; } } } From affb73d651bff6e21116da894ff1c42708ba6ba3 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 22 Aug 2023 23:36:04 -0400 Subject: [PATCH 018/114] Only generate workspace/configuration for relevant adapter --- crates/language/src/language.rs | 46 ++---------------- crates/project/src/project.rs | 64 +++++++++++++------------- crates/zed/src/languages/json.rs | 45 +++++++++--------- crates/zed/src/languages/tailwind.rs | 29 ++++++------ crates/zed/src/languages/typescript.rs | 22 ++++----- crates/zed/src/languages/yaml.rs | 23 +++++---- 6 files changed, 95 insertions(+), 134 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 82245d67ca..58732355a5 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -46,7 +46,7 @@ use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; use util::{http::HttpClient, paths::PathExt}; -use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; +use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] use futures::channel::mpsc; @@ -175,10 +175,7 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration( - &self, - cx: &mut AppContext, - ) -> Option> { + pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { self.adapter.workspace_configuration(cx) } @@ -287,8 +284,8 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> Option> { - None + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + futures::future::ready(serde_json::json!({})).boxed() } fn code_action_kinds(&self) -> Option> { @@ -685,41 +682,6 @@ impl LanguageRegistry { result } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> Task { - let lsp_adapters = { - let state = self.state.read(); - state - .available_languages - .iter() - .filter(|l| !l.loaded) - .flat_map(|l| l.lsp_adapters.clone()) - .chain( - state - .languages - .iter() - .flat_map(|language| &language.adapters) - .map(|adapter| adapter.adapter.clone()), - ) - .collect::>() - }; - - let mut language_configs = Vec::new(); - for adapter in &lsp_adapters { - if let Some(language_config) = adapter.workspace_configuration(cx) { - language_configs.push(language_config); - } - } - - cx.background().spawn(async move { - let mut config = serde_json::json!({}); - let language_configs = futures::future::join_all(language_configs).await; - for language_config in language_configs { - merge_json_value_into(language_config, &mut config); - } - config - }) - } - pub fn add(&self, language: Arc) { self.state.write().add(language); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4b3c80c08a..63b6786d8c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -603,7 +603,7 @@ impl Project { cx.observe_global::(Self::on_settings_changed) ], _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), - _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), + _maintain_workspace_config: Self::maintain_workspace_config(cx), active_entry: None, languages, client, @@ -673,7 +673,7 @@ impl Project { collaborators: Default::default(), join_project_response_message_id: response.message_id, _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), - _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), + _maintain_workspace_config: Self::maintain_workspace_config(cx), languages, user_store: user_store.clone(), fs, @@ -2441,35 +2441,42 @@ impl Project { }) } - fn maintain_workspace_config( - languages: Arc, - cx: &mut ModelContext, - ) -> Task<()> { + fn maintain_workspace_config(cx: &mut ModelContext) -> Task<()> { let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); let settings_observation = cx.observe_global::(move |_, _| { *settings_changed_tx.borrow_mut() = (); }); + cx.spawn_weak(|this, mut cx| async move { while let Some(_) = settings_changed_rx.next().await { - let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; - if let Some(this) = this.upgrade(&cx) { - this.read_with(&cx, |this, _| { - for server_state in this.language_servers.values() { - if let LanguageServerState::Running { server, .. } = server_state { - server - .notify::( - lsp::DidChangeConfigurationParams { - settings: workspace_config.clone(), - }, - ) - .ok(); - } - } - }) - } else { + let Some(this) = this.upgrade(&cx) else { break; + }; + + let servers: Vec<_> = this.read_with(&cx, |this, _| { + this.language_servers + .values() + .filter_map(|state| match state { + LanguageServerState::Starting(_) => None, + LanguageServerState::Running { + adapter, server, .. + } => Some((adapter.clone(), server.clone())), + }) + .collect() + }); + + for (adapter, server) in servers { + let workspace_config = + cx.update(|cx| adapter.workspace_configuration(cx)).await; + server + .notify::( + lsp::DidChangeConfigurationParams { + settings: workspace_config.clone(), + }, + ) + .ok(); } } @@ -2584,7 +2591,6 @@ impl Project { let state = LanguageServerState::Starting({ let adapter = adapter.clone(); let server_name = adapter.name.0.clone(); - let languages = self.languages.clone(); let language = language.clone(); let key = key.clone(); @@ -2594,7 +2600,6 @@ impl Project { initialization_options, pending_server, adapter.clone(), - languages, language.clone(), server_id, key, @@ -2698,7 +2703,6 @@ impl Project { initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, - languages: Arc, language: Arc, server_id: LanguageServerId, key: (WorktreeId, LanguageServerName), @@ -2709,7 +2713,6 @@ impl Project { initialization_options, pending_server, adapter.clone(), - languages, server_id, cx, ); @@ -2742,11 +2745,10 @@ impl Project { initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, - languages: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result>> { - let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; + let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await; let language_server = match pending_server.task.await? { Some(server) => server, None => return Ok(None), @@ -2788,12 +2790,12 @@ impl Project { language_server .on_request::({ - let languages = languages.clone(); + let adapter = adapter.clone(); move |params, mut cx| { - let languages = languages.clone(); + let adapter = adapter.clone(); async move { let workspace_config = - cx.update(|cx| languages.workspace_configuration(cx)).await; + cx.update(|cx| adapter.workspace_configuration(cx)).await; Ok(params .items .into_iter() diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index b7e4ab4ba7..225cea0e92 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -102,7 +102,7 @@ impl LspAdapter for JsonLspAdapter { fn workspace_configuration( &self, cx: &mut AppContext, - ) -> Option> { + ) -> BoxFuture<'static, serde_json::Value> { let action_names = cx.all_action_names().collect::>(); let staff_mode = cx.default_global::().0; let language_names = &self.languages.language_names(); @@ -113,29 +113,28 @@ impl LspAdapter for JsonLspAdapter { }, cx, ); - Some( - future::ready(serde_json::json!({ - "json": { - "format": { - "enable": true, + + future::ready(serde_json::json!({ + "json": { + "format": { + "enable": true, + }, + "schemas": [ + { + "fileMatch": [ + schema_file_match(&paths::SETTINGS), + &*paths::LOCAL_SETTINGS_RELATIVE_PATH, + ], + "schema": settings_schema, }, - "schemas": [ - { - "fileMatch": [ - schema_file_match(&paths::SETTINGS), - &*paths::LOCAL_SETTINGS_RELATIVE_PATH, - ], - "schema": settings_schema, - }, - { - "fileMatch": [schema_file_match(&paths::KEYMAP)], - "schema": KeymapFile::generate_json_schema(&action_names), - } - ] - } - })) - .boxed(), - ) + { + "fileMatch": [schema_file_match(&paths::KEYMAP)], + "schema": KeymapFile::generate_json_schema(&action_names), + } + ] + } + })) + .boxed() } async fn language_ids(&self) -> HashMap { diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 0290bf3334..9a32f69e43 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -103,23 +103,24 @@ impl LspAdapter for TailwindLspAdapter { })) } - fn workspace_configuration(&self, _: &mut AppContext) -> Option> { - Some( - future::ready(json!({ - "tailwindCSS": { - "emmetCompletions": true, - } - })) - .boxed(), - ) + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + future::ready(json!({ + "tailwindCSS": { + "emmetCompletions": true, + } + })) + .boxed() } async fn language_ids(&self) -> HashMap { - HashMap::from([ - ("HTML".to_string(), "html".to_string()), - ("CSS".to_string(), "css".to_string()), - ("JavaScript".to_string(), "javascript".to_string()), - ]) + HashMap::from_iter( + [ + ("HTML".to_string(), "html".to_string()), + ("CSS".to_string(), "css".to_string()), + ("JavaScript".to_string(), "javascript".to_string()), + ] + .into_iter(), + ) } } diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 0a47d365b5..b7e4438e1f 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -202,18 +202,16 @@ impl EsLintLspAdapter { #[async_trait] impl LspAdapter for EsLintLspAdapter { - fn workspace_configuration(&self, _: &mut AppContext) -> Option> { - Some( - future::ready(json!({ - "": { - "validate": "on", - "rulesCustomizations": [], - "run": "onType", - "nodePath": null, - } - })) - .boxed(), - ) + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + future::ready(json!({ + "": { + "validate": "on", + "rulesCustomizations": [], + "run": "onType", + "nodePath": null, + } + })) + .boxed() } async fn name(&self) -> LanguageServerName { diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index b57c6f5699..48d7a3cf87 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -86,21 +86,20 @@ impl LspAdapter for YamlLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &self.node).await } - fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { + fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { let tab_size = all_language_settings(None, cx) .language(Some("YAML")) .tab_size; - Some( - future::ready(serde_json::json!({ - "yaml": { - "keyOrdering": false - }, - "[yaml]": { - "editor.tabSize": tab_size, - } - })) - .boxed(), - ) + + future::ready(serde_json::json!({ + "yaml": { + "keyOrdering": false + }, + "[yaml]": { + "editor.tabSize": tab_size, + } + })) + .boxed() } } From 68408f38382163291b23e6aec2985f858d2a381c Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 22 Aug 2023 23:50:40 -0400 Subject: [PATCH 019/114] Add VSCode CSS language server & add Tailwind to .css files --- crates/zed/src/languages.rs | 10 ++- crates/zed/src/languages/css.rs | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 crates/zed/src/languages/css.rs diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 2cdd540e8a..42b62cdfa9 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -6,6 +6,7 @@ use std::{borrow::Cow, str, sync::Arc}; use util::asset_str; mod c; +mod css; mod elixir; mod go; mod html; @@ -52,7 +53,14 @@ pub fn init(languages: Arc, node_runtime: Arc) { tree_sitter_cpp::language(), vec![Arc::new(c::CLspAdapter)], ); - language("css", tree_sitter_css::language(), vec![]); + language( + "css", + tree_sitter_css::language(), + vec![ + Arc::new(css::CssLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); language( "elixir", tree_sitter_elixir::language(), diff --git a/crates/zed/src/languages/css.rs b/crates/zed/src/languages/css.rs new file mode 100644 index 0000000000..51db8b8ab8 --- /dev/null +++ b/crates/zed/src/languages/css.rs @@ -0,0 +1,126 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::json; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = + "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct CssLspAdapter { + node: Arc, +} + +impl CssLspAdapter { + pub fn new(node: Arc) -> Self { + CssLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for CssLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-css-language-server".into()) + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("vscode-langservers-extracted") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + [("vscode-langservers-extracted", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} From a394aaa52461c1d581badc8648dd558b872b811f Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 23 Aug 2023 00:11:15 -0400 Subject: [PATCH 020/114] Add Tailwind server to JS/TS --- crates/zed/src/languages.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 42b62cdfa9..8aaa11e1cd 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -112,6 +112,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( @@ -120,6 +121,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( From a9871a7a7051c3c4fec4cc1da03f0752e824dc4c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 09:09:01 +0200 Subject: [PATCH 021/114] Add randomized tests for incremental diff --- Cargo.lock | 4 +++ crates/ai/Cargo.toml | 5 +++ crates/ai/src/ai.rs | 8 +++++ crates/ai/src/diff.rs | 69 ++++++++++++++++++++++++++++++++++----- crates/ai/src/refactor.rs | 7 ++-- 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af16a88596..3283d32a94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,15 +102,19 @@ dependencies = [ "anyhow", "chrono", "collections", + "ctor", "editor", + "env_logger 0.9.3", "fs", "futures 0.3.28", "gpui", "indoc", "isahc", "language", + "log", "menu", "project", + "rand 0.8.5", "regex", "schemars", "search", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 5ef371e342..db8772bcb1 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -37,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 diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 52f31d2f56..bad153879f 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -283,3 +283,11 @@ pub async fn stream_completion( } } } + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index 1b5b4cbd20..355748ea3d 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -204,14 +204,67 @@ impl Diff { #[cfg(test)] mod tests { - use super::*; + use std::env; - #[test] - fn test_diff() { - let mut diff = Diff::new("hello world".to_string()); - dbg!(diff.push_new("hello")); - dbg!(diff.push_new(" ciaone")); - // dbg!(diff.push_new(" world")); - dbg!(diff.finish()); + 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::(); + log::info!("old text: {:?}", old); + + let mut diff = Diff::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::(); + 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); } } diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 87f7495fcf..d68d8ce4ed 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -78,15 +78,14 @@ impl RefactoringAssistant { } let hunks = diff.push_new(&new_text); - hunks_tx.send((hunks, new_text)).await?; + hunks_tx.send(hunks).await?; } - - hunks_tx.send((diff.finish(), String::new())).await?; + hunks_tx.send(diff.finish()).await?; anyhow::Ok(()) }); - while let Some((hunks, new_text)) = hunks_rx.next().await { + while let Some(hunks) = hunks_rx.next().await { editor.update(&mut cx, |editor, cx| { editor.buffer().update(cx, |buffer, cx| { buffer.start_transaction(cx); From c2935056e8cf79ae6ce1479a3cb8eaee60b9d174 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 09:32:38 +0200 Subject: [PATCH 022/114] Support multi-byte characters in diff --- crates/ai/src/diff.rs | 57 ++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index 355748ea3d..c68f9b3d5d 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -1,6 +1,7 @@ use std::{ cmp, fmt::{self, Debug}, + ops::Range, }; use collections::BinaryHeap; @@ -71,8 +72,8 @@ pub enum Hunk { } pub struct Diff { - old: String, - new: String, + old: Vec, + new: Vec, scores: Matrix, old_text_ix: usize, new_text_ix: usize, @@ -84,6 +85,7 @@ impl Diff { const EQUALITY_SCORE: isize = 5; pub fn new(old: String) -> Self { + let old = old.chars().collect::>(); let mut scores = Matrix::new(); scores.resize(old.len() + 1, 1); for i in 0..=old.len() { @@ -91,7 +93,7 @@ impl Diff { } Self { old, - new: String::new(), + new: Vec::new(), scores, old_text_ix: 0, new_text_ix: 0, @@ -99,7 +101,7 @@ impl Diff { } pub fn push_new(&mut self, text: &str) -> Vec { - self.new.push_str(text); + 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() { @@ -107,7 +109,7 @@ impl Diff { 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.as_bytes()[i - 1] == self.new.as_bytes()[j - 1] { + let equality_score = if self.old[i - 1] == self.new[j - 1] { self.scores.get(i - 1, j - 1) + Self::EQUALITY_SCORE } else { isize::MIN @@ -138,6 +140,7 @@ impl Diff { } fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { + let mut pending_insert: Option> = None; let mut hunks = Vec::new(); let mut i = old_text_ix; let mut j = new_text_ix; @@ -153,7 +156,7 @@ impl Diff { None }; let equality_score = if i > self.old_text_ix && j > self.new_text_ix { - if self.old.as_bytes()[i - 1] == self.new.as_bytes()[j - 1] { + if self.old[i - 1] == self.new[j - 1] { Some((i - 1, j - 1)) } else { None @@ -169,30 +172,44 @@ impl Diff { .unwrap(); if prev_i == i && prev_j == j - 1 { - if let Some(Hunk::Insert { text }) = hunks.last_mut() { - text.insert_str(0, &self.new[prev_j..j]); + if let Some(pending_insert) = pending_insert.as_mut() { + pending_insert.start = prev_j; } else { - hunks.push(Hunk::Insert { - text: self.new[prev_j..j].to_string(), - }) - } - } else if prev_i == i - 1 && prev_j == j { - if let Some(Hunk::Remove { len }) = hunks.last_mut() { - *len += 1; - } else { - hunks.push(Hunk::Remove { len: 1 }) + pending_insert = Some(prev_j..j); } } else { - if let Some(Hunk::Keep { len }) = hunks.last_mut() { - *len += 1; + 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 { - hunks.push(Hunk::Keep { len: 1 }) + 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 } From a93583065b03738d5ededcc6ea919a6ba491a354 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 09:58:41 +0200 Subject: [PATCH 023/114] Delete unused imports --- crates/ai/src/diff.rs | 3 --- crates/ai/src/refactor.rs | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index c68f9b3d5d..b6e25e0624 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -1,11 +1,8 @@ use std::{ - cmp, fmt::{self, Debug}, ops::Range, }; -use collections::BinaryHeap; - struct Matrix { cells: Vec, rows: usize, diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index d68d8ce4ed..245e59a464 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -7,8 +7,7 @@ use gpui::{ WeakViewHandle, }; use menu::Confirm; -use similar::{Change, ChangeTag, TextDiff}; -use std::{env, iter, ops::Range, sync::Arc}; +use std::{env, sync::Arc}; use util::TryFutureExt; use workspace::{Modal, Workspace}; From a2671a29a0a773e30af0a259bb3c1e590c71cecc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 10:28:43 +0200 Subject: [PATCH 024/114] Highlight text when the diff is the same --- crates/ai/src/refactor.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 245e59a464..7b2f5a248a 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -86,6 +86,8 @@ impl RefactoringAssistant { while let Some(hunks) = hunks_rx.next().await { editor.update(&mut cx, |editor, cx| { + let mut highlights = Vec::new(); + editor.buffer().update(cx, |buffer, cx| { buffer.start_transaction(cx); for hunk in hunks { @@ -102,16 +104,33 @@ impl RefactoringAssistant { edit_start = edit_end; } crate::diff::Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + highlights.push(edit_range); edit_start += len; } } } buffer.end_transaction(cx); - }) + }); + + editor.highlight_text::( + highlights, + gpui::fonts::HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); })?; } diff.await?; + editor.update(&mut cx, |editor, cx| { + editor.clear_text_highlights::(cx); + })?; + anyhow::Ok(()) } .log_err() @@ -172,7 +191,7 @@ impl RefactoringModal { Some(Arc::new(|theme| theme.search.editor.input.clone())), cx, ); - editor.set_text("Replace with match statement.", cx); + editor.set_text("Replace with if statement.", cx); editor }); cx.add_view(|_| RefactoringModal { From aa6d6582fd5ea323a970961b3c16cfb9e5a60c67 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 12:49:55 +0200 Subject: [PATCH 025/114] Add basic styling --- crates/ai/src/refactor.rs | 53 +++++++++++++++++++++--------- crates/theme/src/theme.rs | 10 ++++++ styles/src/style_tree/assistant.ts | 16 +++++++++ 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 7b2f5a248a..58cd360186 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -3,10 +3,10 @@ use collections::HashMap; use editor::{Editor, ToOffset}; use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ - actions, elements::*, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + actions, elements::*, platform::MouseButton, AnyViewHandle, AppContext, Entity, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; -use menu::Confirm; +use menu::{Cancel, Confirm}; use std::{env, sync::Arc}; use util::TryFutureExt; use workspace::{Modal, Workspace}; @@ -17,6 +17,7 @@ pub fn init(cx: &mut AppContext) { cx.set_global(RefactoringAssistant::new()); cx.add_action(RefactoringModal::deploy); cx.add_action(RefactoringModal::confirm); + cx.add_action(RefactoringModal::cancel); } pub struct RefactoringAssistant { @@ -139,14 +140,18 @@ impl RefactoringAssistant { } } +enum Event { + Dismissed, +} + struct RefactoringModal { - editor: WeakViewHandle, + active_editor: WeakViewHandle, prompt_editor: ViewHandle, has_focus: bool, } impl Entity for RefactoringModal { - type Event = (); + type Event = Event; } impl View for RefactoringModal { @@ -155,11 +160,24 @@ impl View for RefactoringModal { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.prompt_editor, cx).into_any() + let theme = theme::current(cx); + + ChildView::new(&self.prompt_editor, cx) + .constrained() + .with_width(theme.assistant.modal.width) + .contained() + .with_style(theme.assistant.modal.container) + .mouse::(0) + .on_click_out(MouseButton::Left, |_, _, cx| cx.emit(Event::Dismissed)) + .on_click_out(MouseButton::Right, |_, _, cx| cx.emit(Event::Dismissed)) + .aligned() + .right() + .into_any() } - fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { self.has_focus = true; + cx.focus(&self.prompt_editor); } fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { @@ -173,29 +191,29 @@ impl Modal for RefactoringModal { } fn dismiss_on_event(event: &Self::Event) -> bool { - // TODO - false + matches!(event, Self::Event::Dismissed) } } impl RefactoringModal { fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { - if let Some(editor) = workspace + if let Some(active_editor) = workspace .active_item(cx) .and_then(|item| Some(item.downcast::()?.downgrade())) { workspace.toggle_modal(cx, |_, cx| { let prompt_editor = cx.add_view(|cx| { let mut editor = Editor::auto_height( - 4, - Some(Arc::new(|theme| theme.search.editor.input.clone())), + theme::current(cx).assistant.modal.editor_max_lines, + Some(Arc::new(|theme| theme.assistant.modal.editor.clone())), cx, ); - editor.set_text("Replace with if statement.", cx); + editor + .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor }); cx.add_view(|_| RefactoringModal { - editor, + active_editor, prompt_editor, has_focus: false, }) @@ -203,12 +221,17 @@ impl RefactoringModal { } } + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(editor) = self.editor.upgrade(cx) { + if let Some(editor) = self.active_editor.upgrade(cx) { let prompt = self.prompt_editor.read(cx).text(cx); cx.update_global(|assistant: &mut RefactoringAssistant, cx| { assistant.refactor(&editor, &prompt, cx); }); + cx.emit(Event::Dismissed); } } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 80e823632a..a42a893241 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1124,6 +1124,16 @@ pub struct AssistantStyle { pub api_key_editor: FieldEditor, pub api_key_prompt: ContainedText, pub saved_conversation: SavedConversation, + pub modal: ModalAssistantStyle, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ModalAssistantStyle { + #[serde(flatten)] + pub container: ContainerStyle, + pub width: f32, + pub editor_max_lines: usize, + pub editor: FieldEditor, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index cfc1f8d813..88efabee1e 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -59,6 +59,22 @@ export default function assistant(): any { background: background(theme.highest), padding: { left: 12 }, }, + modal: { + background: background(theme.lowest), + border: border(theme.lowest), + shadow: theme.modal_shadow, + corner_radius: 12, + padding: { left: 12, right: 0, top: 12, bottom: 12 }, + margin: { right: 12 }, + width: 500, + editor_max_lines: 6, + editor: { + background: background(theme.lowest), + text: text(theme.lowest, "mono", "on"), + placeholder_text: text(theme.lowest, "sans", "on", "disabled"), + selection: theme.players[0], + } + }, message_header: { margin: { bottom: 4, top: 4 }, background: background(theme.highest), From 2e1a4b25912f158121303a7779b65410cabcea6c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 13:26:30 +0200 Subject: [PATCH 026/114] Adjust scoring --- crates/ai/src/diff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index b6e25e0624..0f4f328602 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -79,7 +79,7 @@ pub struct Diff { impl Diff { const INSERTION_SCORE: isize = -1; const DELETION_SCORE: isize = -4; - const EQUALITY_SCORE: isize = 5; + const EQUALITY_SCORE: isize = 15; pub fn new(old: String) -> Self { let old = old.chars().collect::>(); From d3238441ce56c00810a7b651de5d511255a161c1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 16:13:37 +0200 Subject: [PATCH 027/114] :art: --- crates/ai/src/refactor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 58cd360186..aab056fd32 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -1,4 +1,4 @@ -use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; +use crate::{diff::Diff, stream_completion, OpenAIRequest, RequestMessage, Role}; use collections::HashMap; use editor::{Editor, ToOffset}; use futures::{channel::mpsc, SinkExt, StreamExt}; @@ -64,7 +64,7 @@ impl RefactoringAssistant { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { let mut messages = response.await?.ready_chunks(4); - let mut diff = crate::diff::Diff::new(selected_text); + let mut diff = Diff::new(selected_text); while let Some(messages) = messages.next().await { let mut new_text = String::new(); From e4f49746e1b389310ee50e48329095d44056e9c0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 17:09:15 +0200 Subject: [PATCH 028/114] Group modal assistant edits into the same transaction Co-Authored-By: Kyle Caverly --- crates/ai/src/refactor.rs | 30 ++++++++++---- crates/editor/src/multi_buffer.rs | 65 +++++++++++++++++++++++++++++++ crates/language/src/buffer.rs | 8 ++++ crates/text/src/text.rs | 38 ++++++++++++++++-- 4 files changed, 131 insertions(+), 10 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index aab056fd32..1eb54d9373 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -85,35 +85,51 @@ impl RefactoringAssistant { anyhow::Ok(()) }); + let mut last_transaction = None; while let Some(hunks) = hunks_rx.next().await { editor.update(&mut cx, |editor, cx| { let mut highlights = Vec::new(); editor.buffer().update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx); + buffer.start_transaction(cx); - for hunk in hunks { - match hunk { + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { crate::diff::Hunk::Insert { text } => { let edit_start = snapshot.anchor_after(edit_start); - buffer.edit([(edit_start..edit_start, text)], None, cx); + Some((edit_start..edit_start, text)) } crate::diff::Hunk::Remove { len } => { let edit_end = edit_start + len; let edit_range = snapshot.anchor_after(edit_start) ..snapshot.anchor_before(edit_end); - buffer.edit([(edit_range, "")], None, cx); edit_start = edit_end; + Some((edit_range, String::new())) } crate::diff::Hunk::Keep { len } => { let edit_end = edit_start + len; let edit_range = snapshot.anchor_after(edit_start) ..snapshot.anchor_before(edit_end); - highlights.push(edit_range); edit_start += len; + highlights.push(edit_range); + None } + }), + None, + cx, + ); + if let Some(transaction) = buffer.end_transaction(cx) { + if let Some(last_transaction) = last_transaction { + buffer.merge_transaction_into( + last_transaction, + transaction, + cx, + ); } + last_transaction = Some(transaction); + buffer.finalize_last_transaction(cx); } - buffer.end_transaction(cx); }); editor.highlight_text::( @@ -199,7 +215,7 @@ impl RefactoringModal { fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { if let Some(active_editor) = workspace .active_item(cx) - .and_then(|item| Some(item.downcast::()?.downgrade())) + .and_then(|item| Some(item.act_as::(cx)?.downgrade())) { workspace.toggle_modal(cx, |_, cx| { let prompt_editor = cx.add_view(|cx| { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8417c411f2..28b31ef097 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -615,6 +615,42 @@ impl MultiBuffer { } } + pub fn merge_transaction_into( + &mut self, + transaction: TransactionId, + destination: TransactionId, + cx: &mut ModelContext, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.merge_transaction_into(transaction, destination) + }); + } else { + if let Some(transaction) = self.history.remove_transaction(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_transaction_into( + 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.history.finalize_last_transaction(); for BufferState { buffer, .. } in self.buffers.borrow().values() { @@ -3333,6 +3369,35 @@ impl History { } } + fn remove_transaction(&mut self, transaction_id: TransactionId) -> Option { + 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() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0b10432a9f..69668a97c6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1294,6 +1294,14 @@ impl Buffer { self.text.forget_transaction(transaction_id); } + pub fn merge_transaction_into( + &mut self, + transaction: TransactionId, + destination: TransactionId, + ) { + self.text.merge_transaction_into(transaction, destination); + } + pub fn wait_for_edits( &mut self, edit_ids: impl IntoIterator, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 7c94f25e1e..e47e20da0d 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -278,20 +278,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 { 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_transaction_into(&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); + } } } @@ -1202,6 +1225,15 @@ impl Buffer { self.history.forget(transaction_id); } + pub fn merge_transaction_into( + &mut self, + transaction: TransactionId, + destination: TransactionId, + ) { + self.history + .merge_transaction_into(transaction, destination); + } + pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { if let Some(entry) = self.history.pop_redo() { let transaction = entry.transaction.clone(); From a69461dba2e8ac1dc78306d5eb9191514c7e787c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 17:18:36 +0200 Subject: [PATCH 029/114] Don't score whitespace matches Co-Authored-By: Kyle Caverly --- crates/ai/src/diff.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index 0f4f328602..3ba0d005e7 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -107,7 +107,11 @@ impl Diff { 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] { - self.scores.get(i - 1, j - 1) + Self::EQUALITY_SCORE + if self.old[i - 1] == ' ' { + self.scores.get(i - 1, j - 1) + } else { + self.scores.get(i - 1, j - 1) + Self::EQUALITY_SCORE + } } else { isize::MIN }; From 301a12923f5d548bdc9d4e5b60acff9cab4e0d3f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 18:20:42 +0200 Subject: [PATCH 030/114] Merge transactions into the original assistant transaction Co-Authored-By: Nathan Sobo Co-Authored-By: Kyle Caverly --- crates/ai/src/refactor.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 1eb54d9373..f668f8d5ac 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -85,7 +85,7 @@ impl RefactoringAssistant { anyhow::Ok(()) }); - let mut last_transaction = None; + let mut first_transaction = None; while let Some(hunks) = hunks_rx.next().await { editor.update(&mut cx, |editor, cx| { let mut highlights = Vec::new(); @@ -120,14 +120,15 @@ impl RefactoringAssistant { cx, ); if let Some(transaction) = buffer.end_transaction(cx) { - if let Some(last_transaction) = last_transaction { + if let Some(first_transaction) = first_transaction { buffer.merge_transaction_into( - last_transaction, transaction, + first_transaction, cx, ); + } else { + first_transaction = Some(transaction); } - last_transaction = Some(transaction); buffer.finalize_last_transaction(cx); } }); From f22acb602e9aaf2b9db8685aaec7c44fa3963cd5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Aug 2023 19:21:44 +0200 Subject: [PATCH 031/114] Apply a score boost when consecutive triplets of characters match --- crates/ai/src/diff.rs | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index 3ba0d005e7..378206497b 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -1,3 +1,4 @@ +use collections::HashMap; use std::{ fmt::{self, Debug}, ops::Range, @@ -74,12 +75,13 @@ pub struct Diff { scores: Matrix, old_text_ix: usize, new_text_ix: usize, + equal_runs: HashMap<(usize, usize), u32>, } impl Diff { const INSERTION_SCORE: isize = -1; - const DELETION_SCORE: isize = -4; - const EQUALITY_SCORE: isize = 15; + const DELETION_SCORE: isize = -5; + const EQUALITY_BASE: isize = 2; pub fn new(old: String) -> Self { let old = old.chars().collect::>(); @@ -94,6 +96,7 @@ impl Diff { scores, old_text_ix: 0, new_text_ix: 0, + equal_runs: Default::default(), } } @@ -107,36 +110,38 @@ impl Diff { 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); + if self.old[i - 1] == ' ' { self.scores.get(i - 1, j - 1) } else { - self.scores.get(i - 1, j - 1) + Self::EQUALITY_SCORE + self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.pow(equal_run / 3) } } else { isize::MIN }; + let score = insertion_score.max(deletion_score).max(equality_score); self.scores.set(i, j, score); } } let mut max_score = isize::MIN; - let mut best_row = self.old_text_ix; - let mut best_col = self.new_text_ix; + 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() { - for j in self.new_text_ix..=self.new.len() { - let score = self.scores.get(i, j); - if score > max_score { - max_score = score; - best_row = i; - best_col = j; - } + 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(best_row, best_col); - self.old_text_ix = best_row; - self.new_text_ix = best_col; + 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 } From 985397b55c45b2d1c6985a37453901ca81a8a454 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 09:52:07 +0200 Subject: [PATCH 032/114] :memo: --- crates/ai/src/refactor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index f668f8d5ac..2821a1e845 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -91,6 +91,7 @@ impl RefactoringAssistant { let mut highlights = Vec::new(); editor.buffer().update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. buffer.finalize_last_transaction(cx); buffer.start_transaction(cx); @@ -121,6 +122,7 @@ impl RefactoringAssistant { ); if let Some(transaction) = buffer.end_transaction(cx) { if let Some(first_transaction) = first_transaction { + // Group all assistant edits into the first transaction. buffer.merge_transaction_into( transaction, first_transaction, @@ -128,8 +130,8 @@ impl RefactoringAssistant { ); } else { first_transaction = Some(transaction); + buffer.finalize_last_transaction(cx); } - buffer.finalize_last_transaction(cx); } }); From 481bcbf2046db23919ab9f9d2e2c10ab577c0830 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 12:45:21 +0200 Subject: [PATCH 033/114] Normalize indentation when refactoring --- crates/ai/src/refactor.rs | 84 +++++++++++++++++++++++++++++++++++---- crates/rope/src/rope.rs | 10 +++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 2821a1e845..9b36d760b7 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -1,13 +1,14 @@ use crate::{diff::Diff, stream_completion, OpenAIRequest, RequestMessage, Role}; use collections::HashMap; -use editor::{Editor, ToOffset}; +use editor::{Editor, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ actions, elements::*, platform::MouseButton, AnyViewHandle, AppContext, Entity, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::{Point, Rope}; use menu::{Cancel, Confirm}; -use std::{env, sync::Arc}; +use std::{cmp, env, sync::Arc}; use util::TryFutureExt; use workspace::{Modal, Workspace}; @@ -36,7 +37,48 @@ impl RefactoringAssistant { let selection = editor.read(cx).selections.newest_anchor().clone(); let selected_text = snapshot .text_for_range(selection.start..selection.end) - .collect::(); + .collect::(); + + let mut normalized_selected_text = selected_text.clone(); + let mut base_indentation: Option = None; + let selection_start = selection.start.to_point(&snapshot); + let selection_end = selection.end.to_point(&snapshot); + if selection_start.row < selection_end.row { + for row in selection_start.row..=selection_end.row { + if snapshot.is_line_blank(row) { + continue; + } + + let line_indentation = snapshot.indent_size_for_line(row); + if let Some(base_indentation) = base_indentation.as_mut() { + if line_indentation.len < base_indentation.len { + *base_indentation = line_indentation; + } + } else { + base_indentation = Some(line_indentation); + } + } + } + + if let Some(base_indentation) = base_indentation { + for row in selection_start.row..=selection_end.row { + let selection_row = row - selection_start.row; + let line_start = + normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); + let indentation_len = if row == selection_start.row { + base_indentation.len.saturating_sub(selection_start.column) + } else { + let line_len = normalized_selected_text.line_len(selection_row); + cmp::min(line_len, base_indentation.len) + }; + let indentation_end = cmp::min( + line_start + indentation_len as usize, + normalized_selected_text.len(), + ); + normalized_selected_text.replace(line_start..indentation_end, ""); + } + } + let language_name = snapshot .language_at(selection.start) .map(|language| language.name()); @@ -47,7 +89,7 @@ impl RefactoringAssistant { RequestMessage { role: Role::User, content: format!( - "Given the following {language_name} snippet:\n{selected_text}\n{prompt}. Never make remarks and reply only with the new code. Never change the leading whitespace on each line." + "Given the following {language_name} snippet:\n{normalized_selected_text}\n{prompt}. Never make remarks and reply only with the new code." ), }], stream: true, @@ -64,21 +106,49 @@ impl RefactoringAssistant { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { let mut messages = response.await?.ready_chunks(4); - let mut diff = Diff::new(selected_text); + let mut diff = Diff::new(selected_text.to_string()); + let indentation_len; + let indentation_text; + if let Some(base_indentation) = base_indentation { + indentation_len = base_indentation.len; + indentation_text = match base_indentation.kind { + language::IndentKind::Space => " ", + language::IndentKind::Tab => "\t", + }; + } else { + indentation_len = 0; + indentation_text = ""; + }; + + let mut new_text = + indentation_text.repeat( + indentation_len.saturating_sub(selection_start.column) as usize, + ); while let Some(messages) = messages.next().await { - let mut new_text = String::new(); for message in messages { let mut message = message?; if let Some(choice) = message.choices.pop() { if let Some(text) = choice.delta.content { - new_text.push_str(&text); + let mut lines = text.split('\n'); + if let Some(first_line) = lines.next() { + new_text.push_str(&first_line); + } + + for line in lines { + new_text.push('\n'); + new_text.push_str( + &indentation_text.repeat(indentation_len as usize), + ); + new_text.push_str(line); + } } } } let hunks = diff.push_new(&new_text); hunks_tx.send(hunks).await?; + new_text.clear(); } hunks_tx.send(diff.finish()).await?; diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 2bfb090bb2..9c764c468e 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -384,6 +384,16 @@ impl<'a> From<&'a str> for Rope { } } +impl<'a> FromIterator<&'a str> for Rope { + fn from_iter>(iter: T) -> Self { + let mut rope = Rope::new(); + for chunk in iter { + rope.push(chunk); + } + rope + } +} + impl From for Rope { fn from(text: String) -> Self { Rope::from(text.as_str()) From 9674b038559623cdb0fed1c2e6023327e5b59772 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 12:45:44 +0200 Subject: [PATCH 034/114] Make scoring more precise by using floats when diffing AI refactors --- Cargo.lock | 1 + crates/ai/Cargo.toml | 1 + crates/ai/src/diff.rs | 30 +++++++++++++++++------------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3283d32a94..2a4c6c4f43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,7 @@ dependencies = [ "language", "log", "menu", + "ordered-float", "project", "rand 0.8.5", "regex", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index db8772bcb1..b03405bb93 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -26,6 +26,7 @@ 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 diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index 378206497b..7c5af34ff5 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -1,11 +1,13 @@ use collections::HashMap; +use ordered_float::OrderedFloat; use std::{ + cmp, fmt::{self, Debug}, ops::Range, }; struct Matrix { - cells: Vec, + cells: Vec, rows: usize, cols: usize, } @@ -20,12 +22,12 @@ impl Matrix { } fn resize(&mut self, rows: usize, cols: usize) { - self.cells.resize(rows * cols, 0); + self.cells.resize(rows * cols, 0.); self.rows = rows; self.cols = cols; } - fn get(&self, row: usize, col: usize) -> isize { + fn get(&self, row: usize, col: usize) -> f64 { if row >= self.rows { panic!("row out of bounds") } @@ -36,7 +38,7 @@ impl Matrix { self.cells[col * self.rows + row] } - fn set(&mut self, row: usize, col: usize, value: isize) { + fn set(&mut self, row: usize, col: usize, value: f64) { if row >= self.rows { panic!("row out of bounds") } @@ -79,16 +81,17 @@ pub struct Diff { } impl Diff { - const INSERTION_SCORE: isize = -1; - const DELETION_SCORE: isize = -5; - const EQUALITY_BASE: isize = 2; + const INSERTION_SCORE: f64 = -1.; + const DELETION_SCORE: f64 = -5.; + const EQUALITY_BASE: f64 = 1.618; + const MAX_EQUALITY_EXPONENT: i32 = 32; pub fn new(old: String) -> Self { let old = old.chars().collect::>(); let mut scores = Matrix::new(); scores.resize(old.len() + 1, 1); for i in 0..=old.len() { - scores.set(i, 0, i as isize * Self::DELETION_SCORE); + scores.set(i, 0, i as f64 * Self::DELETION_SCORE); } Self { old, @@ -105,7 +108,7 @@ impl Diff { 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 isize * Self::INSERTION_SCORE); + 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; @@ -117,10 +120,11 @@ impl Diff { if self.old[i - 1] == ' ' { self.scores.get(i - 1, j - 1) } else { - self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.pow(equal_run / 3) + let exponent = cmp::min(equal_run as i32 / 3, Self::MAX_EQUALITY_EXPONENT); + self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) } } else { - isize::MIN + f64::NEG_INFINITY }; let score = insertion_score.max(deletion_score).max(equality_score); @@ -128,7 +132,7 @@ impl Diff { } } - let mut max_score = isize::MIN; + 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() { @@ -173,7 +177,7 @@ impl Diff { let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] .iter() - .max_by_key(|cell| cell.map(|(i, j)| self.scores.get(i, j))) + .max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j)))) .unwrap() .unwrap(); From 71a5964c187475564d5b43a66be9642f1b3c078e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 14:26:42 +0200 Subject: [PATCH 035/114] Rename `merge_transaction_into` to `merge_transactions` --- crates/ai/src/refactor.rs | 2 +- crates/editor/src/multi_buffer.rs | 6 +++--- crates/language/src/buffer.rs | 8 ++------ crates/text/src/text.rs | 11 +++-------- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 9b36d760b7..82bebeb336 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -193,7 +193,7 @@ impl RefactoringAssistant { if let Some(transaction) = buffer.end_transaction(cx) { if let Some(first_transaction) = first_transaction { // Group all assistant edits into the first transaction. - buffer.merge_transaction_into( + buffer.merge_transactions( transaction, first_transaction, cx, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 28b31ef097..88c66d5200 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -615,7 +615,7 @@ impl MultiBuffer { } } - pub fn merge_transaction_into( + pub fn merge_transactions( &mut self, transaction: TransactionId, destination: TransactionId, @@ -623,7 +623,7 @@ impl MultiBuffer { ) { if let Some(buffer) = self.as_singleton() { buffer.update(cx, |buffer, _| { - buffer.merge_transaction_into(transaction, destination) + buffer.merge_transactions(transaction, destination) }); } else { if let Some(transaction) = self.history.remove_transaction(transaction) { @@ -634,7 +634,7 @@ impl MultiBuffer { { if let Some(state) = self.buffers.borrow().get(&buffer_id) { state.buffer.update(cx, |buffer, _| { - buffer.merge_transaction_into( + buffer.merge_transactions( buffer_transaction_id, *destination_buffer_transaction_id, ) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 69668a97c6..e2154f498e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1294,12 +1294,8 @@ impl Buffer { self.text.forget_transaction(transaction_id); } - pub fn merge_transaction_into( - &mut self, - transaction: TransactionId, - destination: TransactionId, - ) { - self.text.merge_transaction_into(transaction, destination); + pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { + self.text.merge_transactions(transaction, destination); } pub fn wait_for_edits( diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index e47e20da0d..8f15535ccf 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -310,7 +310,7 @@ impl History { Some(&mut entry.transaction) } - fn merge_transaction_into(&mut self, transaction: TransactionId, destination: TransactionId) { + 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); @@ -1225,13 +1225,8 @@ impl Buffer { self.history.forget(transaction_id); } - pub fn merge_transaction_into( - &mut self, - transaction: TransactionId, - destination: TransactionId, - ) { - self.history - .merge_transaction_into(transaction, destination); + pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { + self.history.merge_transactions(transaction, destination); } pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { From 24685061899a7ed5a0901cb180a57d71df5c38a8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 14:29:05 +0200 Subject: [PATCH 036/114] Always clear refactoring text highlights, even if an error occurs --- crates/ai/src/refactor.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactor.rs index 82bebeb336..1cb370dbba 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactor.rs @@ -101,6 +101,16 @@ impl RefactoringAssistant { editor.id(), cx.spawn(|mut cx| { async move { + let _clear_highlights = util::defer({ + let mut cx = cx.clone(); + let editor = editor.clone(); + move || { + let _ = editor.update(&mut cx, |editor, cx| { + editor.clear_text_highlights::(cx); + }); + } + }); + let mut edit_start = selection.start.to_offset(&snapshot); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); @@ -215,11 +225,7 @@ impl RefactoringAssistant { ); })?; } - diff.await?; - editor.update(&mut cx, |editor, cx| { - editor.clear_text_highlights::(cx); - })?; anyhow::Ok(()) } From c1d9b37dbc3102285b1499fa7123853291025dc9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 15:46:18 +0200 Subject: [PATCH 037/114] Move to an inline refactoring prompt --- crates/ai/src/ai.rs | 7 +- .../{refactor.rs => refactoring_assistant.rs} | 166 +++++------------- crates/ai/src/refactoring_modal.rs | 134 ++++++++++++++ crates/ai/src/{diff.rs => streaming_diff.rs} | 8 +- styles/src/style_tree/assistant.ts | 3 +- 5 files changed, 186 insertions(+), 132 deletions(-) rename crates/ai/src/{refactor.rs => refactoring_assistant.rs} (69%) create mode 100644 crates/ai/src/refactoring_modal.rs rename crates/ai/src/{diff.rs => streaming_diff.rs} (98%) diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index bad153879f..48f490c9c0 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,7 +1,8 @@ pub mod assistant; mod assistant_settings; -mod diff; -mod refactor; +mod refactoring_assistant; +mod refactoring_modal; +mod streaming_diff; use anyhow::{anyhow, Result}; pub use assistant::AssistantPanel; @@ -195,7 +196,7 @@ struct OpenAIChoice { pub fn init(cx: &mut AppContext) { assistant::init(cx); - refactor::init(cx); + refactoring_modal::init(cx); } pub async fn stream_completion( diff --git a/crates/ai/src/refactor.rs b/crates/ai/src/refactoring_assistant.rs similarity index 69% rename from crates/ai/src/refactor.rs rename to crates/ai/src/refactoring_assistant.rs index 1cb370dbba..5562cb4606 100644 --- a/crates/ai/src/refactor.rs +++ b/crates/ai/src/refactoring_assistant.rs @@ -1,25 +1,16 @@ -use crate::{diff::Diff, stream_completion, OpenAIRequest, RequestMessage, Role}; use collections::HashMap; use editor::{Editor, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, StreamExt}; -use gpui::{ - actions, elements::*, platform::MouseButton, AnyViewHandle, AppContext, Entity, Task, View, - ViewContext, ViewHandle, WeakViewHandle, -}; +use gpui::{AppContext, Task, ViewHandle}; use language::{Point, Rope}; -use menu::{Cancel, Confirm}; -use std::{cmp, env, sync::Arc}; +use std::{cmp, env, fmt::Write}; use util::TryFutureExt; -use workspace::{Modal, Workspace}; -actions!(assistant, [Refactor]); - -pub fn init(cx: &mut AppContext) { - cx.set_global(RefactoringAssistant::new()); - cx.add_action(RefactoringModal::deploy); - cx.add_action(RefactoringModal::confirm); - cx.add_action(RefactoringModal::cancel); -} +use crate::{ + stream_completion, + streaming_diff::{Hunk, StreamingDiff}, + OpenAIRequest, RequestMessage, Role, +}; pub struct RefactoringAssistant { pending_edits_by_editor: HashMap>>, @@ -32,7 +23,30 @@ impl RefactoringAssistant { } } - fn refactor(&mut self, editor: &ViewHandle, prompt: &str, cx: &mut AppContext) { + pub fn update(cx: &mut AppContext, f: F) -> T + where + F: FnOnce(&mut Self, &mut AppContext) -> T, + { + if !cx.has_global::() { + cx.set_global(Self::new()); + } + + cx.update_global(f) + } + + pub fn refactor( + &mut self, + editor: &ViewHandle, + user_prompt: &str, + cx: &mut AppContext, + ) { + let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { + api_key + } else { + // TODO: ensure the API key is present by going through the assistant panel's flow. + return; + }; + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selection = editor.read(cx).selections.newest_anchor().clone(); let selected_text = snapshot @@ -83,18 +97,20 @@ impl RefactoringAssistant { .language_at(selection.start) .map(|language| language.name()); let language_name = language_name.as_deref().unwrap_or(""); + + let mut prompt = String::new(); + writeln!(prompt, "Given the following {language_name} snippet:").unwrap(); + writeln!(prompt, "{normalized_selected_text}").unwrap(); + writeln!(prompt, "{user_prompt}.").unwrap(); + writeln!(prompt, "Never make remarks, reply only with the new code.").unwrap(); let request = OpenAIRequest { model: "gpt-4".into(), - messages: vec![ - RequestMessage { + messages: vec![RequestMessage { role: Role::User, - content: format!( - "Given the following {language_name} snippet:\n{normalized_selected_text}\n{prompt}. Never make remarks and reply only with the new code." - ), + content: prompt, }], stream: true, }; - let api_key = env::var("OPENAI_API_KEY").unwrap(); let response = stream_completion(api_key, cx.background().clone(), request); let editor = editor.downgrade(); self.pending_edits_by_editor.insert( @@ -116,7 +132,7 @@ impl RefactoringAssistant { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { let mut messages = response.await?.ready_chunks(4); - let mut diff = Diff::new(selected_text.to_string()); + let mut diff = StreamingDiff::new(selected_text.to_string()); let indentation_len; let indentation_text; @@ -177,18 +193,18 @@ impl RefactoringAssistant { buffer.start_transaction(cx); buffer.edit( hunks.into_iter().filter_map(|hunk| match hunk { - crate::diff::Hunk::Insert { text } => { + Hunk::Insert { text } => { let edit_start = snapshot.anchor_after(edit_start); Some((edit_start..edit_start, text)) } - crate::diff::Hunk::Remove { len } => { + Hunk::Remove { len } => { let edit_end = edit_start + len; let edit_range = snapshot.anchor_after(edit_start) ..snapshot.anchor_before(edit_end); edit_start = edit_end; Some((edit_range, String::new())) } - crate::diff::Hunk::Keep { len } => { + Hunk::Keep { len } => { let edit_end = edit_start + len; let edit_range = snapshot.anchor_after(edit_start) ..snapshot.anchor_before(edit_end); @@ -234,99 +250,3 @@ impl RefactoringAssistant { ); } } - -enum Event { - Dismissed, -} - -struct RefactoringModal { - active_editor: WeakViewHandle, - prompt_editor: ViewHandle, - has_focus: bool, -} - -impl Entity for RefactoringModal { - type Event = Event; -} - -impl View for RefactoringModal { - fn ui_name() -> &'static str { - "RefactoringModal" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - - ChildView::new(&self.prompt_editor, cx) - .constrained() - .with_width(theme.assistant.modal.width) - .contained() - .with_style(theme.assistant.modal.container) - .mouse::(0) - .on_click_out(MouseButton::Left, |_, _, cx| cx.emit(Event::Dismissed)) - .on_click_out(MouseButton::Right, |_, _, cx| cx.emit(Event::Dismissed)) - .aligned() - .right() - .into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - cx.focus(&self.prompt_editor); - } - - fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } -} - -impl Modal for RefactoringModal { - fn has_focus(&self) -> bool { - self.has_focus - } - - fn dismiss_on_event(event: &Self::Event) -> bool { - matches!(event, Self::Event::Dismissed) - } -} - -impl RefactoringModal { - fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { - if let Some(active_editor) = workspace - .active_item(cx) - .and_then(|item| Some(item.act_as::(cx)?.downgrade())) - { - workspace.toggle_modal(cx, |_, cx| { - let prompt_editor = cx.add_view(|cx| { - let mut editor = Editor::auto_height( - theme::current(cx).assistant.modal.editor_max_lines, - Some(Arc::new(|theme| theme.assistant.modal.editor.clone())), - cx, - ); - editor - .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor - }); - cx.add_view(|_| RefactoringModal { - active_editor, - prompt_editor, - has_focus: false, - }) - }); - } - } - - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(Event::Dismissed); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(editor) = self.active_editor.upgrade(cx) { - let prompt = self.prompt_editor.read(cx).text(cx); - cx.update_global(|assistant: &mut RefactoringAssistant, cx| { - assistant.refactor(&editor, &prompt, cx); - }); - cx.emit(Event::Dismissed); - } - } -} diff --git a/crates/ai/src/refactoring_modal.rs b/crates/ai/src/refactoring_modal.rs new file mode 100644 index 0000000000..2203acc921 --- /dev/null +++ b/crates/ai/src/refactoring_modal.rs @@ -0,0 +1,134 @@ +use crate::refactoring_assistant::RefactoringAssistant; +use collections::HashSet; +use editor::{ + display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle}, + scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, + Editor, +}; +use gpui::{ + actions, elements::*, platform::MouseButton, AnyViewHandle, AppContext, Entity, View, + ViewContext, ViewHandle, WeakViewHandle, +}; +use std::sync::Arc; +use workspace::Workspace; + +actions!(assistant, [Refactor]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(RefactoringModal::deploy); + cx.add_action(RefactoringModal::confirm); + cx.add_action(RefactoringModal::cancel); +} + +enum Event { + Dismissed, +} + +struct RefactoringModal { + active_editor: WeakViewHandle, + prompt_editor: ViewHandle, + has_focus: bool, +} + +impl Entity for RefactoringModal { + type Event = Event; +} + +impl View for RefactoringModal { + fn ui_name() -> &'static str { + "RefactoringModal" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(&self.prompt_editor, cx) + .mouse::(0) + .on_click_out(MouseButton::Left, |_, _, cx| cx.emit(Event::Dismissed)) + .on_click_out(MouseButton::Right, |_, _, cx| cx.emit(Event::Dismissed)) + .into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; + cx.focus(&self.prompt_editor); + } + + fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if !self.prompt_editor.is_focused(cx) { + self.has_focus = false; + cx.emit(Event::Dismissed); + } + } +} + +impl RefactoringModal { + fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { + if let Some(active_editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + active_editor.update(cx, |editor, cx| { + let position = editor.selections.newest_anchor().head(); + let prompt_editor = cx.add_view(|cx| { + Editor::single_line( + Some(Arc::new(|theme| theme.assistant.modal.editor.clone())), + cx, + ) + }); + let active_editor = cx.weak_handle(); + let refactoring = cx.add_view(|_| RefactoringModal { + active_editor, + prompt_editor, + has_focus: false, + }); + cx.focus(&refactoring); + + let block_id = editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position, + height: 2, + render: Arc::new({ + let refactoring = refactoring.clone(); + move |cx: &mut BlockContext| { + ChildView::new(&refactoring, cx) + .contained() + .with_padding_left(cx.gutter_width) + .aligned() + .left() + .into_any() + } + }), + disposition: BlockDisposition::Below, + }], + Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), + cx, + )[0]; + cx.subscribe(&refactoring, move |_, refactoring, event, cx| { + let Event::Dismissed = event; + if let Some(active_editor) = refactoring.read(cx).active_editor.upgrade(cx) { + cx.window_context().defer(move |cx| { + active_editor.update(cx, |editor, cx| { + editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + }) + }); + } + }) + .detach(); + }); + } + } + + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if let Some(editor) = self.active_editor.upgrade(cx) { + let prompt = self.prompt_editor.read(cx).text(cx); + RefactoringAssistant::update(cx, |assistant, cx| { + assistant.refactor(&editor, &prompt, cx); + }); + cx.emit(Event::Dismissed); + } + } +} diff --git a/crates/ai/src/diff.rs b/crates/ai/src/streaming_diff.rs similarity index 98% rename from crates/ai/src/diff.rs rename to crates/ai/src/streaming_diff.rs index 7c5af34ff5..1e5189d4d8 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/streaming_diff.rs @@ -71,7 +71,7 @@ pub enum Hunk { Keep { len: usize }, } -pub struct Diff { +pub struct StreamingDiff { old: Vec, new: Vec, scores: Matrix, @@ -80,10 +80,10 @@ pub struct Diff { equal_runs: HashMap<(usize, usize), u32>, } -impl Diff { +impl StreamingDiff { const INSERTION_SCORE: f64 = -1.; const DELETION_SCORE: f64 = -5.; - const EQUALITY_BASE: f64 = 1.618; + const EQUALITY_BASE: f64 = 2.; const MAX_EQUALITY_EXPONENT: i32 = 32; pub fn new(old: String) -> Self { @@ -250,7 +250,7 @@ mod tests { .collect::(); log::info!("old text: {:?}", old); - let mut diff = Diff::new(old.clone()); + let mut diff = StreamingDiff::new(old.clone()); let mut hunks = Vec::new(); let mut new_len = 0; let mut new = String::new(); diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 88efabee1e..a02d7eb40c 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -69,8 +69,7 @@ export default function assistant(): any { width: 500, editor_max_lines: 6, editor: { - background: background(theme.lowest), - text: text(theme.lowest, "mono", "on"), + text: text(theme.lowest, "mono", "on", { size: "sm" }), placeholder_text: text(theme.lowest, "sans", "on", "disabled"), selection: theme.players[0], } From cbf7160054962980968dae6176e79d6c308289c4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 16:32:55 +0200 Subject: [PATCH 038/114] Improve scoring --- crates/ai/src/streaming_diff.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/streaming_diff.rs b/crates/ai/src/streaming_diff.rs index 1e5189d4d8..5425b75bbe 100644 --- a/crates/ai/src/streaming_diff.rs +++ b/crates/ai/src/streaming_diff.rs @@ -83,8 +83,8 @@ pub struct StreamingDiff { impl StreamingDiff { const INSERTION_SCORE: f64 = -1.; const DELETION_SCORE: f64 = -5.; - const EQUALITY_BASE: f64 = 2.; - const MAX_EQUALITY_EXPONENT: i32 = 32; + const EQUALITY_BASE: f64 = 1.4; + const MAX_EQUALITY_EXPONENT: i32 = 64; pub fn new(old: String) -> Self { let old = old.chars().collect::>(); @@ -120,7 +120,7 @@ impl StreamingDiff { if self.old[i - 1] == ' ' { self.scores.get(i - 1, j - 1) } else { - let exponent = cmp::min(equal_run as i32 / 3, Self::MAX_EQUALITY_EXPONENT); + let exponent = cmp::min(equal_run as i32, Self::MAX_EQUALITY_EXPONENT); self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) } } else { From 805e44915cdcb9e96879ea472879a2827dfe4d40 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 17:23:12 +0200 Subject: [PATCH 039/114] WIP --- crates/ai/src/refactoring_modal.rs | 7 +++++-- crates/theme/src/theme.rs | 2 -- styles/src/style_tree/assistant.ts | 13 +++++-------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/ai/src/refactoring_modal.rs b/crates/ai/src/refactoring_modal.rs index 2203acc921..675e0fae99 100644 --- a/crates/ai/src/refactoring_modal.rs +++ b/crates/ai/src/refactoring_modal.rs @@ -40,7 +40,12 @@ impl View for RefactoringModal { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = theme::current(cx); ChildView::new(&self.prompt_editor, cx) + .aligned() + .left() + .contained() + .with_style(theme.assistant.modal.container) .mouse::(0) .on_click_out(MouseButton::Left, |_, _, cx| cx.emit(Event::Dismissed)) .on_click_out(MouseButton::Right, |_, _, cx| cx.emit(Event::Dismissed)) @@ -93,8 +98,6 @@ impl RefactoringModal { ChildView::new(&refactoring, cx) .contained() .with_padding_left(cx.gutter_width) - .aligned() - .left() .into_any() } }), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a42a893241..ebc9591239 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1131,8 +1131,6 @@ pub struct AssistantStyle { pub struct ModalAssistantStyle { #[serde(flatten)] pub container: ContainerStyle, - pub width: f32, - pub editor_max_lines: usize, pub editor: FieldEditor, } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index a02d7eb40c..ac91d1118d 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -60,14 +60,11 @@ export default function assistant(): any { padding: { left: 12 }, }, modal: { - background: background(theme.lowest), - border: border(theme.lowest), - shadow: theme.modal_shadow, - corner_radius: 12, - padding: { left: 12, right: 0, top: 12, bottom: 12 }, - margin: { right: 12 }, - width: 500, - editor_max_lines: 6, + border: border(theme.lowest, "on", { + top: true, + bottom: true, + overlay: true, + }), editor: { text: text(theme.lowest, "mono", "on", { size: "sm" }), placeholder_text: text(theme.lowest, "sans", "on", "disabled"), From cb4b816d0e8e8b08874a7ed54625d8b85b4c4e48 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 17:31:15 +0200 Subject: [PATCH 040/114] Add todo for modal assistant --- todo.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 todo.md diff --git a/todo.md b/todo.md new file mode 100644 index 0000000000..59b4a5a839 --- /dev/null +++ b/todo.md @@ -0,0 +1,7 @@ +- Style the current inline editor +- Find a way to understand whether we want to refactor or append, or both. (function calls) +- Add a system prompt that makes GPT an expert of language X +- Provide context around the cursor/selection. We should try to fill the context window as much as possible (try to fill half of it so that we can spit out another half) +- When you hit escape, the assistant should stop. +- When you hit undo and you undo a transaction from the assistant, we should stop generating. +- Keep the inline editor around until the assistant is done. Add a cancel button to stop, and and undo button to undo the whole thing. (Interactive) From b6035ee6a6b804436fcb564aaef4d8f55ab3e472 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Aug 2023 20:00:25 +0200 Subject: [PATCH 041/114] WIP --- todo.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/todo.md b/todo.md index 59b4a5a839..e07d19bc95 100644 --- a/todo.md +++ b/todo.md @@ -5,3 +5,59 @@ - When you hit escape, the assistant should stop. - When you hit undo and you undo a transaction from the assistant, we should stop generating. - Keep the inline editor around until the assistant is done. Add a cancel button to stop, and and undo button to undo the whole thing. (Interactive) + + +# 9:39 AM + +- Hit `ctrl-enter` + +- Puts me in assistant mode with the selected text highlighted in a special color. If text was selected, I'm in transformation mode. +- If there's no selection, put me on the line below, aligned with the indent of the line. +- Enter starts generation +- Ctrl-enter inserts a newline +- Once generations starts, enter "confirms it" by dismissing the inline editor. +- Escape in the inline editor cancels/undoes/dismisses. +- To generate text in reference to other text, we can *mark* text. + + +- Hit ctrl-enter deploys an edit prompt + - Empty selection (cursor) => append text + - On end of line: Edit prompt on end of line. + - Middle of line: Edit prompt near cursor head on a different line + - Non-empty selection => refactor + - Edit prompt near cursor head on a different line + - What was selected when you hit ctrl-enter is colored. +- Selection is cleared and cursor is moved to prompt input +- When cursor is inside a prompt + - Escape cancels/undoes + - Enter confirms +- Multicursor + - Run the same prompt for every selection in parallel + - Position the prompt editor at the newest cursor +- Follow up ship: Marks + - Global across all buffers + - Select text, hit a binding + - That text gets added to the marks + - Simplest: Marks are a set, and you add to them with this binding. + - Could this be a stack? That might be too much. + - When you hit ctrl-enter to generate / transform text, we include the marked text in the context. + +- During inference, always send marked text. +- During inference, send as much context as possible given the user's desired generation length. + +- This would assume a convenient binding for setting the generation length. + + +~~~~~~~~~ + +Dial up / dial down how much context we send +Dial up / down your max generation length. + + +------- (merge to main) + +- Text in the prompt should soft wrap + +----------- (maybe pause) + +- Excurse outside of the editor without dismissing it... kind of like a message in the assistant. From c1bd03587501ead64f0d1783d362d5c9554e400a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 11:39:27 +0200 Subject: [PATCH 042/114] Rework inline assistant --- Cargo.lock | 1 - assets/keymaps/default.json | 3 +- crates/ai/Cargo.toml | 1 - crates/ai/src/ai.rs | 3 - crates/ai/src/assistant.rs | 574 +++++++++++++++++++++++-- crates/ai/src/refactoring_assistant.rs | 252 ----------- crates/ai/src/refactoring_modal.rs | 137 ------ crates/ai/src/streaming_diff.rs | 12 +- crates/editor/src/editor.rs | 4 +- crates/editor/src/multi_buffer.rs | 16 +- crates/language/src/buffer.rs | 16 + crates/text/src/text.rs | 9 + crates/theme/src/theme.rs | 5 +- styles/src/style_tree/assistant.ts | 5 +- 14 files changed, 600 insertions(+), 438 deletions(-) delete mode 100644 crates/ai/src/refactoring_assistant.rs delete mode 100644 crates/ai/src/refactoring_modal.rs diff --git a/Cargo.lock b/Cargo.lock index 2a4c6c4f43..e74068b2d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,7 +122,6 @@ dependencies = [ "serde", "serde_json", "settings", - "similar", "smol", "theme", "tiktoken-rs 0.4.5", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 3ec994335e..a81c18f708 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -528,7 +528,8 @@ "bindings": { "alt-enter": "editor::OpenExcerpts", "cmd-f8": "editor::GoToHunk", - "cmd-shift-f8": "editor::GoToPrevHunk" + "cmd-shift-f8": "editor::GoToPrevHunk", + "ctrl-enter": "assistant::InlineAssist" } }, { diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index b03405bb93..4438f88108 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -31,7 +31,6 @@ regex.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true -similar = "1.3" smol.workspace = true tiktoken-rs = "0.4" diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 48f490c9c0..0b56fedb11 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,7 +1,5 @@ pub mod assistant; mod assistant_settings; -mod refactoring_assistant; -mod refactoring_modal; mod streaming_diff; use anyhow::{anyhow, Result}; @@ -196,7 +194,6 @@ struct OpenAIChoice { pub fn init(cx: &mut AppContext) { assistant::init(cx); - refactoring_modal::init(cx); } pub async fn stream_completion( diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index f134eeeeb6..58be0fe584 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,18 +1,22 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, - stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, - Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, + stream_completion, + streaming_diff::{Hunk, StreamingDiff}, + MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role, + SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::{HashMap, HashSet}; use editor::{ - display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint}, + display_map::{ + BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, + }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, ToOffset, + Anchor, Editor, ToOffset, ToPoint, }; use fs::Fs; -use futures::StreamExt; +use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ actions, elements::*, @@ -21,7 +25,10 @@ use gpui::{ Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; +use language::{ + language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, Selection, ToOffset as _, + TransactionId, +}; use search::BufferSearchBar; use settings::SettingsStore; use std::{ @@ -53,6 +60,7 @@ actions!( QuoteSelection, ToggleFocus, ResetKey, + InlineAssist ] ); @@ -84,6 +92,9 @@ pub fn init(cx: &mut AppContext) { workspace.toggle_panel_focus::(cx); }, ); + cx.add_action(AssistantPanel::inline_assist); + cx.add_action(InlineAssistant::confirm); + cx.add_action(InlineAssistant::cancel); } #[derive(Debug)] @@ -113,6 +124,9 @@ pub struct AssistantPanel { languages: Arc, fs: Arc, subscriptions: Vec, + next_inline_assist_id: usize, + pending_inline_assists: HashMap, + pending_inline_assist_ids_by_editor: HashMap, Vec>, _watch_saved_conversations: Task>, } @@ -176,6 +190,9 @@ impl AssistantPanel { width: None, height: None, subscriptions: Default::default(), + next_inline_assist_id: 0, + pending_inline_assists: Default::default(), + pending_inline_assist_ids_by_editor: Default::default(), _watch_saved_conversations, }; @@ -196,6 +213,425 @@ impl AssistantPanel { }) } + fn inline_assist(workspace: &mut Workspace, _: &InlineAssist, cx: &mut ViewContext) { + let assistant = if let Some(assistant) = workspace.panel::(cx) { + if assistant + .update(cx, |assistant, cx| assistant.load_api_key(cx)) + .is_some() + { + assistant + } else { + workspace.focus_panel::(cx); + return; + } + } else { + return; + }; + + let active_editor = if let Some(active_editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + active_editor + } else { + return; + }; + + assistant.update(cx, |assistant, cx| { + assistant.new_inline_assist(&active_editor, cx) + }); + } + + fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { + let id = post_inc(&mut self.next_inline_assist_id); + let (block_id, inline_assistant, selection) = editor.update(cx, |editor, cx| { + let selection = editor.selections.newest_anchor().clone(); + let prompt_editor = cx.add_view(|cx| { + Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ) + }); + let assist_kind = if editor.selections.newest::(cx).is_empty() { + InlineAssistKind::Insert + } else { + InlineAssistKind::Edit + }; + let assistant = cx.add_view(|_| InlineAssistant { + id, + prompt_editor, + confirmed: false, + has_focus: false, + assist_kind, + }); + cx.focus(&assistant); + + let block_id = editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: selection.head(), + height: 2, + render: Arc::new({ + let assistant = assistant.clone(); + move |cx: &mut BlockContext| { + ChildView::new(&assistant, cx) + .contained() + .with_padding_left(match assist_kind { + InlineAssistKind::Edit => cx.gutter_width, + InlineAssistKind::Insert => cx.anchor_x, + }) + .into_any() + } + }), + disposition: if selection.reversed { + BlockDisposition::Above + } else { + BlockDisposition::Below + }, + }], + Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), + cx, + )[0]; + editor.highlight_background::( + vec![selection.start..selection.end], + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + + (block_id, assistant, selection) + }); + + self.pending_inline_assists.insert( + id, + PendingInlineAssist { + editor: editor.downgrade(), + selection, + inline_assistant_block_id: Some(block_id), + code_generation: Task::ready(None), + transaction_id: None, + _subscriptions: vec![ + cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), + cx.subscribe(editor, { + let inline_assistant = inline_assistant.downgrade(); + move |_, editor, event, cx| { + if let Some(inline_assistant) = inline_assistant.upgrade(cx) { + if let editor::Event::SelectionsChanged { local } = event { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); + } + } + } + } + }), + ], + }, + ); + self.pending_inline_assist_ids_by_editor + .entry(editor.downgrade()) + .or_default() + .push(id); + } + + fn handle_inline_assistant_event( + &mut self, + inline_assistant: ViewHandle, + event: &InlineAssistantEvent, + cx: &mut ViewContext, + ) { + let assist_id = inline_assistant.read(cx).id; + match event { + InlineAssistantEvent::Confirmed { prompt } => { + self.generate_code(assist_id, prompt, cx); + } + InlineAssistantEvent::Canceled => { + self.complete_inline_assist(assist_id, true, cx); + } + InlineAssistantEvent::Dismissed => { + self.dismiss_inline_assist(assist_id, cx); + } + } + } + + fn complete_inline_assist( + &mut self, + assist_id: usize, + cancel: bool, + cx: &mut ViewContext, + ) { + self.dismiss_inline_assist(assist_id, cx); + + if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { + self.pending_inline_assist_ids_by_editor + .remove(&pending_assist.editor); + + if let Some(editor) = pending_assist.editor.upgrade(cx) { + editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(cx); + editor.clear_text_highlights::(cx); + }); + + if cancel { + if let Some(transaction_id) = pending_assist.transaction_id { + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + buffer.undo_and_forget(transaction_id, cx) + }); + }); + } + } + } + } + } + + fn dismiss_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { + if let Some(editor) = pending_assist.editor.upgrade(cx) { + if let Some(block_id) = pending_assist.inline_assistant_block_id.take() { + editor.update(cx, |editor, cx| { + editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + }); + } + } + } + } + + pub fn generate_code( + &mut self, + inline_assist_id: usize, + user_prompt: &str, + cx: &mut ViewContext, + ) { + let api_key = if let Some(api_key) = self.api_key.borrow().clone() { + api_key + } else { + return; + }; + + let pending_assist = + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { + pending_assist + } else { + return; + }; + + let editor = if let Some(editor) = pending_assist.editor.upgrade(cx) { + editor + } else { + return; + }; + + let selection = pending_assist.selection.clone(); + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let selected_text = snapshot + .text_for_range(selection.start..selection.end) + .collect::(); + + let mut normalized_selected_text = selected_text.clone(); + let mut base_indentation: Option = None; + let selection_start = selection.start.to_point(&snapshot); + let selection_end = selection.end.to_point(&snapshot); + if selection_start.row < selection_end.row { + for row in selection_start.row..=selection_end.row { + if snapshot.is_line_blank(row) { + continue; + } + + let line_indentation = snapshot.indent_size_for_line(row); + if let Some(base_indentation) = base_indentation.as_mut() { + if line_indentation.len < base_indentation.len { + *base_indentation = line_indentation; + } + } else { + base_indentation = Some(line_indentation); + } + } + } + + if let Some(base_indentation) = base_indentation { + for row in selection_start.row..=selection_end.row { + let selection_row = row - selection_start.row; + let line_start = + normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); + let indentation_len = if row == selection_start.row { + base_indentation.len.saturating_sub(selection_start.column) + } else { + let line_len = normalized_selected_text.line_len(selection_row); + cmp::min(line_len, base_indentation.len) + }; + let indentation_end = cmp::min( + line_start + indentation_len as usize, + normalized_selected_text.len(), + ); + normalized_selected_text.replace(line_start..indentation_end, ""); + } + } + + let language_name = snapshot + .language_at(selection.start) + .map(|language| language.name()); + let language_name = language_name.as_deref().unwrap_or(""); + + let mut prompt = String::new(); + writeln!(prompt, "Given the following {language_name} snippet:").unwrap(); + writeln!(prompt, "{normalized_selected_text}").unwrap(); + writeln!(prompt, "{user_prompt}.").unwrap(); + writeln!(prompt, "Never make remarks, reply only with the new code.").unwrap(); + let request = OpenAIRequest { + model: "gpt-4".into(), + messages: vec![RequestMessage { + role: Role::User, + content: prompt, + }], + stream: true, + }; + let response = stream_completion(api_key, cx.background().clone(), request); + let editor = editor.downgrade(); + + pending_assist.code_generation = cx.spawn(|this, mut cx| { + async move { + let _cleanup = util::defer({ + let mut cx = cx.clone(); + let this = this.clone(); + move || { + let _ = this.update(&mut cx, |this, cx| { + this.complete_inline_assist(inline_assist_id, false, cx) + }); + } + }); + + let mut edit_start = selection.start.to_offset(&snapshot); + + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let mut messages = response.await?; + let mut diff = StreamingDiff::new(selected_text.to_string()); + + let indentation_len; + let indentation_text; + if let Some(base_indentation) = base_indentation { + indentation_len = base_indentation.len; + indentation_text = match base_indentation.kind { + language::IndentKind::Space => " ", + language::IndentKind::Tab => "\t", + }; + } else { + indentation_len = 0; + indentation_text = ""; + }; + + let mut new_text = indentation_text + .repeat(indentation_len.saturating_sub(selection_start.column) as usize); + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + if let Some(text) = choice.delta.content { + let mut lines = text.split('\n'); + if let Some(first_line) = lines.next() { + new_text.push_str(&first_line); + } + + for line in lines { + new_text.push('\n'); + new_text.push_str( + &indentation_text.repeat(indentation_len as usize), + ); + new_text.push_str(line); + } + } + } + + let hunks = diff.push_new(&new_text); + hunks_tx.send(hunks).await?; + new_text.clear(); + } + hunks_tx.send(diff.finish()).await?; + + anyhow::Ok(()) + }); + + while let Some(hunks) = hunks_rx.next().await { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))?; + editor.update(&mut cx, |editor, cx| { + let mut highlights = Vec::new(); + + let transaction = editor.buffer().update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); + + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start += len; + highlights.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + this.update(cx, |this, cx| { + if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + if let Some(first_transaction) = pending_assist.transaction_id { + // Group all assistant edits into the first transaction. + editor.buffer().update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + pending_assist.transaction_id = Some(transaction); + editor.buffer().update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } + }); + } + + editor.highlight_text::( + highlights, + gpui::fonts::HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + })?; + } + diff.await?; + + anyhow::Ok(()) + } + .log_err() + }); + } + fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { let editor = cx.add_view(|cx| { ConversationEditor::new( @@ -565,6 +1001,32 @@ impl AssistantPanel { .iter() .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) } + + pub fn load_api_key(&mut self, cx: &mut ViewContext) -> Option { + if self.api_key.borrow().is_none() && !self.has_read_credentials { + self.has_read_credentials = true; + let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { + Some(api_key) + } else if let Some((_, api_key)) = cx + .platform() + .read_credentials(OPENAI_API_URL) + .log_err() + .flatten() + { + String::from_utf8(api_key).log_err() + } else { + None + }; + if let Some(api_key) = api_key { + *self.api_key.borrow_mut() = Some(api_key); + } else if self.api_key_editor.is_none() { + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.notify(); + } + } + + self.api_key.borrow().clone() + } } fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { @@ -748,27 +1210,7 @@ impl Panel for AssistantPanel { fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active { - if self.api_key.borrow().is_none() && !self.has_read_credentials { - self.has_read_credentials = true; - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - Some(api_key) - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() - { - String::from_utf8(api_key).log_err() - } else { - None - }; - if let Some(api_key) = api_key { - *self.api_key.borrow_mut() = Some(api_key); - } else if self.api_key_editor.is_none() { - self.api_key_editor = Some(build_api_key_editor(cx)); - cx.notify(); - } - } + self.load_api_key(cx); if self.editors.is_empty() { self.new_conversation(cx); @@ -2139,6 +2581,84 @@ impl Message { } } +enum InlineAssistantEvent { + Confirmed { prompt: String }, + Canceled, + Dismissed, +} + +#[derive(Copy, Clone)] +enum InlineAssistKind { + Edit, + Insert, +} + +struct InlineAssistant { + id: usize, + prompt_editor: ViewHandle, + confirmed: bool, + assist_kind: InlineAssistKind, + has_focus: bool, +} + +impl Entity for InlineAssistant { + type Event = InlineAssistantEvent; +} + +impl View for InlineAssistant { + fn ui_name() -> &'static str { + "InlineAssistant" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = theme::current(cx); + let prompt_editor = ChildView::new(&self.prompt_editor, cx).aligned().left(); + match self.assist_kind { + InlineAssistKind::Edit => prompt_editor + .contained() + .with_style(theme.assistant.inline.container) + .into_any(), + InlineAssistKind::Insert => prompt_editor.into_any(), + } + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.focus(&self.prompt_editor); + self.has_focus = true; + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl InlineAssistant { + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + cx.emit(InlineAssistantEvent::Canceled); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if self.confirmed { + cx.emit(InlineAssistantEvent::Dismissed); + } else { + let prompt = self.prompt_editor.read(cx).text(cx); + self.prompt_editor + .update(cx, |editor, _| editor.set_read_only(true)); + cx.emit(InlineAssistantEvent::Confirmed { prompt }); + self.confirmed = true; + } + } +} + +struct PendingInlineAssist { + editor: WeakViewHandle, + selection: Selection, + inline_assistant_block_id: Option, + code_generation: Task>, + transaction_id: Option, + _subscriptions: Vec, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ai/src/refactoring_assistant.rs b/crates/ai/src/refactoring_assistant.rs deleted file mode 100644 index 5562cb4606..0000000000 --- a/crates/ai/src/refactoring_assistant.rs +++ /dev/null @@ -1,252 +0,0 @@ -use collections::HashMap; -use editor::{Editor, ToOffset, ToPoint}; -use futures::{channel::mpsc, SinkExt, StreamExt}; -use gpui::{AppContext, Task, ViewHandle}; -use language::{Point, Rope}; -use std::{cmp, env, fmt::Write}; -use util::TryFutureExt; - -use crate::{ - stream_completion, - streaming_diff::{Hunk, StreamingDiff}, - OpenAIRequest, RequestMessage, Role, -}; - -pub struct RefactoringAssistant { - pending_edits_by_editor: HashMap>>, -} - -impl RefactoringAssistant { - fn new() -> Self { - Self { - pending_edits_by_editor: Default::default(), - } - } - - pub fn update(cx: &mut AppContext, f: F) -> T - where - F: FnOnce(&mut Self, &mut AppContext) -> T, - { - if !cx.has_global::() { - cx.set_global(Self::new()); - } - - cx.update_global(f) - } - - pub fn refactor( - &mut self, - editor: &ViewHandle, - user_prompt: &str, - cx: &mut AppContext, - ) { - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - api_key - } else { - // TODO: ensure the API key is present by going through the assistant panel's flow. - return; - }; - - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let selection = editor.read(cx).selections.newest_anchor().clone(); - let selected_text = snapshot - .text_for_range(selection.start..selection.end) - .collect::(); - - let mut normalized_selected_text = selected_text.clone(); - let mut base_indentation: Option = None; - let selection_start = selection.start.to_point(&snapshot); - let selection_end = selection.end.to_point(&snapshot); - if selection_start.row < selection_end.row { - for row in selection_start.row..=selection_end.row { - if snapshot.is_line_blank(row) { - continue; - } - - let line_indentation = snapshot.indent_size_for_line(row); - if let Some(base_indentation) = base_indentation.as_mut() { - if line_indentation.len < base_indentation.len { - *base_indentation = line_indentation; - } - } else { - base_indentation = Some(line_indentation); - } - } - } - - if let Some(base_indentation) = base_indentation { - for row in selection_start.row..=selection_end.row { - let selection_row = row - selection_start.row; - let line_start = - normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); - let indentation_len = if row == selection_start.row { - base_indentation.len.saturating_sub(selection_start.column) - } else { - let line_len = normalized_selected_text.line_len(selection_row); - cmp::min(line_len, base_indentation.len) - }; - let indentation_end = cmp::min( - line_start + indentation_len as usize, - normalized_selected_text.len(), - ); - normalized_selected_text.replace(line_start..indentation_end, ""); - } - } - - let language_name = snapshot - .language_at(selection.start) - .map(|language| language.name()); - let language_name = language_name.as_deref().unwrap_or(""); - - let mut prompt = String::new(); - writeln!(prompt, "Given the following {language_name} snippet:").unwrap(); - writeln!(prompt, "{normalized_selected_text}").unwrap(); - writeln!(prompt, "{user_prompt}.").unwrap(); - writeln!(prompt, "Never make remarks, reply only with the new code.").unwrap(); - let request = OpenAIRequest { - model: "gpt-4".into(), - messages: vec![RequestMessage { - role: Role::User, - content: prompt, - }], - stream: true, - }; - let response = stream_completion(api_key, cx.background().clone(), request); - let editor = editor.downgrade(); - self.pending_edits_by_editor.insert( - editor.id(), - cx.spawn(|mut cx| { - async move { - let _clear_highlights = util::defer({ - let mut cx = cx.clone(); - let editor = editor.clone(); - move || { - let _ = editor.update(&mut cx, |editor, cx| { - editor.clear_text_highlights::(cx); - }); - } - }); - - let mut edit_start = selection.start.to_offset(&snapshot); - - let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background().spawn(async move { - let mut messages = response.await?.ready_chunks(4); - let mut diff = StreamingDiff::new(selected_text.to_string()); - - let indentation_len; - let indentation_text; - if let Some(base_indentation) = base_indentation { - indentation_len = base_indentation.len; - indentation_text = match base_indentation.kind { - language::IndentKind::Space => " ", - language::IndentKind::Tab => "\t", - }; - } else { - indentation_len = 0; - indentation_text = ""; - }; - - let mut new_text = - indentation_text.repeat( - indentation_len.saturating_sub(selection_start.column) as usize, - ); - while let Some(messages) = messages.next().await { - for message in messages { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - if let Some(text) = choice.delta.content { - let mut lines = text.split('\n'); - if let Some(first_line) = lines.next() { - new_text.push_str(&first_line); - } - - for line in lines { - new_text.push('\n'); - new_text.push_str( - &indentation_text.repeat(indentation_len as usize), - ); - new_text.push_str(line); - } - } - } - } - - let hunks = diff.push_new(&new_text); - hunks_tx.send(hunks).await?; - new_text.clear(); - } - hunks_tx.send(diff.finish()).await?; - - anyhow::Ok(()) - }); - - let mut first_transaction = None; - while let Some(hunks) = hunks_rx.next().await { - editor.update(&mut cx, |editor, cx| { - let mut highlights = Vec::new(); - - editor.buffer().update(cx, |buffer, cx| { - // Avoid grouping assistant edits with user edits. - buffer.finalize_last_transaction(cx); - - buffer.start_transaction(cx); - buffer.edit( - hunks.into_iter().filter_map(|hunk| match hunk { - Hunk::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - Some((edit_start..edit_start, text)) - } - Hunk::Remove { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start = edit_end; - Some((edit_range, String::new())) - } - Hunk::Keep { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start += len; - highlights.push(edit_range); - None - } - }), - None, - cx, - ); - if let Some(transaction) = buffer.end_transaction(cx) { - if let Some(first_transaction) = first_transaction { - // Group all assistant edits into the first transaction. - buffer.merge_transactions( - transaction, - first_transaction, - cx, - ); - } else { - first_transaction = Some(transaction); - buffer.finalize_last_transaction(cx); - } - } - }); - - editor.highlight_text::( - highlights, - gpui::fonts::HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); - })?; - } - diff.await?; - - anyhow::Ok(()) - } - .log_err() - }), - ); - } -} diff --git a/crates/ai/src/refactoring_modal.rs b/crates/ai/src/refactoring_modal.rs deleted file mode 100644 index 675e0fae99..0000000000 --- a/crates/ai/src/refactoring_modal.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::refactoring_assistant::RefactoringAssistant; -use collections::HashSet; -use editor::{ - display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle}, - scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Editor, -}; -use gpui::{ - actions, elements::*, platform::MouseButton, AnyViewHandle, AppContext, Entity, View, - ViewContext, ViewHandle, WeakViewHandle, -}; -use std::sync::Arc; -use workspace::Workspace; - -actions!(assistant, [Refactor]); - -pub fn init(cx: &mut AppContext) { - cx.add_action(RefactoringModal::deploy); - cx.add_action(RefactoringModal::confirm); - cx.add_action(RefactoringModal::cancel); -} - -enum Event { - Dismissed, -} - -struct RefactoringModal { - active_editor: WeakViewHandle, - prompt_editor: ViewHandle, - has_focus: bool, -} - -impl Entity for RefactoringModal { - type Event = Event; -} - -impl View for RefactoringModal { - fn ui_name() -> &'static str { - "RefactoringModal" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() - .contained() - .with_style(theme.assistant.modal.container) - .mouse::(0) - .on_click_out(MouseButton::Left, |_, _, cx| cx.emit(Event::Dismissed)) - .on_click_out(MouseButton::Right, |_, _, cx| cx.emit(Event::Dismissed)) - .into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - cx.focus(&self.prompt_editor); - } - - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if !self.prompt_editor.is_focused(cx) { - self.has_focus = false; - cx.emit(Event::Dismissed); - } - } -} - -impl RefactoringModal { - fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { - if let Some(active_editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - { - active_editor.update(cx, |editor, cx| { - let position = editor.selections.newest_anchor().head(); - let prompt_editor = cx.add_view(|cx| { - Editor::single_line( - Some(Arc::new(|theme| theme.assistant.modal.editor.clone())), - cx, - ) - }); - let active_editor = cx.weak_handle(); - let refactoring = cx.add_view(|_| RefactoringModal { - active_editor, - prompt_editor, - has_focus: false, - }); - cx.focus(&refactoring); - - let block_id = editor.insert_blocks( - [BlockProperties { - style: BlockStyle::Flex, - position, - height: 2, - render: Arc::new({ - let refactoring = refactoring.clone(); - move |cx: &mut BlockContext| { - ChildView::new(&refactoring, cx) - .contained() - .with_padding_left(cx.gutter_width) - .into_any() - } - }), - disposition: BlockDisposition::Below, - }], - Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), - cx, - )[0]; - cx.subscribe(&refactoring, move |_, refactoring, event, cx| { - let Event::Dismissed = event; - if let Some(active_editor) = refactoring.read(cx).active_editor.upgrade(cx) { - cx.window_context().defer(move |cx| { - active_editor.update(cx, |editor, cx| { - editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); - }) - }); - } - }) - .detach(); - }); - } - } - - fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - cx.emit(Event::Dismissed); - } - - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - if let Some(editor) = self.active_editor.upgrade(cx) { - let prompt = self.prompt_editor.read(cx).text(cx); - RefactoringAssistant::update(cx, |assistant, cx| { - assistant.refactor(&editor, &prompt, cx); - }); - cx.emit(Event::Dismissed); - } - } -} diff --git a/crates/ai/src/streaming_diff.rs b/crates/ai/src/streaming_diff.rs index 5425b75bbe..7ea7f6dacd 100644 --- a/crates/ai/src/streaming_diff.rs +++ b/crates/ai/src/streaming_diff.rs @@ -83,8 +83,8 @@ pub struct StreamingDiff { impl StreamingDiff { const INSERTION_SCORE: f64 = -1.; const DELETION_SCORE: f64 = -5.; - const EQUALITY_BASE: f64 = 1.4; - const MAX_EQUALITY_EXPONENT: i32 = 64; + const EQUALITY_BASE: f64 = 2.; + const MAX_EQUALITY_EXPONENT: i32 = 20; pub fn new(old: String) -> Self { let old = old.chars().collect::>(); @@ -117,12 +117,8 @@ impl StreamingDiff { equal_run += 1; self.equal_runs.insert((i, j), equal_run); - if self.old[i - 1] == ' ' { - self.scores.get(i - 1, j - 1) - } else { - let exponent = cmp::min(equal_run as i32, Self::MAX_EQUALITY_EXPONENT); - self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) - } + 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 }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 904e77c9f0..0283b396f3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8209,7 +8209,7 @@ impl View for Editor { "Editor" } - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { let focused_event = EditorFocused(cx.handle()); cx.emit(Event::Focused); @@ -8217,7 +8217,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::() { if !self.focused { self.blink_manager.update(cx, BlinkManager::enable); } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 88c66d5200..0990fdbcb7 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -626,7 +626,7 @@ impl MultiBuffer { buffer.merge_transactions(transaction, destination) }); } else { - if let Some(transaction) = self.history.remove_transaction(transaction) { + 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) = @@ -822,6 +822,18 @@ impl MultiBuffer { None } + pub fn undo_and_forget(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, cx| buffer.undo_and_forget(transaction_id, cx)); + } else if let Some(transaction) = self.history.forget(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_and_forget(transaction_id, cx)); + } + } + } + } + pub fn stream_excerpts_with_context_lines( &mut self, excerpts: Vec<(ModelHandle, Vec>)>, @@ -3369,7 +3381,7 @@ impl History { } } - fn remove_transaction(&mut self, transaction_id: TransactionId) -> Option { + fn forget(&mut self, transaction_id: TransactionId) -> Option { if let Some(ix) = self .undo_stack .iter() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e2154f498e..e8bbe29b47 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1664,6 +1664,22 @@ impl Buffer { } } + pub fn undo_and_forget( + &mut self, + transaction_id: TransactionId, + cx: &mut ModelContext, + ) -> bool { + let was_dirty = self.is_dirty(); + let old_version = self.version.clone(); + if let Some(operation) = self.text.undo_and_forget(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, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 8f15535ccf..02f1be718f 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -22,6 +22,7 @@ use postage::{oneshot, prelude::*}; pub use rope::*; pub use selection::*; +use util::ResultExt; use std::{ cmp::{self, Ordering, Reverse}, @@ -1206,6 +1207,14 @@ impl Buffer { } } + pub fn undo_and_forget(&mut self, transaction_id: TransactionId) -> Option { + if let Some(transaction) = self.history.forget(transaction_id) { + self.undo_or_redo(transaction).log_err() + } else { + None + } + } + #[allow(clippy::needless_collect)] pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { let transactions = self diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ebc9591239..02d0de4905 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1124,14 +1124,15 @@ pub struct AssistantStyle { pub api_key_editor: FieldEditor, pub api_key_prompt: ContainedText, pub saved_conversation: SavedConversation, - pub modal: ModalAssistantStyle, + pub inline: InlineAssistantStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ModalAssistantStyle { +pub struct InlineAssistantStyle { #[serde(flatten)] pub container: ContainerStyle, pub editor: FieldEditor, + pub pending_edit_background: Color, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index ac91d1118d..97bb3402b6 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -59,7 +59,7 @@ export default function assistant(): any { background: background(theme.highest), padding: { left: 12 }, }, - modal: { + inline: { border: border(theme.lowest, "on", { top: true, bottom: true, @@ -69,7 +69,8 @@ export default function assistant(): any { text: text(theme.lowest, "mono", "on", { size: "sm" }), placeholder_text: text(theme.lowest, "sans", "on", "disabled"), selection: theme.players[0], - } + }, + pending_edit_background: background(theme.highest, "positive"), }, message_header: { margin: { bottom: 4, top: 4 }, From 66a496edd7e484f4db0076c86c4fdbb5fba4ac6b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 12:16:28 +0200 Subject: [PATCH 043/114] Allow generating code without editing it --- crates/ai/src/assistant.rs | 159 +++++++++++++++++++----------- crates/editor/src/multi_buffer.rs | 10 ++ 2 files changed, 112 insertions(+), 57 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 58be0fe584..4c75506b7a 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -244,40 +244,47 @@ impl AssistantPanel { fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { let id = post_inc(&mut self.next_inline_assist_id); - let (block_id, inline_assistant, selection) = editor.update(cx, |editor, cx| { - let selection = editor.selections.newest_anchor().clone(); - let prompt_editor = cx.add_view(|cx| { - Editor::single_line( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ) - }); - let assist_kind = if editor.selections.newest::(cx).is_empty() { - InlineAssistKind::Insert - } else { - InlineAssistKind::Edit - }; - let assistant = cx.add_view(|_| InlineAssistant { + let selection = editor.read(cx).selections.newest_anchor().clone(); + let assist_kind = if editor.read(cx).selections.newest::(cx).is_empty() { + InlineAssistKind::Insert + } else { + InlineAssistKind::Refactor + }; + let prompt_editor = cx.add_view(|cx| { + Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ) + }); + let inline_assistant = cx.add_view(|cx| { + let assistant = InlineAssistant { id, prompt_editor, confirmed: false, has_focus: false, assist_kind, - }); - cx.focus(&assistant); - - let block_id = editor.insert_blocks( + }; + cx.focus_self(); + assistant + }); + let block_id = editor.update(cx, |editor, cx| { + editor.highlight_background::( + vec![selection.start..selection.end], + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, position: selection.head(), height: 2, render: Arc::new({ - let assistant = assistant.clone(); + let inline_assistant = inline_assistant.clone(); move |cx: &mut BlockContext| { - ChildView::new(&assistant, cx) + ChildView::new(&inline_assistant, cx) .contained() .with_padding_left(match assist_kind { - InlineAssistKind::Edit => cx.gutter_width, + InlineAssistKind::Refactor => cx.gutter_width, InlineAssistKind::Insert => cx.anchor_x, }) .into_any() @@ -291,19 +298,13 @@ impl AssistantPanel { }], Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), cx, - )[0]; - editor.highlight_background::( - vec![selection.start..selection.end], - |theme| theme.assistant.inline.pending_edit_background, - cx, - ); - - (block_id, assistant, selection) + )[0] }); self.pending_inline_assists.insert( id, PendingInlineAssist { + kind: assist_kind, editor: editor.downgrade(), selection, inline_assistant_block_id: Some(block_id), @@ -341,7 +342,7 @@ impl AssistantPanel { let assist_id = inline_assistant.read(cx).id; match event { InlineAssistantEvent::Confirmed { prompt } => { - self.generate_code(assist_id, prompt, cx); + self.generate(assist_id, prompt, cx); } InlineAssistantEvent::Canceled => { self.complete_inline_assist(assist_id, true, cx); @@ -395,12 +396,7 @@ impl AssistantPanel { } } - pub fn generate_code( - &mut self, - inline_assist_id: usize, - user_prompt: &str, - cx: &mut ViewContext, - ) { + fn generate(&mut self, inline_assist_id: usize, user_prompt: &str, cx: &mut ViewContext) { let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { @@ -426,27 +422,32 @@ impl AssistantPanel { .text_for_range(selection.start..selection.end) .collect::(); - let mut normalized_selected_text = selected_text.clone(); let mut base_indentation: Option = None; let selection_start = selection.start.to_point(&snapshot); let selection_end = selection.end.to_point(&snapshot); - if selection_start.row < selection_end.row { - for row in selection_start.row..=selection_end.row { - if snapshot.is_line_blank(row) { - continue; - } - - let line_indentation = snapshot.indent_size_for_line(row); - if let Some(base_indentation) = base_indentation.as_mut() { - if line_indentation.len < base_indentation.len { - *base_indentation = line_indentation; - } - } else { - base_indentation = Some(line_indentation); - } + let mut start_row = selection_start.row; + if snapshot.is_line_blank(start_row) { + if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { + start_row = prev_non_blank_row; } } + for row in start_row..=selection_end.row { + if snapshot.is_line_blank(row) { + continue; + } + + let line_indentation = snapshot.indent_size_for_line(row); + if let Some(base_indentation) = base_indentation.as_mut() { + if line_indentation.len < base_indentation.len { + *base_indentation = line_indentation; + } + } else { + base_indentation = Some(line_indentation); + } + } + + let mut normalized_selected_text = selected_text.clone(); if let Some(base_indentation) = base_indentation { for row in selection_start.row..=selection_end.row { let selection_row = row - selection_start.row; @@ -472,10 +473,53 @@ impl AssistantPanel { let language_name = language_name.as_deref().unwrap_or(""); let mut prompt = String::new(); - writeln!(prompt, "Given the following {language_name} snippet:").unwrap(); - writeln!(prompt, "{normalized_selected_text}").unwrap(); - writeln!(prompt, "{user_prompt}.").unwrap(); - writeln!(prompt, "Never make remarks, reply only with the new code.").unwrap(); + writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); + writeln!( + prompt, + "You're currently working inside an editor on this code:" + ) + .unwrap(); + match pending_assist.kind { + InlineAssistKind::Refactor => { + writeln!(prompt, "```{language_name}").unwrap(); + writeln!(prompt, "{normalized_selected_text}").unwrap(); + writeln!(prompt, "```").unwrap(); + writeln!( + prompt, + "Modify the code given the user prompt: {user_prompt}" + ) + .unwrap(); + } + InlineAssistKind::Insert => { + writeln!(prompt, "```{language_name}").unwrap(); + for chunk in snapshot.text_for_range(Anchor::min()..selection.head()) { + write!(prompt, "{chunk}").unwrap(); + } + write!(prompt, "<|>").unwrap(); + for chunk in snapshot.text_for_range(selection.head()..Anchor::max()) { + write!(prompt, "{chunk}").unwrap(); + } + writeln!(prompt).unwrap(); + writeln!(prompt, "```").unwrap(); + writeln!( + prompt, + "Assume the cursor is located where the `<|>` marker is." + ) + .unwrap(); + writeln!( + prompt, + "Complete the code given the user prompt: {user_prompt}" + ) + .unwrap(); + } + } + writeln!( + prompt, + "You MUST not return anything that isn't valid {language_name}" + ) + .unwrap(); + writeln!(prompt, "DO NOT wrap your response in Markdown blocks.").unwrap(); + let request = OpenAIRequest { model: "gpt-4".into(), messages: vec![RequestMessage { @@ -2589,7 +2633,7 @@ enum InlineAssistantEvent { #[derive(Copy, Clone)] enum InlineAssistKind { - Edit, + Refactor, Insert, } @@ -2614,7 +2658,7 @@ impl View for InlineAssistant { let theme = theme::current(cx); let prompt_editor = ChildView::new(&self.prompt_editor, cx).aligned().left(); match self.assist_kind { - InlineAssistKind::Edit => prompt_editor + InlineAssistKind::Refactor => prompt_editor .contained() .with_style(theme.assistant.inline.container) .into_any(), @@ -2651,6 +2695,7 @@ impl InlineAssistant { } struct PendingInlineAssist { + kind: InlineAssistKind, editor: WeakViewHandle, selection: Selection, inline_assistant_block_id: Option, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 0990fdbcb7..ac3b726b26 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2352,6 +2352,16 @@ impl MultiBufferSnapshot { } } + pub fn prev_non_blank_row(&self, mut row: u32) -> Option { + 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 From 144f5c5d41a5c6d02bc5f0a2066aca6dec36f91c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 12:25:43 +0200 Subject: [PATCH 044/114] Use a left bias for the prompt editor --- crates/ai/src/assistant.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4c75506b7a..e68d3ef7b8 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -276,7 +276,9 @@ impl AssistantPanel { editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, - position: selection.head(), + position: selection + .head() + .bias_left(&editor.buffer().read(cx).snapshot(cx)), height: 2, render: Arc::new({ let inline_assistant = inline_assistant.clone(); @@ -506,6 +508,7 @@ impl AssistantPanel { "Assume the cursor is located where the `<|>` marker is." ) .unwrap(); + writeln!(prompt, "Assume your answer will be inserted at the cursor.").unwrap(); writeln!( prompt, "Complete the code given the user prompt: {user_prompt}" @@ -513,12 +516,9 @@ impl AssistantPanel { .unwrap(); } } - writeln!( - prompt, - "You MUST not return anything that isn't valid {language_name}" - ) - .unwrap(); + writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); writeln!(prompt, "DO NOT wrap your response in Markdown blocks.").unwrap(); + writeln!(prompt, "Never make remarks, always output code.").unwrap(); let request = OpenAIRequest { model: "gpt-4".into(), From 971c833e8008c6a488b10a0e59ba8c62f478ecd6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 12:35:36 +0200 Subject: [PATCH 045/114] Improve background highlighting of inline assists --- crates/ai/src/assistant.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index e68d3ef7b8..9311a00751 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -26,7 +26,7 @@ use gpui::{ Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use language::{ - language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, Selection, ToOffset as _, + language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _, TransactionId, }; use search::BufferSearchBar; @@ -244,7 +244,9 @@ impl AssistantPanel { fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { let id = post_inc(&mut self.next_inline_assist_id); + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selection = editor.read(cx).selections.newest_anchor().clone(); + let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); let assist_kind = if editor.read(cx).selections.newest::(cx).is_empty() { InlineAssistKind::Insert } else { @@ -269,16 +271,14 @@ impl AssistantPanel { }); let block_id = editor.update(cx, |editor, cx| { editor.highlight_background::( - vec![selection.start..selection.end], + vec![range.clone()], |theme| theme.assistant.inline.pending_edit_background, cx, ); editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, - position: selection - .head() - .bias_left(&editor.buffer().read(cx).snapshot(cx)), + position: selection.head().bias_left(&snapshot), height: 2, render: Arc::new({ let inline_assistant = inline_assistant.clone(); @@ -308,7 +308,7 @@ impl AssistantPanel { PendingInlineAssist { kind: assist_kind, editor: editor.downgrade(), - selection, + range, inline_assistant_block_id: Some(block_id), code_generation: Task::ready(None), transaction_id: None, @@ -418,15 +418,15 @@ impl AssistantPanel { return; }; - let selection = pending_assist.selection.clone(); + let range = pending_assist.range.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selected_text = snapshot - .text_for_range(selection.start..selection.end) + .text_for_range(range.start..range.end) .collect::(); let mut base_indentation: Option = None; - let selection_start = selection.start.to_point(&snapshot); - let selection_end = selection.end.to_point(&snapshot); + let selection_start = range.start.to_point(&snapshot); + let selection_end = range.end.to_point(&snapshot); let mut start_row = selection_start.row; if snapshot.is_line_blank(start_row) { if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { @@ -470,7 +470,7 @@ impl AssistantPanel { } let language_name = snapshot - .language_at(selection.start) + .language_at(range.start) .map(|language| language.name()); let language_name = language_name.as_deref().unwrap_or(""); @@ -494,11 +494,11 @@ impl AssistantPanel { } InlineAssistKind::Insert => { writeln!(prompt, "```{language_name}").unwrap(); - for chunk in snapshot.text_for_range(Anchor::min()..selection.head()) { + for chunk in snapshot.text_for_range(Anchor::min()..range.start) { write!(prompt, "{chunk}").unwrap(); } write!(prompt, "<|>").unwrap(); - for chunk in snapshot.text_for_range(selection.head()..Anchor::max()) { + for chunk in snapshot.text_for_range(range.start..Anchor::max()) { write!(prompt, "{chunk}").unwrap(); } writeln!(prompt).unwrap(); @@ -543,7 +543,7 @@ impl AssistantPanel { } }); - let mut edit_start = selection.start.to_offset(&snapshot); + let mut edit_start = range.start.to_offset(&snapshot); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { @@ -2697,7 +2697,7 @@ impl InlineAssistant { struct PendingInlineAssist { kind: InlineAssistKind, editor: WeakViewHandle, - selection: Selection, + range: Range, inline_assistant_block_id: Option, code_generation: Task>, transaction_id: Option, From 0444b5a7757abce9dab6cbdf218014cc07e72c4a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 13:36:52 +0200 Subject: [PATCH 046/114] :lipstick: --- crates/ai/src/assistant.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 9311a00751..b8458bb9ac 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -214,12 +214,12 @@ impl AssistantPanel { } fn inline_assist(workspace: &mut Workspace, _: &InlineAssist, cx: &mut ViewContext) { - let assistant = if let Some(assistant) = workspace.panel::(cx) { - if assistant + let this = if let Some(this) = workspace.panel::(cx) { + if this .update(cx, |assistant, cx| assistant.load_api_key(cx)) .is_some() { - assistant + this } else { workspace.focus_panel::(cx); return; @@ -237,7 +237,7 @@ impl AssistantPanel { return; }; - assistant.update(cx, |assistant, cx| { + this.update(cx, |assistant, cx| { assistant.new_inline_assist(&active_editor, cx) }); } @@ -1046,7 +1046,7 @@ impl AssistantPanel { .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) } - pub fn load_api_key(&mut self, cx: &mut ViewContext) -> Option { + fn load_api_key(&mut self, cx: &mut ViewContext) -> Option { if self.api_key.borrow().is_none() && !self.has_read_credentials { self.has_read_credentials = true; let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { From fdbf4680bb4b68d6eb760022e4e7560252fda5ee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 15:38:38 +0200 Subject: [PATCH 047/114] Ensure the inline assistant works with gpt-3.5 --- crates/ai/src/assistant.rs | 137 ++++++++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 19 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index cfe3da22b5..4405712afe 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -344,7 +344,7 @@ impl AssistantPanel { let assist_id = inline_assistant.read(cx).id; match event { InlineAssistantEvent::Confirmed { prompt } => { - self.generate(assist_id, prompt, cx); + self.confirm_inline_assist(assist_id, prompt, cx); } InlineAssistantEvent::Canceled => { self.complete_inline_assist(assist_id, true, cx); @@ -398,7 +398,12 @@ impl AssistantPanel { } } - fn generate(&mut self, inline_assist_id: usize, user_prompt: &str, cx: &mut ViewContext) { + fn confirm_inline_assist( + &mut self, + inline_assist_id: usize, + user_prompt: &str, + cx: &mut ViewContext, + ) { let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { @@ -473,26 +478,51 @@ impl AssistantPanel { .language_at(range.start) .map(|language| language.name()); let language_name = language_name.as_deref().unwrap_or(""); + let model = settings::get::(cx) + .default_open_ai_model + .clone(); let mut prompt = String::new(); writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); - writeln!( - prompt, - "You're currently working inside an editor on this code:" - ) - .unwrap(); match pending_assist.kind { InlineAssistKind::Refactor => { + writeln!( + prompt, + "You're currently working inside an editor on this code:" + ) + .unwrap(); + writeln!(prompt, "```{language_name}").unwrap(); + for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { + write!(prompt, "{chunk}").unwrap(); + } + writeln!(prompt, "```").unwrap(); + + writeln!( + prompt, + "In particular, the user has selected the following code:" + ) + .unwrap(); writeln!(prompt, "```{language_name}").unwrap(); writeln!(prompt, "{normalized_selected_text}").unwrap(); writeln!(prompt, "```").unwrap(); + writeln!(prompt).unwrap(); writeln!( prompt, - "Modify the code given the user prompt: {user_prompt}" + "Modify the selected code given the user prompt: {user_prompt}" + ) + .unwrap(); + writeln!( + prompt, + "You MUST reply only with the edited selected code, not the entire file." ) .unwrap(); } InlineAssistKind::Insert => { + writeln!( + prompt, + "You're currently working inside an editor on this code:" + ) + .unwrap(); writeln!(prompt, "```{language_name}").unwrap(); for chunk in snapshot.text_for_range(Anchor::min()..range.start) { write!(prompt, "{chunk}").unwrap(); @@ -517,11 +547,11 @@ impl AssistantPanel { } } writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); - writeln!(prompt, "DO NOT wrap your response in Markdown blocks.").unwrap(); + writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); writeln!(prompt, "Never make remarks, always output code.").unwrap(); let request = OpenAIRequest { - model: "gpt-4".into(), + model: model.full_name().into(), messages: vec![RequestMessage { role: Role::User, content: prompt, @@ -563,23 +593,92 @@ impl AssistantPanel { indentation_text = ""; }; - let mut new_text = indentation_text - .repeat(indentation_len.saturating_sub(selection_start.column) as usize); + let mut inside_first_line = true; + let mut starts_with_fenced_code_block = None; + let mut has_pending_newline = false; + let mut new_text = String::new(); + while let Some(message) = messages.next().await { let mut message = message?; - if let Some(choice) = message.choices.pop() { + if let Some(mut choice) = message.choices.pop() { + if has_pending_newline { + has_pending_newline = false; + choice + .delta + .content + .get_or_insert(String::new()) + .insert(0, '\n'); + } + + // Buffer a trailing codeblock fence. Note that we don't stop + // right away because this may be an inner fence that we need + // to insert into the editor. + if starts_with_fenced_code_block.is_some() + && choice.delta.content.as_deref() == Some("\n```") + { + new_text.push_str("\n```"); + continue; + } + + // If this was the last completion and we started with a codeblock + // fence and we ended with another codeblock fence, then we can + // stop right away. Otherwise, whatever text we buffered will be + // processed normally. + if choice.finish_reason.is_some() + && starts_with_fenced_code_block.unwrap_or(false) + && new_text == "\n```" + { + break; + } + if let Some(text) = choice.delta.content { + // Never push a newline if there's nothing after it. This is + // useful to detect if the newline was pushed because of a + // trailing codeblock fence. + let text = if let Some(prefix) = text.strip_suffix('\n') { + has_pending_newline = true; + prefix + } else { + text.as_str() + }; + + if text.is_empty() { + continue; + } + let mut lines = text.split('\n'); - if let Some(first_line) = lines.next() { - new_text.push_str(&first_line); + if let Some(line) = lines.next() { + if starts_with_fenced_code_block.is_none() { + starts_with_fenced_code_block = + Some(line.starts_with("```")); + } + + // Avoid pushing the first line if it's the start of a fenced code block. + if !inside_first_line || !starts_with_fenced_code_block.unwrap() + { + new_text.push_str(&line); + } } for line in lines { - new_text.push('\n'); - new_text.push_str( - &indentation_text.repeat(indentation_len as usize), - ); + if inside_first_line && starts_with_fenced_code_block.unwrap() { + // If we were inside the first line and that line was the + // start of a fenced code block, we just need to push the + // leading indentation of the original selection. + new_text.push_str(&indentation_text.repeat( + indentation_len.saturating_sub(selection_start.column) + as usize, + )); + } else { + // Otherwise, we need to push a newline and the base indentation. + new_text.push('\n'); + new_text.push_str( + &indentation_text.repeat(indentation_len as usize), + ); + } + new_text.push_str(line); + inside_first_line = false; } } } From b101a7edffe582a218a07ec93b55afa052ae3222 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 15:54:52 +0200 Subject: [PATCH 048/114] Cancel last inline assist when escaping from the editor --- crates/ai/src/assistant.rs | 66 ++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4405712afe..a1de7bf736 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -7,7 +7,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use collections::{HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet}; use editor::{ display_map::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, @@ -93,6 +93,7 @@ pub fn init(cx: &mut AppContext) { }, ); cx.add_action(AssistantPanel::inline_assist); + cx.add_action(AssistantPanel::cancel_last_inline_assist); cx.add_action(InlineAssistant::confirm); cx.add_action(InlineAssistant::cancel); } @@ -347,25 +348,64 @@ impl AssistantPanel { self.confirm_inline_assist(assist_id, prompt, cx); } InlineAssistantEvent::Canceled => { - self.complete_inline_assist(assist_id, true, cx); + self.close_inline_assist(assist_id, true, cx); } InlineAssistantEvent::Dismissed => { - self.dismiss_inline_assist(assist_id, cx); + self.hide_inline_assist(assist_id, cx); } } } - fn complete_inline_assist( - &mut self, - assist_id: usize, - cancel: bool, - cx: &mut ViewContext, + fn cancel_last_inline_assist( + workspace: &mut Workspace, + _: &editor::Cancel, + cx: &mut ViewContext, ) { - self.dismiss_inline_assist(assist_id, cx); + let panel = if let Some(panel) = workspace.panel::(cx) { + panel + } else { + return; + }; + let editor = if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + editor + } else { + return; + }; + + let handled = panel.update(cx, |panel, cx| { + if let Some(assist_id) = panel + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .and_then(|assist_ids| assist_ids.last().copied()) + { + panel.close_inline_assist(assist_id, true, cx); + true + } else { + false + } + }); + + if !handled { + cx.propagate_action(); + } + } + + fn close_inline_assist(&mut self, assist_id: usize, cancel: bool, cx: &mut ViewContext) { + self.hide_inline_assist(assist_id, cx); if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { - self.pending_inline_assist_ids_by_editor - .remove(&pending_assist.editor); + if let hash_map::Entry::Occupied(mut entry) = self + .pending_inline_assist_ids_by_editor + .entry(pending_assist.editor) + { + entry.get_mut().retain(|id| *id != assist_id); + if entry.get().is_empty() { + entry.remove(); + } + } if let Some(editor) = pending_assist.editor.upgrade(cx) { editor.update(cx, |editor, cx| { @@ -386,7 +426,7 @@ impl AssistantPanel { } } - fn dismiss_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { + fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { if let Some(editor) = pending_assist.editor.upgrade(cx) { if let Some(block_id) = pending_assist.inline_assistant_block_id.take() { @@ -568,7 +608,7 @@ impl AssistantPanel { let this = this.clone(); move || { let _ = this.update(&mut cx, |this, cx| { - this.complete_inline_assist(inline_assist_id, false, cx) + this.close_inline_assist(inline_assist_id, false, cx) }); } }); From 75a6a94e96a6bfcff40747449f1712bd1f55a2bc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 17:24:26 +0200 Subject: [PATCH 049/114] Add placeholder text for inline assistant prompts --- crates/ai/src/assistant.rs | 43 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index a1de7bf736..326dbfc046 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -249,15 +249,21 @@ impl AssistantPanel { let selection = editor.read(cx).selections.newest_anchor().clone(); let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); let assist_kind = if editor.read(cx).selections.newest::(cx).is_empty() { - InlineAssistKind::Insert + InlineAssistKind::Generate } else { - InlineAssistKind::Refactor + InlineAssistKind::Transform }; let prompt_editor = cx.add_view(|cx| { - Editor::single_line( + let mut editor = Editor::single_line( Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), cx, - ) + ); + let placeholder = match assist_kind { + InlineAssistKind::Transform => "Enter transformation prompt…", + InlineAssistKind::Generate => "Enter generation prompt…", + }; + editor.set_placeholder_text(placeholder, cx); + editor }); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant { @@ -284,12 +290,12 @@ impl AssistantPanel { render: Arc::new({ let inline_assistant = inline_assistant.clone(); move |cx: &mut BlockContext| { + let theme = theme::current(cx); ChildView::new(&inline_assistant, cx) .contained() - .with_padding_left(match assist_kind { - InlineAssistKind::Refactor => cx.gutter_width, - InlineAssistKind::Insert => cx.anchor_x, - }) + .with_padding_left(cx.anchor_x) + .contained() + .with_style(theme.assistant.inline.container) .into_any() } }), @@ -525,7 +531,7 @@ impl AssistantPanel { let mut prompt = String::new(); writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); match pending_assist.kind { - InlineAssistKind::Refactor => { + InlineAssistKind::Transform => { writeln!( prompt, "You're currently working inside an editor on this code:" @@ -557,7 +563,7 @@ impl AssistantPanel { ) .unwrap(); } - InlineAssistKind::Insert => { + InlineAssistKind::Generate => { writeln!( prompt, "You're currently working inside an editor on this code:" @@ -2775,8 +2781,8 @@ enum InlineAssistantEvent { #[derive(Copy, Clone)] enum InlineAssistKind { - Refactor, - Insert, + Transform, + Generate, } struct InlineAssistant { @@ -2797,15 +2803,10 @@ impl View for InlineAssistant { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - let prompt_editor = ChildView::new(&self.prompt_editor, cx).aligned().left(); - match self.assist_kind { - InlineAssistKind::Refactor => prompt_editor - .contained() - .with_style(theme.assistant.inline.container) - .into_any(), - InlineAssistKind::Insert => prompt_editor.into_any(), - } + ChildView::new(&self.prompt_editor, cx) + .aligned() + .left() + .into_any() } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { From c4966ff57a24e8d7cdecbaa08cfccc6aa2109f0f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 17:35:14 +0200 Subject: [PATCH 050/114] Remove warning --- crates/ai/src/assistant.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 326dbfc046..65b255d458 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -271,7 +271,6 @@ impl AssistantPanel { prompt_editor, confirmed: false, has_focus: false, - assist_kind, }; cx.focus_self(); assistant @@ -2789,7 +2788,6 @@ struct InlineAssistant { id: usize, prompt_editor: ViewHandle, confirmed: bool, - assist_kind: InlineAssistKind, has_focus: bool, } From 7c5200e757feaec01cdbef34d8b2ad8d528c728f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Aug 2023 17:51:13 +0200 Subject: [PATCH 051/114] More styling --- styles/src/style_tree/assistant.ts | 3 +- todo.md | 45 +++++++++++------------------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 97bb3402b6..bdca8a16e5 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -60,13 +60,14 @@ export default function assistant(): any { padding: { left: 12 }, }, inline: { + margin: { top: 3, bottom: 3 }, border: border(theme.lowest, "on", { top: true, bottom: true, overlay: true, }), editor: { - text: text(theme.lowest, "mono", "on", { size: "sm" }), + text: text(theme.highest, "mono", "default", { size: "sm" }), placeholder_text: text(theme.lowest, "sans", "on", "disabled"), selection: theme.players[0], }, diff --git a/todo.md b/todo.md index e07d19bc95..8a9a8b5b3d 100644 --- a/todo.md +++ b/todo.md @@ -1,36 +1,25 @@ -- Style the current inline editor -- Find a way to understand whether we want to refactor or append, or both. (function calls) -- Add a system prompt that makes GPT an expert of language X -- Provide context around the cursor/selection. We should try to fill the context window as much as possible (try to fill half of it so that we can spit out another half) -- When you hit escape, the assistant should stop. -- When you hit undo and you undo a transaction from the assistant, we should stop generating. -- Keep the inline editor around until the assistant is done. Add a cancel button to stop, and and undo button to undo the whole thing. (Interactive) - - -# 9:39 AM - -- Hit `ctrl-enter` - -- Puts me in assistant mode with the selected text highlighted in a special color. If text was selected, I'm in transformation mode. -- If there's no selection, put me on the line below, aligned with the indent of the line. -- Enter starts generation -- Ctrl-enter inserts a newline -- Once generations starts, enter "confirms it" by dismissing the inline editor. -- Escape in the inline editor cancels/undoes/dismisses. -- To generate text in reference to other text, we can *mark* text. - - - Hit ctrl-enter deploys an edit prompt - Empty selection (cursor) => append text - On end of line: Edit prompt on end of line. - - Middle of line: Edit prompt near cursor head on a different line + - [x] Middle of line: Edit prompt near cursor head on a different line - Non-empty selection => refactor - - Edit prompt near cursor head on a different line - - What was selected when you hit ctrl-enter is colored. -- Selection is cleared and cursor is moved to prompt input + - [x] Edit prompt near cursor head on a different line + - [x] What was selected when you hit ctrl-enter is colored. +- [x] Add placeholder text + - If non-empty selection: Enter prompt to transform selected text + - If empty selection: Enter prompt to generate text - When cursor is inside a prompt - - Escape cancels/undoes - - Enter confirms + - [x] Escape cancels/undoes + - [x] Enter confirms +- [ ] Selection is cleared and cursor is moved to prompt input +- [ ] Ability to highlight background multiple times for the same type +- [x] Basic Styling +- [ ] Match lowest indentation level of selected lines when inserting an inline assist +- [ ] Look into why insert prompts have a weird indentation sometimes + + + + - Multicursor - Run the same prompt for every selection in parallel - Position the prompt editor at the newest cursor From fc457d45f5f529fa4d0ade52c48b62516baa388c Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 25 Aug 2023 18:46:30 -0400 Subject: [PATCH 052/114] Add `word_characters` to language overrides & use for more things Use word_characters to feed completion trigger characters as well and also recognize kebab as a potential sub-word splitter. This is fine for non-kebab-case languages because we'd only ever attempt to split a word with a kebab in it in language scopes which are kebab-cased Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 2 + crates/editor/src/editor_tests.rs | 96 ++++++++++++++++++- crates/editor/src/movement.rs | 22 ++--- crates/editor/src/multi_buffer.rs | 10 +- .../src/test/editor_lsp_test_context.rs | 2 +- crates/language/src/buffer.rs | 16 ++-- crates/language/src/language.rs | 9 ++ .../LiveKitBridge/Package.resolved | 4 +- crates/project/src/search.rs | 8 +- crates/vim/src/motion.rs | 22 ++--- crates/vim/src/normal/change.rs | 10 +- crates/vim/src/object.rs | 30 +++--- .../zed/src/languages/javascript/config.toml | 3 + 13 files changed, 178 insertions(+), 56 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9e24e56efe..bfa804c56c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2654,6 +2654,7 @@ impl Editor { false }); } + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { let offset = position.to_offset(buffer); let (word_range, kind) = buffer.surrounding_word(offset); @@ -8878,6 +8879,7 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "bg-blue".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-red".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-yellow".into(), + ..Default::default() + }, + ]))) + }); + + cx.set_state(r#"

"#); + + // Trigger completion when typing a dash, because the dash is an extra + // word character in the 'element' scope, which contains the cursor. + cx.simulate_keystroke("-"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-red", "bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + // When filtering completions, consider the character after the '-' to + // be the start of a subword. + cx.set_state(r#"

"#); + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 5917b8b3bd..d55b2a464f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -177,20 +177,20 @@ pub fn line_end( pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_preceding_boundary(map, point, |left, right| { - (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) + (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace()) || left == '\n' }) } pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_preceding_boundary(map, point, |left, right| { let is_word_start = - char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); + char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace(); let is_subword_start = left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start || left == '\n' @@ -199,19 +199,19 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_boundary(map, point, |left, right| { - (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) + (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace()) || right == '\n' }) } pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_boundary(map, point, |left, right| { let is_word_end = - (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); + (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace(); let is_subword_end = left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_end || is_subword_end || right == '\n' @@ -399,14 +399,14 @@ pub fn find_boundary_in_line( pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; - let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c)); + let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c)); let prev_char_kind = text .reversed_chars_at(ix) .next() - .map(|c| char_kind(language, c)); + .map(|c| char_kind(&scope, c)); prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d4061f25dc..99dcbd189c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1360,11 +1360,13 @@ impl MultiBuffer { return false; } - if char.is_alphanumeric() || char == '_' { + let snapshot = self.snapshot(cx); + let position = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(position); + if char_kind(&scope, char) == CharKind::Word { return true; } - let snapshot = self.snapshot(cx); let anchor = snapshot.anchor_before(position); anchor .buffer_id @@ -1866,8 +1868,8 @@ impl MultiBufferSnapshot { let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); - let language = self.language_at(start); - let kind = |c| char_kind(language, c); + let scope = self.language_scope_at(start); + let kind = |c| char_kind(&scope, c); let word_kind = cmp::max( prev_chars.peek().copied().map(kind), next_chars.peek().copied().map(kind), diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 83aaa3b703..a2a7d71dce 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -50,7 +50,7 @@ impl<'a> EditorLspTestContext<'a> { language .path_suffixes() .first() - .unwrap_or(&"txt".to_string()) + .expect("language must have a path suffix for EditorLspTestContext") ); let mut fake_servers = language diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index eff95460c4..44ee870797 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2175,8 +2175,8 @@ impl BufferSnapshot { let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); - let language = self.language_at(start); - let kind = |c| char_kind(language, c); + let scope = self.language_scope_at(start); + let kind = |c| char_kind(&scope, c); let word_kind = cmp::max( prev_chars.peek().copied().map(kind), next_chars.peek().copied().map(kind), @@ -2988,17 +2988,21 @@ pub fn contiguous_ranges( }) } -pub fn char_kind(language: Option<&Arc>, c: char) -> CharKind { +pub fn char_kind(scope: &Option, c: char) -> CharKind { if c.is_whitespace() { return CharKind::Whitespace; } else if c.is_alphanumeric() || c == '_' { return CharKind::Word; } - if let Some(language) = language { - if language.config.word_characters.contains(&c) { - return CharKind::Word; + + if let Some(scope) = scope { + if let Some(characters) = scope.word_characters() { + if characters.contains(&c) { + return CharKind::Word; + } } } + CharKind::Punctuation } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 58732355a5..ccc1937032 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -370,6 +370,8 @@ pub struct LanguageConfigOverride { pub block_comment: Override<(Arc, Arc)>, #[serde(skip_deserializing)] pub disabled_bracket_ixs: Vec, + #[serde(default)] + pub word_characters: Override>, } #[derive(Clone, Deserialize, Debug)] @@ -1557,6 +1559,13 @@ impl LanguageScope { .map(|e| (&e.0, &e.1)) } + pub fn word_characters(&self) -> Option<&HashSet> { + Override::as_option( + self.config_override().map(|o| &o.word_characters), + Some(&self.language.config.word_characters), + ) + } + pub fn brackets(&self) -> impl Iterator { let mut disabled_ids = self .config_override() diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index 85ae088565..b925bc8f0d 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e", - "version": "1.21.0" + "revision": "ce20dc083ee485524b802669890291c0d8090170", + "version": "1.22.1" } } ] diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index bfa34c0422..c0ba5a609e 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -204,15 +204,14 @@ impl SearchQuery { if self.as_str().is_empty() { return Default::default(); } - let language = buffer.language_at(0); + + let range_offset = subrange.as_ref().map(|r| r.start).unwrap_or(0); let rope = if let Some(range) = subrange { buffer.as_rope().slice(range) } else { buffer.as_rope().clone() }; - let kind = |c| char_kind(language, c); - let mut matches = Vec::new(); match self { Self::Text { @@ -228,6 +227,9 @@ impl SearchQuery { let mat = mat.unwrap(); if *whole_word { + let scope = buffer.language_scope_at(range_offset + mat.start()); + let kind = |c| char_kind(&scope, c); + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind); let start_kind = kind(rope.chars_at(mat.start()).next().unwrap()); let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap()); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 1defee70da..243653680b 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -439,12 +439,12 @@ pub(crate) fn next_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { let mut crossed_newline = false; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); let at_newline = right == '\n'; let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) @@ -464,12 +464,12 @@ fn next_word_end( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { *point.column_mut() += 1; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); @@ -495,13 +495,13 @@ fn previous_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { // This works even though find_preceding_boundary is called for every character in the line containing // cursor because the newline is checked only once. point = movement::find_preceding_boundary(map, point, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); (left_kind != right_kind && !right.is_whitespace()) || left == '\n' }); @@ -511,7 +511,7 @@ fn previous_word_start( fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { let mut last_point = DisplayPoint::new(from.row(), 0); - let language = map.buffer_snapshot.language_at(from.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { return from; @@ -519,7 +519,7 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi last_point = point; - if char_kind(language, ch) != CharKind::Whitespace { + if char_kind(&scope, ch) != CharKind::Whitespace { break; } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 50bc049a3a..1c9aa48951 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -82,19 +82,19 @@ fn expand_changed_word_selection( ignore_punctuation: bool, ) -> bool { if times.is_none() || times.unwrap() == 1 { - let language = map + let scope = map .buffer_snapshot - .language_at(selection.start.to_point(map)); + .language_scope_at(selection.start.to_point(map)); let in_word = map .chars_at(selection.head()) .next() - .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace) .unwrap_or_default(); if in_word { selection.end = movement::find_boundary(map, selection.end, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index d0bcad36c2..a6cc91d7bd 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -122,18 +122,20 @@ fn in_word( ignore_punctuation: bool, ) -> Option> { // Use motion::right so that we consider the character under the cursor when looking for the start - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }, ); let end = movement::find_boundary_in_line(map, relative_to, |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }); Some(start..end) @@ -156,11 +158,13 @@ fn around_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); let in_word = map .chars_at(relative_to) .next() - .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace) .unwrap_or(false); if in_word { @@ -184,21 +188,23 @@ fn around_next_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); // Get the start of the word let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }, ); let mut word_found = false; let end = movement::find_boundary(map, relative_to, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index c23ddcd6e7..7f6c6931e4 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -17,3 +17,6 @@ brackets = [ [overrides.element] line_comment = { remove = true } block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] From c8e5c3963b89187ff2997395bad60f6dba5e7b10 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Aug 2023 09:51:47 +0200 Subject: [PATCH 053/114] Clear selection when deploying inline assistant --- crates/ai/src/assistant.rs | 3 +++ todo.md | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 65b255d458..b99d4b4fac 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -276,6 +276,9 @@ impl AssistantPanel { assistant }); let block_id = editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_anchor_ranges([selection.head()..selection.head()]) + }); editor.highlight_background::( vec![range.clone()], |theme| theme.assistant.inline.pending_edit_background, diff --git a/todo.md b/todo.md index 8a9a8b5b3d..71ca5a7c7b 100644 --- a/todo.md +++ b/todo.md @@ -14,12 +14,8 @@ - [ ] Selection is cleared and cursor is moved to prompt input - [ ] Ability to highlight background multiple times for the same type - [x] Basic Styling -- [ ] Match lowest indentation level of selected lines when inserting an inline assist - [ ] Look into why insert prompts have a weird indentation sometimes - - - - Multicursor - Run the same prompt for every selection in parallel - Position the prompt editor at the newest cursor From 658d616b9661f086681b25e33b5e76620fd18b1d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Aug 2023 11:55:03 +0200 Subject: [PATCH 054/114] Allow multiple inline assistant highlights at once --- crates/ai/src/assistant.rs | 216 +++++++++++++++++++++++------------- crates/editor/src/editor.rs | 1 + todo.md | 4 +- 3 files changed, 143 insertions(+), 78 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index b99d4b4fac..c5bf027fcc 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -13,13 +13,14 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, ToOffset, ToPoint, + Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ actions, elements::*, + fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton}, Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, @@ -279,11 +280,6 @@ impl AssistantPanel { editor.change_selections(None, cx, |selections| { selections.select_anchor_ranges([selection.head()..selection.head()]) }); - editor.highlight_background::( - vec![range.clone()], - |theme| theme.assistant.inline.pending_edit_background, - cx, - ); editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, @@ -318,6 +314,7 @@ impl AssistantPanel { kind: assist_kind, editor: editor.downgrade(), range, + highlighted_ranges: Default::default(), inline_assistant_block_id: Some(block_id), code_generation: Task::ready(None), transaction_id: None, @@ -342,6 +339,7 @@ impl AssistantPanel { .entry(editor.downgrade()) .or_default() .push(id); + self.update_highlights_for_editor(&editor, cx); } fn handle_inline_assistant_event( @@ -416,10 +414,7 @@ impl AssistantPanel { } if let Some(editor) = pending_assist.editor.upgrade(cx) { - editor.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx); - editor.clear_text_highlights::(cx); - }); + self.update_highlights_for_editor(&editor, cx); if cancel { if let Some(transaction_id) = pending_assist.transaction_id { @@ -741,78 +736,75 @@ impl AssistantPanel { }); while let Some(hunks) = hunks_rx.next().await { - let this = this + let editor = editor .upgrade(&cx) - .ok_or_else(|| anyhow!("assistant was dropped"))?; - editor.update(&mut cx, |editor, cx| { - let mut highlights = Vec::new(); + .ok_or_else(|| anyhow!("editor was dropped"))?; - let transaction = editor.buffer().update(cx, |buffer, cx| { - // Avoid grouping assistant edits with user edits. - buffer.finalize_last_transaction(cx); + this.update(&mut cx, |this, cx| { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + pending_assist + } else { + return; + }; - buffer.start_transaction(cx); - buffer.edit( - hunks.into_iter().filter_map(|hunk| match hunk { - Hunk::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - Some((edit_start..edit_start, text)) - } - Hunk::Remove { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start = edit_end; - Some((edit_range, String::new())) - } - Hunk::Keep { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start += len; - highlights.push(edit_range); - None - } - }), - None, - cx, - ); + pending_assist.highlighted_ranges.clear(); + editor.update(cx, |editor, cx| { + let transaction = editor.buffer().update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); - buffer.end_transaction(cx) + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start += len; + pending_assist.highlighted_ranges.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + if let Some(first_transaction) = pending_assist.transaction_id { + // Group all assistant edits into the first transaction. + editor.buffer().update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + pending_assist.transaction_id = Some(transaction); + editor.buffer().update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } }); - if let Some(transaction) = transaction { - this.update(cx, |this, cx| { - if let Some(pending_assist) = - this.pending_inline_assists.get_mut(&inline_assist_id) - { - if let Some(first_transaction) = pending_assist.transaction_id { - // Group all assistant edits into the first transaction. - editor.buffer().update(cx, |buffer, cx| { - buffer.merge_transactions( - transaction, - first_transaction, - cx, - ) - }); - } else { - pending_assist.transaction_id = Some(transaction); - editor.buffer().update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx) - }); - } - } - }); - } - - editor.highlight_text::( - highlights, - gpui::fonts::HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); + this.update_highlights_for_editor(&editor, cx); })?; } diff.await?; @@ -823,6 +815,55 @@ impl AssistantPanel { }); } + fn update_highlights_for_editor( + &self, + editor: &ViewHandle, + cx: &mut ViewContext, + ) { + let mut background_ranges = Vec::new(); + let mut foreground_ranges = Vec::new(); + let empty_inline_assist_ids = Vec::new(); + let inline_assist_ids = self + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .unwrap_or(&empty_inline_assist_ids); + + for inline_assist_id in inline_assist_ids { + if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { + background_ranges.push(pending_assist.range.clone()); + foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned()); + } + } + + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + merge_ranges(&mut background_ranges, &snapshot); + merge_ranges(&mut foreground_ranges, &snapshot); + editor.update(cx, |editor, cx| { + if background_ranges.is_empty() { + editor.clear_background_highlights::(cx); + } else { + editor.highlight_background::( + background_ranges, + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + } + + if foreground_ranges.is_empty() { + editor.clear_text_highlights::(cx); + } else { + editor.highlight_text::( + foreground_ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + } + }); + } + fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { let editor = cx.add_view(|cx| { ConversationEditor::new( @@ -2842,12 +2883,35 @@ struct PendingInlineAssist { kind: InlineAssistKind, editor: WeakViewHandle, range: Range, + highlighted_ranges: Vec>, inline_assistant_block_id: Option, code_generation: Task>, transaction_id: Option, _subscriptions: Vec, } +fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { + ranges.sort_unstable_by(|a, b| { + a.start + .cmp(&b.start, buffer) + .then_with(|| b.end.cmp(&a.end, buffer)) + }); + + let mut ix = 0; + while ix + 1 < ranges.len() { + let b = ranges[ix + 1].clone(); + let a = &mut ranges[ix]; + if a.end.cmp(&b.start, buffer).is_gt() { + if a.end.cmp(&b.end, buffer).is_lt() { + a.end = b.end; + } + ranges.remove(ix + 1); + } else { + ix += 1; + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b206f2ec8b..d5141a7f7d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7592,6 +7592,7 @@ impl Editor { } results } + pub fn background_highlights_in_range_for( &self, search_range: Range, diff --git a/todo.md b/todo.md index 71ca5a7c7b..0507708502 100644 --- a/todo.md +++ b/todo.md @@ -11,8 +11,8 @@ - When cursor is inside a prompt - [x] Escape cancels/undoes - [x] Enter confirms -- [ ] Selection is cleared and cursor is moved to prompt input -- [ ] Ability to highlight background multiple times for the same type +- [x] Selection is cleared and cursor is moved to prompt input +- [x] Ability to highlight background multiple times for the same type - [x] Basic Styling - [ ] Look into why insert prompts have a weird indentation sometimes From 55bf45d2657278da8c1a4d70ae4baca2099b510d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Aug 2023 12:07:03 +0200 Subject: [PATCH 055/114] Add disabled style for prompt editor after confirming --- crates/ai/src/assistant.rs | 11 +++++++++-- crates/editor/src/editor.rs | 9 +++++++++ crates/theme/src/theme.rs | 1 + styles/src/style_tree/assistant.ts | 10 +++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index c5bf027fcc..f7abcdf748 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -2871,8 +2871,15 @@ impl InlineAssistant { cx.emit(InlineAssistantEvent::Dismissed); } else { let prompt = self.prompt_editor.read(cx).text(cx); - self.prompt_editor - .update(cx, |editor, _| editor.set_read_only(true)); + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + }); cx.emit(InlineAssistantEvent::Confirmed { prompt }); self.confirmed = true; } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d5141a7f7d..75fb6006c0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1606,6 +1606,15 @@ impl Editor { self.read_only = read_only; } + pub fn set_field_editor_style( + &mut self, + style: Option>, + cx: &mut ViewContext, + ) { + self.get_field_editor_theme = style; + cx.notify(); + } + pub fn replica_id_map(&self) -> Option<&HashMap> { self.replica_id_mapping.as_ref() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bc0c98bac7..7913685b7a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1158,6 +1158,7 @@ pub struct InlineAssistantStyle { #[serde(flatten)] pub container: ContainerStyle, pub editor: FieldEditor, + pub disabled_editor: FieldEditor, pub pending_edit_background: Color, } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index bdca8a16e5..8bef2ce16b 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -68,9 +68,17 @@ export default function assistant(): any { }), editor: { text: text(theme.highest, "mono", "default", { size: "sm" }), - placeholder_text: text(theme.lowest, "sans", "on", "disabled"), + 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"), }, message_header: { From 937aabfdfdd435807368068f6e47f7d03981919c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 11:24:55 +0200 Subject: [PATCH 056/114] Extract a `strip_markdown_codeblock` function --- crates/ai/src/assistant.rs | 197 +++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 87 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index f7abcdf748..0333a723e9 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -16,7 +16,7 @@ use editor::{ Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; -use futures::{channel::mpsc, SinkExt, StreamExt}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{ actions, elements::*, @@ -620,7 +620,10 @@ impl AssistantPanel { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { - let mut messages = response.await?; + let chunks = strip_markdown_codeblock(response.await?.filter_map( + |message| async move { message.ok()?.choices.pop()?.delta.content }, + )); + futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); let indentation_len; @@ -636,93 +639,21 @@ impl AssistantPanel { indentation_text = ""; }; - let mut inside_first_line = true; - let mut starts_with_fenced_code_block = None; - let mut has_pending_newline = false; - let mut new_text = String::new(); + let mut new_text = indentation_text + .repeat(indentation_len.saturating_sub(selection_start.column) as usize); - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(mut choice) = message.choices.pop() { - if has_pending_newline { - has_pending_newline = false; - choice - .delta - .content - .get_or_insert(String::new()) - .insert(0, '\n'); - } + while let Some(message) = chunks.next().await { + let mut lines = message.split('\n'); + if let Some(first_line) = lines.next() { + new_text.push_str(first_line); + } - // Buffer a trailing codeblock fence. Note that we don't stop - // right away because this may be an inner fence that we need - // to insert into the editor. - if starts_with_fenced_code_block.is_some() - && choice.delta.content.as_deref() == Some("\n```") - { - new_text.push_str("\n```"); - continue; - } - - // If this was the last completion and we started with a codeblock - // fence and we ended with another codeblock fence, then we can - // stop right away. Otherwise, whatever text we buffered will be - // processed normally. - if choice.finish_reason.is_some() - && starts_with_fenced_code_block.unwrap_or(false) - && new_text == "\n```" - { - break; - } - - if let Some(text) = choice.delta.content { - // Never push a newline if there's nothing after it. This is - // useful to detect if the newline was pushed because of a - // trailing codeblock fence. - let text = if let Some(prefix) = text.strip_suffix('\n') { - has_pending_newline = true; - prefix - } else { - text.as_str() - }; - - if text.is_empty() { - continue; - } - - let mut lines = text.split('\n'); - if let Some(line) = lines.next() { - if starts_with_fenced_code_block.is_none() { - starts_with_fenced_code_block = - Some(line.starts_with("```")); - } - - // Avoid pushing the first line if it's the start of a fenced code block. - if !inside_first_line || !starts_with_fenced_code_block.unwrap() - { - new_text.push_str(&line); - } - } - - for line in lines { - if inside_first_line && starts_with_fenced_code_block.unwrap() { - // If we were inside the first line and that line was the - // start of a fenced code block, we just need to push the - // leading indentation of the original selection. - new_text.push_str(&indentation_text.repeat( - indentation_len.saturating_sub(selection_start.column) - as usize, - )); - } else { - // Otherwise, we need to push a newline and the base indentation. - new_text.push('\n'); - new_text.push_str( - &indentation_text.repeat(indentation_len as usize), - ); - } - - new_text.push_str(line); - inside_first_line = false; - } + for line in lines { + new_text.push('\n'); + if !line.is_empty() { + new_text + .push_str(&indentation_text.repeat(indentation_len as usize)); + new_text.push_str(line); } } @@ -2919,10 +2850,58 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } +fn strip_markdown_codeblock(stream: impl Stream) -> impl Stream { + let mut first_line = true; + let mut buffer = String::new(); + let mut starts_with_fenced_code_block = false; + stream.filter_map(move |chunk| { + buffer.push_str(&chunk); + + if first_line { + if buffer == "" || buffer == "`" || buffer == "``" { + return futures::future::ready(None); + } else if buffer.starts_with("```") { + starts_with_fenced_code_block = true; + if let Some(newline_ix) = buffer.find('\n') { + buffer.replace_range(..newline_ix + 1, ""); + first_line = false; + } else { + return futures::future::ready(None); + } + } + } + + let text = if starts_with_fenced_code_block { + buffer + .strip_suffix("\n```") + .or_else(|| buffer.strip_suffix("\n``")) + .or_else(|| buffer.strip_suffix("\n`")) + .or_else(|| buffer.strip_suffix('\n')) + .unwrap_or(&buffer) + } else { + &buffer + }; + + if text.contains('\n') { + first_line = false; + } + + let remainder = buffer.split_off(text.len()); + let result = if buffer.is_empty() { + None + } else { + Some(buffer.clone()) + }; + buffer = remainder; + futures::future::ready(result) + }) +} + #[cfg(test)] mod tests { use super::*; use crate::MessageId; + use futures::stream; use gpui::AppContext; #[gpui::test] @@ -3291,6 +3270,50 @@ mod tests { ); } + #[gpui::test] + async fn test_strip_markdown_codeblock() { + assert_eq!( + strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) + .collect::() + .await, + "```js\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .collect::() + .await, + "``\nLorem ipsum dolor\n```" + ); + + fn chunks(text: &str, size: usize) -> impl Stream { + stream::iter( + text.chars() + .collect::>() + .chunks(size) + .map(|chunk| chunk.iter().collect::()) + .collect::>(), + ) + } + } + fn messages( conversation: &ModelHandle, cx: &AppContext, From d804afcfa96e44044ad64a89d2b72fe07b2bec9b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 11:57:02 +0200 Subject: [PATCH 057/114] Don't auto-indent when the assistant starts responding with indentation --- crates/ai/src/assistant.rs | 81 +++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 0333a723e9..7803a89ea9 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -472,48 +472,48 @@ impl AssistantPanel { .text_for_range(range.start..range.end) .collect::(); - let mut base_indentation: Option = None; let selection_start = range.start.to_point(&snapshot); let selection_end = range.end.to_point(&snapshot); + + let mut base_indent: Option = None; let mut start_row = selection_start.row; if snapshot.is_line_blank(start_row) { if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { start_row = prev_non_blank_row; } } - for row in start_row..=selection_end.row { if snapshot.is_line_blank(row) { continue; } - let line_indentation = snapshot.indent_size_for_line(row); - if let Some(base_indentation) = base_indentation.as_mut() { - if line_indentation.len < base_indentation.len { - *base_indentation = line_indentation; + let line_indent = snapshot.indent_size_for_line(row); + if let Some(base_indent) = base_indent.as_mut() { + if line_indent.len < base_indent.len { + *base_indent = line_indent; } } else { - base_indentation = Some(line_indentation); + base_indent = Some(line_indent); } } let mut normalized_selected_text = selected_text.clone(); - if let Some(base_indentation) = base_indentation { + if let Some(base_indent) = base_indent { for row in selection_start.row..=selection_end.row { let selection_row = row - selection_start.row; let line_start = normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); - let indentation_len = if row == selection_start.row { - base_indentation.len.saturating_sub(selection_start.column) + let indent_len = if row == selection_start.row { + base_indent.len.saturating_sub(selection_start.column) } else { let line_len = normalized_selected_text.line_len(selection_row); - cmp::min(line_len, base_indentation.len) + cmp::min(line_len, base_indent.len) }; - let indentation_end = cmp::min( - line_start + indentation_len as usize, + let indent_end = cmp::min( + line_start + indent_len as usize, normalized_selected_text.len(), ); - normalized_selected_text.replace(line_start..indentation_end, ""); + normalized_selected_text.replace(line_start..indent_end, ""); } } @@ -581,7 +581,11 @@ impl AssistantPanel { "Assume the cursor is located where the `<|>` marker is." ) .unwrap(); - writeln!(prompt, "Assume your answer will be inserted at the cursor.").unwrap(); + writeln!( + prompt, + "Code can't be replaced, so assume your answer will be inserted at the cursor." + ) + .unwrap(); writeln!( prompt, "Complete the code given the user prompt: {user_prompt}" @@ -591,7 +595,11 @@ impl AssistantPanel { } writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!(prompt, "Never make remarks, always output code.").unwrap(); + writeln!( + prompt, + "Never make remarks about the output, always output just code." + ) + .unwrap(); let request = OpenAIRequest { model: model.full_name().into(), @@ -626,40 +634,51 @@ impl AssistantPanel { futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); - let indentation_len; - let indentation_text; - if let Some(base_indentation) = base_indentation { - indentation_len = base_indentation.len; - indentation_text = match base_indentation.kind { + let indent_len; + let indent_text; + if let Some(base_indent) = base_indent { + indent_len = base_indent.len; + indent_text = match base_indent.kind { language::IndentKind::Space => " ", language::IndentKind::Tab => "\t", }; } else { - indentation_len = 0; - indentation_text = ""; + indent_len = 0; + indent_text = ""; }; - let mut new_text = indentation_text - .repeat(indentation_len.saturating_sub(selection_start.column) as usize); + let mut autoindent = true; + let mut first_chunk = true; + let mut new_text = String::new(); - while let Some(message) = chunks.next().await { - let mut lines = message.split('\n'); + while let Some(chunk) = chunks.next().await { + if first_chunk && (chunk.starts_with(' ') || chunk.starts_with('\t')) { + autoindent = false; + } + + if first_chunk && autoindent { + let first_line_indent = + indent_len.saturating_sub(selection_start.column) as usize; + new_text = indent_text.repeat(first_line_indent); + } + + let mut lines = chunk.split('\n'); if let Some(first_line) = lines.next() { new_text.push_str(first_line); } for line in lines { new_text.push('\n'); - if !line.is_empty() { - new_text - .push_str(&indentation_text.repeat(indentation_len as usize)); - new_text.push_str(line); + if !line.is_empty() && autoindent { + new_text.push_str(&indent_text.repeat(indent_len as usize)); } + new_text.push_str(line); } let hunks = diff.push_new(&new_text); hunks_tx.send(hunks).await?; new_text.clear(); + first_chunk = false; } hunks_tx.send(diff.finish()).await?; From 1fb7ce0f4a3dfa492cbc73a95db56c6671bcd23c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 12:13:44 +0200 Subject: [PATCH 058/114] Show icon to toggle inline assist --- Cargo.lock | 1 + crates/ai/src/assistant.rs | 6 ++++- crates/quick_action_bar/Cargo.toml | 1 + .../quick_action_bar/src/quick_action_bar.rs | 23 +++++++++++++++++-- crates/zed/src/zed.rs | 5 ++-- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c232afa081..84b8093be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5647,6 +5647,7 @@ dependencies = [ name = "quick_action_bar" version = "0.1.0" dependencies = [ + "ai", "editor", "gpui", "search", diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 7803a89ea9..193cba8db4 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -215,7 +215,11 @@ impl AssistantPanel { }) } - fn inline_assist(workspace: &mut Workspace, _: &InlineAssist, cx: &mut ViewContext) { + pub fn inline_assist( + workspace: &mut Workspace, + _: &InlineAssist, + cx: &mut ViewContext, + ) { let this = if let Some(this) = workspace.panel::(cx) { if this .update(cx, |assistant, cx| assistant.load_api_key(cx)) diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 6953ac0e02..1f8ec4e92b 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -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" } diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 3055399c13..a7734deac5 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -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, active_item: Option>, _inlay_hints_enabled_subscription: Option, + workspace: WeakViewHandle, } impl QuickActionBar { - pub fn new(buffer_search_bar: ViewHandle) -> Self { + pub fn new(buffer_search_bar: ViewHandle, workspace: &Workspace) -> Self { Self { buffer_search_bar, active_item: None, _inlay_hints_enabled_subscription: None, + workspace: workspace.weak_handle(), } } @@ -86,6 +90,21 @@ impl View for QuickActionBar { )); } + bar.add_child(render_quick_action_bar_button( + 2, + "icons/radix/magic-wand.svg", + false, + ("Generate code...".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() } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index de05c259c8..6421818b62 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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); From c587cf66ce5cb6f38d3cee843e049adf3830babe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 12:24:01 +0200 Subject: [PATCH 059/114] Remove ellipsis from tooltip --- crates/quick_action_bar/src/quick_action_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 7d6587795e..da5a8e6d72 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -96,7 +96,7 @@ impl View for QuickActionBar { 2, "icons/radix/magic-wand.svg", false, - ("Generate code...".into(), Some(Box::new(InlineAssist))), + ("Generate code".into(), Some(Box::new(InlineAssist))), cx, move |this, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { From 8c4d2ccf80a066d9344702950de9986fc3d1f636 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:23:42 +0200 Subject: [PATCH 060/114] Close inline assist when the associated transaction is undone --- crates/ai/src/assistant.rs | 34 ++++++++++++++++++++++++++-------- crates/editor/src/editor.rs | 6 ++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 3b444082c6..4dad12ad08 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -249,7 +249,7 @@ impl AssistantPanel { } fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { - let id = post_inc(&mut self.next_inline_assist_id); + let inline_assist_id = post_inc(&mut self.next_inline_assist_id); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selection = editor.read(cx).selections.newest_anchor().clone(); let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); @@ -272,7 +272,7 @@ impl AssistantPanel { }); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant { - id, + id: inline_assist_id, prompt_editor, confirmed: false, has_focus: false, @@ -313,7 +313,7 @@ impl AssistantPanel { }); self.pending_inline_assists.insert( - id, + inline_assist_id, PendingInlineAssist { kind: assist_kind, editor: editor.downgrade(), @@ -326,12 +326,30 @@ impl AssistantPanel { cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), cx.subscribe(editor, { let inline_assistant = inline_assistant.downgrade(); - move |_, editor, event, cx| { + move |this, editor, event, cx| { if let Some(inline_assistant) = inline_assistant.upgrade(cx) { - if let editor::Event::SelectionsChanged { local } = event { - if *local && inline_assistant.read(cx).has_focus { - cx.focus(&editor); + match event { + editor::Event::SelectionsChanged { local } => { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); + } } + editor::Event::TransactionUndone { + transaction_id: tx_id, + } => { + if let Some(pending_assist) = + this.pending_inline_assists.get(&inline_assist_id) + { + if pending_assist.transaction_id == Some(*tx_id) { + this.close_inline_assist( + inline_assist_id, + false, + cx, + ); + } + } + } + _ => {} } } } @@ -342,7 +360,7 @@ impl AssistantPanel { self.pending_inline_assist_ids_by_editor .entry(editor.downgrade()) .or_default() - .push(id); + .push(inline_assist_id); self.update_highlights_for_editor(&editor, cx); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6396536b83..fde280f8fe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4975,6 +4975,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, + }); } } @@ -8404,6 +8407,9 @@ pub enum Event { local: bool, autoscroll: bool, }, + TransactionUndone { + transaction_id: TransactionId, + }, Closed, } From b9df85e01fa5d6934e67da4c1238173f002db5cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:25:01 +0200 Subject: [PATCH 061/114] Remove todo.md --- todo.md | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 todo.md diff --git a/todo.md b/todo.md deleted file mode 100644 index 0507708502..0000000000 --- a/todo.md +++ /dev/null @@ -1,48 +0,0 @@ -- Hit ctrl-enter deploys an edit prompt - - Empty selection (cursor) => append text - - On end of line: Edit prompt on end of line. - - [x] Middle of line: Edit prompt near cursor head on a different line - - Non-empty selection => refactor - - [x] Edit prompt near cursor head on a different line - - [x] What was selected when you hit ctrl-enter is colored. -- [x] Add placeholder text - - If non-empty selection: Enter prompt to transform selected text - - If empty selection: Enter prompt to generate text -- When cursor is inside a prompt - - [x] Escape cancels/undoes - - [x] Enter confirms -- [x] Selection is cleared and cursor is moved to prompt input -- [x] Ability to highlight background multiple times for the same type -- [x] Basic Styling -- [ ] Look into why insert prompts have a weird indentation sometimes - -- Multicursor - - Run the same prompt for every selection in parallel - - Position the prompt editor at the newest cursor -- Follow up ship: Marks - - Global across all buffers - - Select text, hit a binding - - That text gets added to the marks - - Simplest: Marks are a set, and you add to them with this binding. - - Could this be a stack? That might be too much. - - When you hit ctrl-enter to generate / transform text, we include the marked text in the context. - -- During inference, always send marked text. -- During inference, send as much context as possible given the user's desired generation length. - -- This would assume a convenient binding for setting the generation length. - - -~~~~~~~~~ - -Dial up / dial down how much context we send -Dial up / down your max generation length. - - -------- (merge to main) - -- Text in the prompt should soft wrap - ------------ (maybe pause) - -- Excurse outside of the editor without dismissing it... kind of like a message in the assistant. From 52e1e014ad08ecba01843c36f4ff6dac905933c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:42:41 +0200 Subject: [PATCH 062/114] Allow redoing edits performed by inline assistant after cancelling it --- crates/ai/src/assistant.rs | 2 +- crates/editor/src/multi_buffer.rs | 24 ++++++++++++++++++------ crates/language/src/buffer.rs | 4 ++-- crates/text/src/text.rs | 29 +++++++++++++++++++++-------- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4dad12ad08..952c924292 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -442,7 +442,7 @@ impl AssistantPanel { if let Some(transaction_id) = pending_assist.transaction_id { editor.update(cx, |editor, cx| { editor.buffer().update(cx, |buffer, cx| { - buffer.undo_and_forget(transaction_id, cx) + buffer.undo_transaction(transaction_id, cx) }); }); } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c7c4028995..0c499c16c4 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -824,13 +824,15 @@ impl MultiBuffer { None } - pub fn undo_and_forget(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { + pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| buffer.undo_and_forget(transaction_id, cx)); - } else if let Some(transaction) = self.history.forget(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_and_forget(transaction_id, cx)); + 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) + }); } } } @@ -3454,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 { let mut count = 0; let mut transactions = self.undo_stack.iter(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2ed99d8526..4310f84830 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1668,14 +1668,14 @@ impl Buffer { } } - pub fn undo_and_forget( + pub fn undo_transaction( &mut self, transaction_id: TransactionId, cx: &mut ModelContext, ) -> bool { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); - if let Some(operation) = self.text.undo_and_forget(transaction_id) { + 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 diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 43bcef0825..6a00ea12db 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -264,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(); @@ -1207,19 +1219,20 @@ impl Buffer { } } - pub fn undo_and_forget(&mut self, transaction_id: TransactionId) -> Option { - if let Some(transaction) = self.history.forget(transaction_id) { - self.undo_or_redo(transaction).log_err() - } else { - None - } + pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option { + 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 { let transactions = self .history - .remove_from_undo(transaction_id) + .remove_from_undo_until(transaction_id) .iter() .map(|entry| entry.transaction.clone()) .collect::>(); From ccec59337a6dea4f2daed7ed5fc2cc6350241970 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:46:05 +0200 Subject: [PATCH 063/114] :memo: --- crates/ai/src/assistant.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 952c924292..80c3771085 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -341,6 +341,9 @@ impl AssistantPanel { this.pending_inline_assists.get(&inline_assist_id) { if pending_assist.transaction_id == Some(*tx_id) { + // Notice we are supplying `undo: false` here. This + // is because there's no need to undo the transaction + // because the user just did so. this.close_inline_assist( inline_assist_id, false, @@ -421,7 +424,7 @@ impl AssistantPanel { } } - fn close_inline_assist(&mut self, assist_id: usize, cancel: bool, cx: &mut ViewContext) { + fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { self.hide_inline_assist(assist_id, cx); if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { @@ -438,7 +441,7 @@ impl AssistantPanel { if let Some(editor) = pending_assist.editor.upgrade(cx) { self.update_highlights_for_editor(&editor, cx); - if cancel { + if undo { if let Some(transaction_id) = pending_assist.transaction_id { editor.update(cx, |editor, cx| { editor.buffer().update(cx, |buffer, cx| { From ded6decb29d04b1bc56a723ee48127558e1e1b63 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 28 Aug 2023 11:27:45 -0400 Subject: [PATCH 064/114] Initial unstyled language server short name in completions Co-Authored-By: Kirill Bulatov --- crates/editor/src/editor.rs | 61 ++++++++++++++++++++------ crates/language/src/language.rs | 9 ++++ crates/zed/src/languages/c.rs | 4 ++ crates/zed/src/languages/css.rs | 4 ++ crates/zed/src/languages/elixir.rs | 4 ++ crates/zed/src/languages/go.rs | 4 ++ crates/zed/src/languages/html.rs | 4 ++ crates/zed/src/languages/json.rs | 4 ++ crates/zed/src/languages/lua.rs | 4 ++ crates/zed/src/languages/php.rs | 4 ++ crates/zed/src/languages/python.rs | 4 ++ crates/zed/src/languages/ruby.rs | 4 ++ crates/zed/src/languages/rust.rs | 4 ++ crates/zed/src/languages/svelte.rs | 4 ++ crates/zed/src/languages/tailwind.rs | 4 ++ crates/zed/src/languages/typescript.rs | 8 ++++ crates/zed/src/languages/yaml.rs | 4 ++ 17 files changed, 121 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bfa804c56c..a188a47e35 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -810,6 +810,7 @@ struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, + project: Option>, completions: Arc<[Completion]>, match_candidates: Vec, matches: Arc<[StringMatch]>, @@ -853,6 +854,26 @@ impl CompletionsMenu { fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { enum CompletionTag {} + let language_servers = self.project.as_ref().map(|project| { + project + .read(cx) + .language_servers_for_buffer(self.buffer.read(cx), cx) + .map(|(adapter, server)| (server.server_id(), format!("{}: ", adapter.short_name))) + .collect::>() + }); + let get_server_name = move |lookup_server_id: lsp::LanguageServerId| -> Option { + language_servers + .iter() + .flatten() + .find_map(|(server_id, server_name)| { + if *server_id == lookup_server_id { + Some(server_name.clone()) + } else { + None + } + }) + }; + let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; @@ -879,19 +900,31 @@ impl CompletionsMenu { style.autocomplete.item }; - Text::new(completion.label.text.clone(), style.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - style.text.color.into(), - styled_runs_for_code_label( - &completion.label, - &style.syntax, - ), - &mat.positions, - )) - .contained() - .with_style(item_style) + let completion_label = + Text::new(completion.label.text.clone(), style.text.clone()) + .with_soft_wrap(false) + .with_highlights( + combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + style.text.color.into(), + styled_runs_for_code_label( + &completion.label, + &style.syntax, + ), + &mat.positions, + ), + ); + + if let Some(server_name) = get_server_name(completion.server_id) { + Flex::row() + .with_child(Text::new(server_name, style.text.clone())) + .with_child(completion_label) + .into_any() + } else { + completion_label.into_any() + } + .contained() + .with_style(item_style) }, ) .with_cursor_style(CursorStyle::PointingHand) @@ -2850,6 +2883,7 @@ impl Editor { }); let id = post_inc(&mut self.next_completion_id); + let project = self.project.clone(); let task = cx.spawn(|this, mut cx| { async move { let menu = if let Some(completions) = completions.await.log_err() { @@ -2868,6 +2902,7 @@ impl Editor { }) .collect(), buffer, + project, completions: completions.into(), matches: Vec::new().into(), selected_item: 0, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ccc1937032..a2d02ecd96 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -90,6 +90,7 @@ pub struct LanguageServerName(pub Arc); /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, + pub short_name: &'static str, pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, @@ -100,6 +101,7 @@ pub struct CachedLspAdapter { impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; + let short_name = adapter.short_name(); let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = @@ -108,6 +110,7 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, + short_name, initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, @@ -216,6 +219,8 @@ pub trait LspAdapterDelegate: Send + Sync { pub trait LspAdapter: 'static + Send + Sync { async fn name(&self) -> LanguageServerName; + fn short_name(&self) -> &'static str; + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -1696,6 +1701,10 @@ impl LspAdapter for Arc { LanguageServerName(self.name.into()) } + fn short_name(&self) -> &'static str { + "FakeLspAdapter" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 47aa2b739c..ad5a68f8dd 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -19,6 +19,10 @@ impl super::LspAdapter for CLspAdapter { LanguageServerName("clangd".into()) } + fn short_name(&self) -> &'static str { + "clangd" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/css.rs b/crates/zed/src/languages/css.rs index 51db8b8ab8..f2103050f3 100644 --- a/crates/zed/src/languages/css.rs +++ b/crates/zed/src/languages/css.rs @@ -37,6 +37,10 @@ impl LspAdapter for CssLspAdapter { LanguageServerName("vscode-css-language-server".into()) } + fn short_name(&self) -> &'static str { + "css" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index c32927e15c..b166feda76 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -27,6 +27,10 @@ impl LspAdapter for ElixirLspAdapter { LanguageServerName("elixir-ls".into()) } + fn short_name(&self) -> &'static str { + "elixir-ls" + } + fn will_start_server( &self, delegate: &Arc, diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index d7982f7bdb..19b7013709 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -37,6 +37,10 @@ impl super::LspAdapter for GoLspAdapter { LanguageServerName("gopls".into()) } + fn short_name(&self) -> &'static str { + "gopls" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index ecc839fca6..cfb6a5dde9 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -37,6 +37,10 @@ impl LspAdapter for HtmlLspAdapter { LanguageServerName("vscode-html-language-server".into()) } + fn short_name(&self) -> &'static str { + "html" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 225cea0e92..f7e8f87492 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -43,6 +43,10 @@ impl LspAdapter for JsonLspAdapter { LanguageServerName("json-language-server".into()) } + fn short_name(&self) -> &'static str { + "json" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 7c5c7179d0..ee6d0f8579 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -29,6 +29,10 @@ impl super::LspAdapter for LuaLspAdapter { LanguageServerName("lua-language-server".into()) } + fn short_name(&self) -> &'static str { + "lua" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index 6a01d00300..73bb4b019c 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -41,6 +41,10 @@ impl LspAdapter for IntelephenseLspAdapter { LanguageServerName("intelephense".into()) } + fn short_name(&self) -> &'static str { + "php" + } + async fn fetch_latest_server_version( &self, _delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 41ad28ba86..023bbab13f 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -35,6 +35,10 @@ impl LspAdapter for PythonLspAdapter { LanguageServerName("pyright".into()) } + fn short_name(&self) -> &'static str { + "pyright" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs index 358441352a..3890b90dbd 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/zed/src/languages/ruby.rs @@ -12,6 +12,10 @@ impl LspAdapter for RubyLanguageServer { LanguageServerName("solargraph".into()) } + fn short_name(&self) -> &'static str { + "solargraph" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3c7f84fec7..bf8ad00293 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -22,6 +22,10 @@ impl LspAdapter for RustLspAdapter { LanguageServerName("rust-analyzer".into()) } + fn short_name(&self) -> &'static str { + "rust" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/svelte.rs b/crates/zed/src/languages/svelte.rs index 8416859f5a..35665e864f 100644 --- a/crates/zed/src/languages/svelte.rs +++ b/crates/zed/src/languages/svelte.rs @@ -36,6 +36,10 @@ impl LspAdapter for SvelteLspAdapter { LanguageServerName("svelte-language-server".into()) } + fn short_name(&self) -> &'static str { + "svelte" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 9a32f69e43..d7c11f0c73 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -41,6 +41,10 @@ impl LspAdapter for TailwindLspAdapter { LanguageServerName("tailwindcss-language-server".into()) } + fn short_name(&self) -> &'static str { + "tailwind" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index b7e4438e1f..e3bb0aae95 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -56,6 +56,10 @@ impl LspAdapter for TypeScriptLspAdapter { LanguageServerName("typescript-language-server".into()) } + fn short_name(&self) -> &'static str { + "tsserver" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -218,6 +222,10 @@ impl LspAdapter for EsLintLspAdapter { LanguageServerName("eslint".into()) } + fn short_name(&self) -> &'static str { + "eslint" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 48d7a3cf87..21155cc231 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -40,6 +40,10 @@ impl LspAdapter for YamlLspAdapter { LanguageServerName("yaml-language-server".into()) } + fn short_name(&self) -> &'static str { + "yaml" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, From 35b7787e02c1b544fd8cfb94551382543f69da11 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 28 Aug 2023 11:56:44 -0400 Subject: [PATCH 065/114] Add Tailwind server to TSX --- crates/zed/src/languages.rs | 2 +- crates/zed/src/languages/tailwind.rs | 2 ++ crates/zed/src/languages/tsx/config.toml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 8aaa11e1cd..f0b8a1444a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -104,6 +104,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( @@ -112,7 +113,6 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index d7c11f0c73..1b7c271d10 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -103,6 +103,7 @@ impl LspAdapter for TailwindLspAdapter { "html": "html", "css": "css", "javascript": "javascript", + "typescriptreact": "typescriptreact", }, })) } @@ -122,6 +123,7 @@ impl LspAdapter for TailwindLspAdapter { ("HTML".to_string(), "html".to_string()), ("CSS".to_string(), "css".to_string()), ("JavaScript".to_string(), "javascript".to_string()), + ("TSX".to_string(), "typescriptreact".to_string()), ] .into_iter(), ) diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index 234dc6b013..2f676f6710 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -16,3 +16,6 @@ brackets = [ [overrides.element] line_comment = { remove = true } block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] From c2b60df5afaeb624b21e89ec3a61b9b794331840 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 16:36:07 +0200 Subject: [PATCH 066/114] Allow including conversation when triggering inline assist --- crates/ai/src/assistant.rs | 156 ++++++++++++++++++++++++----- crates/theme/src/theme.rs | 1 + styles/src/style_tree/assistant.ts | 58 ++++++++++- 3 files changed, 189 insertions(+), 26 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 80c3771085..ae223fdb57 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,12 +19,16 @@ use fs::Fs; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{ actions, - elements::*, + elements::{ + ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable, + Stack, Svg, Text, UniformList, UniformListState, + }, fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton}, - Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, + ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; use language::{ language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _, @@ -33,7 +37,7 @@ use language::{ use search::BufferSearchBar; use settings::SettingsStore; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, cmp, env, fmt::Write, iter, @@ -43,7 +47,10 @@ use std::{ sync::Arc, time::Duration, }; -use theme::AssistantStyle; +use theme::{ + components::{action_button::Button, ComponentExt}, + AssistantStyle, +}; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -61,7 +68,8 @@ actions!( QuoteSelection, ToggleFocus, ResetKey, - InlineAssist + InlineAssist, + ToggleIncludeConversation, ] ); @@ -97,6 +105,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(AssistantPanel::cancel_last_inline_assist); cx.add_action(InlineAssistant::confirm); cx.add_action(InlineAssistant::cancel); + cx.add_action(InlineAssistant::toggle_include_conversation); } #[derive(Debug)] @@ -129,6 +138,7 @@ pub struct AssistantPanel { next_inline_assist_id: usize, pending_inline_assists: HashMap, pending_inline_assist_ids_by_editor: HashMap, Vec>, + include_conversation_in_next_inline_assist: bool, _watch_saved_conversations: Task>, } @@ -195,6 +205,7 @@ impl AssistantPanel { next_inline_assist_id: 0, pending_inline_assists: Default::default(), pending_inline_assist_ids_by_editor: Default::default(), + include_conversation_in_next_inline_assist: false, _watch_saved_conversations, }; @@ -270,12 +281,15 @@ impl AssistantPanel { editor.set_placeholder_text(placeholder, cx); editor }); + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant { id: inline_assist_id, prompt_editor, confirmed: false, has_focus: false, + include_conversation: self.include_conversation_in_next_inline_assist, + measurements: measurements.clone(), }; cx.focus_self(); assistant @@ -292,13 +306,11 @@ impl AssistantPanel { render: Arc::new({ let inline_assistant = inline_assistant.clone(); move |cx: &mut BlockContext| { - let theme = theme::current(cx); - ChildView::new(&inline_assistant, cx) - .contained() - .with_padding_left(cx.anchor_x) - .contained() - .with_style(theme.assistant.inline.container) - .into_any() + measurements.set(BlockMeasurements { + anchor_x: cx.anchor_x, + gutter_width: cx.gutter_width, + }); + ChildView::new(&inline_assistant, cx).into_any() } }), disposition: if selection.reversed { @@ -375,8 +387,11 @@ impl AssistantPanel { ) { let assist_id = inline_assistant.read(cx).id; match event { - InlineAssistantEvent::Confirmed { prompt } => { - self.confirm_inline_assist(assist_id, prompt, cx); + InlineAssistantEvent::Confirmed { + prompt, + include_conversation, + } => { + self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx); } InlineAssistantEvent::Canceled => { self.close_inline_assist(assist_id, true, cx); @@ -470,14 +485,24 @@ impl AssistantPanel { &mut self, inline_assist_id: usize, user_prompt: &str, + include_conversation: bool, cx: &mut ViewContext, ) { + self.include_conversation_in_next_inline_assist = include_conversation; + let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { return; }; + let conversation = if include_conversation { + self.active_editor() + .map(|editor| editor.read(cx).conversation.clone()) + } else { + None + }; + let pending_assist = if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { pending_assist @@ -626,14 +651,25 @@ impl AssistantPanel { ) .unwrap(); - let request = OpenAIRequest { + let mut request = OpenAIRequest { model: model.full_name().into(), - messages: vec![RequestMessage { - role: Role::User, - content: prompt, - }], + messages: Vec::new(), stream: true, }; + if let Some(conversation) = conversation { + let conversation = conversation.read(cx); + let buffer = conversation.buffer.read(cx); + request.messages.extend( + conversation + .messages(cx) + .map(|message| message.to_open_ai_message(buffer)), + ); + } + + request.messages.push(RequestMessage { + role: Role::User, + content: prompt, + }); let response = stream_completion(api_key, cx.background().clone(), request); let editor = editor.downgrade(); @@ -2799,7 +2835,10 @@ impl Message { } enum InlineAssistantEvent { - Confirmed { prompt: String }, + Confirmed { + prompt: String, + include_conversation: bool, + }, Canceled, Dismissed, } @@ -2815,6 +2854,8 @@ struct InlineAssistant { prompt_editor: ViewHandle, confirmed: bool, has_focus: bool, + include_conversation: bool, + measurements: Rc>, } impl Entity for InlineAssistant { @@ -2827,9 +2868,55 @@ impl View for InlineAssistant { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() + let theme = theme::current(cx); + + Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned() + .constrained() + .dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f(measurements.gutter_width, constraint.min.y()), + max: vec2f(measurements.gutter_width, constraint.max.y()), + } + } + }), + ) + .with_child(Empty::new().constrained().dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.min.y(), + ), + max: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.max.y(), + ), + } + } + })) + .with_child( + ChildView::new(&self.prompt_editor, cx) + .aligned() + .left() + .flex(1., true), + ) + .contained() + .with_style(theme.assistant.inline.container) + .into_any() .into_any() } @@ -2862,10 +2949,29 @@ impl InlineAssistant { cx, ); }); - cx.emit(InlineAssistantEvent::Confirmed { prompt }); + cx.emit(InlineAssistantEvent::Confirmed { + prompt, + include_conversation: self.include_conversation, + }); self.confirmed = true; } } + + fn toggle_include_conversation( + &mut self, + _: &ToggleIncludeConversation, + cx: &mut ViewContext, + ) { + self.include_conversation = !self.include_conversation; + cx.notify(); + } +} + +// This wouldn't need to exist if we could pass parameters when rendering child views. +#[derive(Copy, Clone, Default)] +struct BlockMeasurements { + anchor_x: f32, + gutter_width: f32, } struct PendingInlineAssist { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 7913685b7a..261933f057 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1160,6 +1160,7 @@ pub struct InlineAssistantStyle { pub editor: FieldEditor, pub disabled_editor: FieldEditor, pub pending_edit_background: Color, + pub include_conversation: ToggleIconButtonStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 8bef2ce16b..e660bf078f 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -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" @@ -80,6 +80,62 @@ export default function assistant(): any { }, }, 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 }, From 08df24412a4f4c04572696699b0f495caf269c21 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 14:31:58 +0200 Subject: [PATCH 067/114] Delete less aggressively --- crates/ai/src/streaming_diff.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/streaming_diff.rs b/crates/ai/src/streaming_diff.rs index 7ea7f6dacd..7399a7b4fa 100644 --- a/crates/ai/src/streaming_diff.rs +++ b/crates/ai/src/streaming_diff.rs @@ -82,9 +82,9 @@ pub struct StreamingDiff { impl StreamingDiff { const INSERTION_SCORE: f64 = -1.; - const DELETION_SCORE: f64 = -5.; - const EQUALITY_BASE: f64 = 2.; - const MAX_EQUALITY_EXPONENT: i32 = 20; + 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::>(); From 2332f824421875ce6db4c24a27f5df4755cce3a5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 14:41:02 +0200 Subject: [PATCH 068/114] More polish --- crates/ai/src/assistant.rs | 61 ++++++++++++++++++++---------- styles/src/style_tree/assistant.ts | 1 + 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ae223fdb57..62ff7212bf 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -567,24 +567,37 @@ impl AssistantPanel { } } - let language_name = snapshot - .language_at(range.start) - .map(|language| language.name()); - let language_name = language_name.as_deref().unwrap_or(""); + let language = snapshot.language_at(range.start); + let language_name = if let Some(language) = language.as_ref() { + if Arc::ptr_eq(language, &language::PLAIN_TEXT) { + None + } else { + Some(language.name()) + } + } else { + None + }; + let language_name = language_name.as_deref(); let model = settings::get::(cx) .default_open_ai_model .clone(); let mut prompt = String::new(); - writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); + } match pending_assist.kind { InlineAssistKind::Transform => { writeln!( prompt, - "You're currently working inside an editor on this code:" + "You're currently working inside an editor on this file:" ) .unwrap(); - writeln!(prompt, "```{language_name}").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { write!(prompt, "{chunk}").unwrap(); } @@ -592,31 +605,39 @@ impl AssistantPanel { writeln!( prompt, - "In particular, the user has selected the following code:" + "In particular, the user has selected the following text:" ) .unwrap(); - writeln!(prompt, "```{language_name}").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } writeln!(prompt, "{normalized_selected_text}").unwrap(); writeln!(prompt, "```").unwrap(); writeln!(prompt).unwrap(); writeln!( prompt, - "Modify the selected code given the user prompt: {user_prompt}" + "Modify the selected text given the user prompt: {user_prompt}" ) .unwrap(); writeln!( prompt, - "You MUST reply only with the edited selected code, not the entire file." + "You MUST reply only with the edited selected text, not the entire file." ) .unwrap(); } InlineAssistKind::Generate => { writeln!( prompt, - "You're currently working inside an editor on this code:" + "You're currently working inside an editor on this file:" ) .unwrap(); - writeln!(prompt, "```{language_name}").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } for chunk in snapshot.text_for_range(Anchor::min()..range.start) { write!(prompt, "{chunk}").unwrap(); } @@ -633,23 +654,21 @@ impl AssistantPanel { .unwrap(); writeln!( prompt, - "Code can't be replaced, so assume your answer will be inserted at the cursor." + "Text can't be replaced, so assume your answer will be inserted at the cursor." ) .unwrap(); writeln!( prompt, - "Complete the code given the user prompt: {user_prompt}" + "Complete the text given the user prompt: {user_prompt}" ) .unwrap(); } } - writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); + } writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!( - prompt, - "Never make remarks about the output, always output just code." - ) - .unwrap(); + writeln!(prompt, "Never make remarks about the output.").unwrap(); let mut request = OpenAIRequest { model: model.full_name().into(), diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index e660bf078f..4a33ef9b19 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -60,6 +60,7 @@ export default function assistant(): any { padding: { left: 12 }, }, inline: { + background: background(theme.highest), margin: { top: 3, bottom: 3 }, border: border(theme.lowest, "on", { top: true, From 72413dbaf235ddd9f332e4e7cbd9569936f32932 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 14:51:00 +0200 Subject: [PATCH 069/114] Remove the ability to reply to specific message in assistant --- crates/ai/src/assistant.rs | 255 +++++++++++++++++-------------------- 1 file changed, 114 insertions(+), 141 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 62ff7212bf..ab60d108f0 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1767,15 +1767,20 @@ impl Conversation { cx: &mut ModelContext, ) -> Vec { let mut user_messages = Vec::new(); - let mut tasks = Vec::new(); - let last_message_id = self.message_anchors.iter().rev().find_map(|message| { - message - .start - .is_valid(self.buffer.read(cx)) - .then_some(message.id) - }); + let last_message_id = if let Some(last_message_id) = + self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + }) { + last_message_id + } else { + return Default::default(); + }; + let mut should_assist = false; for selected_message_id in selected_messages { let selected_message_role = if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { @@ -1792,144 +1797,111 @@ impl Conversation { cx, ) { user_messages.push(user_message); - } else { - continue; } } else { - let request = OpenAIRequest { - model: self.model.full_name().to_string(), - messages: self - .messages(cx) - .filter(|message| matches!(message.status, MessageStatus::Done)) - .flat_map(|message| { - let mut system_message = None; - if message.id == selected_message_id { - system_message = Some(RequestMessage { - role: Role::System, - content: concat!( - "Treat the following messages as additional knowledge you have learned about, ", - "but act as if they were not part of this conversation. That is, treat them ", - "as if the user didn't see them and couldn't possibly inquire about them." - ).into() - }); - } - - Some(message.to_open_ai_message(self.buffer.read(cx))).into_iter().chain(system_message) - }) - .chain(Some(RequestMessage { - role: Role::System, - content: format!( - "Direct your reply to message with id {}. Do not include a [Message X] header.", - selected_message_id.0 - ), - })) - .collect(), - stream: true, - }; - - let Some(api_key) = self.api_key.borrow().clone() else { - continue; - }; - let stream = stream_completion(api_key, cx.background().clone(), request); - let assistant_message = self - .insert_message_after( - selected_message_id, - Role::Assistant, - MessageStatus::Pending, - cx, - ) - .unwrap(); - - // Queue up the user's next reply - if Some(selected_message_id) == last_message_id { - let user_message = self - .insert_message_after( - assistant_message.id, - Role::User, - MessageStatus::Done, - cx, - ) - .unwrap(); - user_messages.push(user_message); - } - - tasks.push(cx.spawn_weak({ - |this, mut cx| async move { - let assistant_message_id = assistant_message.id; - let stream_completion = async { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - let text: Arc = choice.delta.content?.into(); - let message_ix = this.message_anchors.iter().position( - |message| message.id == assistant_message_id, - )?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message - .start - .to_offset(buffer) - .saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) - }); - } - smol::future::yield_now().await; - } - - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions.retain(|completion| { - completion.id != this.completion_count - }); - this.summarize(cx); - }); - - anyhow::Ok(()) - }; - - let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = MessageStatus::Error( - error.to_string().trim().into(), - ); - } - } - cx.notify(); - } - }); - } - } - })); + should_assist = true; } } - if !tasks.is_empty() { + if should_assist { + let Some(api_key) = self.api_key.borrow().clone() else { + return Default::default(); + }; + + let request = OpenAIRequest { + model: self.model.full_name().to_string(), + messages: self + .messages(cx) + .filter(|message| matches!(message.status, MessageStatus::Done)) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .collect(), + stream: true, + }; + + let stream = stream_completion(api_key, cx.background().clone(), request); + let assistant_message = self + .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) + .unwrap(); + + // Queue up the user's next reply. + let user_message = self + .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) + .unwrap(); + user_messages.push(user_message); + + let task = cx.spawn_weak({ + |this, mut cx| async move { + let assistant_message_id = assistant_message.id; + let stream_completion = async { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + let text: Arc = choice.delta.content?.into(); + let message_ix = + this.message_anchors.iter().position(|message| { + message.id == assistant_message_id + })?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] + .iter() + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message + .start + .to_offset(buffer) + .saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); + }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + }); + } + smol::future::yield_now().await; + } + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + }); + + anyhow::Ok(()) + }; + + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = + MessageStatus::Error(error.to_string().trim().into()); + } + } + cx.notify(); + } + }); + } + } + }); + self.pending_completions.push(PendingCompletion { id: post_inc(&mut self.completion_count), - _tasks: tasks, + _task: task, }); } @@ -2296,7 +2268,7 @@ impl Conversation { struct PendingCompletion { id: usize, - _tasks: Vec>, + _task: Task<()>, } enum ConversationEditorEvent { @@ -2844,8 +2816,9 @@ pub struct Message { impl Message { fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { - let mut content = format!("[Message {}]\n", self.id.0).to_string(); - content.extend(buffer.text_for_range(self.offset_range.clone())); + let content = buffer + .text_for_range(self.offset_range.clone()) + .collect::(); RequestMessage { role: self.role, content: content.trim_end().into(), From 15628af04b727d6729f20eceac7a8ce2ca0685e1 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 29 Aug 2023 11:21:02 -0400 Subject: [PATCH 070/114] Style language server name in completion menu Omit in buffers with one or zero running language servers with the capability to provide completions Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 118 ++++++++++++++++++++++++-------- crates/theme/src/theme.rs | 3 + styles/src/style_tree/editor.ts | 3 + 3 files changed, 96 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a188a47e35..ab2be13a25 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -44,7 +44,7 @@ use gpui::{ elements::*, executor, fonts::{self, HighlightStyle, TextStyle}, - geometry::vector::Vector2F, + geometry::vector::{vec2f, Vector2F}, impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, @@ -858,21 +858,43 @@ impl CompletionsMenu { project .read(cx) .language_servers_for_buffer(self.buffer.read(cx), cx) - .map(|(adapter, server)| (server.server_id(), format!("{}: ", adapter.short_name))) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .map(|(adapter, server)| (server.server_id(), adapter.short_name)) .collect::>() }); - let get_server_name = move |lookup_server_id: lsp::LanguageServerId| -> Option { - language_servers - .iter() - .flatten() - .find_map(|(server_id, server_name)| { - if *server_id == lookup_server_id { - Some(server_name.clone()) - } else { - None - } - }) - }; + let needs_server_name = language_servers + .as_ref() + .map_or(false, |servers| servers.len() > 1); + + let get_server_name = + move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> { + language_servers + .iter() + .flatten() + .find_map(|(server_id, server_name)| { + if *server_id == lookup_server_id { + Some(*server_name) + } else { + None + } + }) + }; + + let widest_completion_ix = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completion = &self.completions[mat.candidate_id]; + let mut len = completion.label.text.chars().count(); + + if let Some(server_name) = get_server_name(completion.server_id) { + len += server_name.chars().count(); + } + + len + }) + .map(|(ix, _)| ix); let completions = self.completions.clone(); let matches = self.matches.clone(); @@ -917,14 +939,66 @@ impl CompletionsMenu { if let Some(server_name) = get_server_name(completion.server_id) { Flex::row() - .with_child(Text::new(server_name, style.text.clone())) .with_child(completion_label) + .with_children((|| { + if !needs_server_name { + return None; + } + + let text_style = TextStyle { + color: style.autocomplete.server_name_color, + font_size: style.text.font_size + * style.autocomplete.server_name_size_percent, + ..style.text.clone() + }; + + let label = Text::new(server_name, text_style) + .aligned() + .constrained() + .dynamically(move |constraint, _, _| { + gpui::SizeConstraint { + min: constraint.min, + max: vec2f( + constraint.max.x(), + constraint.min.y(), + ), + } + }); + + if Some(item_ix) == widest_completion_ix { + Some( + label + .contained() + .with_style( + style + .autocomplete + .server_name_container, + ) + .into_any(), + ) + } else { + Some(label.flex_float().into_any()) + } + })()) .into_any() } else { completion_label.into_any() } .contained() .with_style(item_style) + .constrained() + .dynamically( + move |constraint, _, _| { + if Some(item_ix) == widest_completion_ix { + constraint + } else { + gpui::SizeConstraint { + min: constraint.min, + max: constraint.min, + } + } + }, + ) }, ) .with_cursor_style(CursorStyle::PointingHand) @@ -941,19 +1015,7 @@ impl CompletionsMenu { } }, ) - .with_width_from_item( - self.matches - .iter() - .enumerate() - .max_by_key(|(_, mat)| { - self.completions[mat.candidate_id] - .label - .text - .chars() - .count() - }) - .map(|(ix, _)| ix), - ) + .with_width_from_item(widest_completion_ix) .contained() .with_style(container_style) .into_any() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4766f636f3..d692660738 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -775,6 +775,9 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, + pub server_name_container: ContainerStyle, + pub server_name_color: Color, + pub server_name_size_percent: f32, } #[derive(Clone, Copy, Default, Deserialize, JsonSchema)] diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9ad008f38d..0b99b6fba6 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -205,6 +205,9 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, + server_name_container: { padding: { left: 40 } }, + server_name_color: text(theme.middle, "sans", "disabled", {}).color, + server_name_size_percent: 0.75, }, diagnostic_header: { background: background(theme.middle), From df377d5195f980e87cd8dada82ab1db4adb76532 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 17:32:23 +0200 Subject: [PATCH 071/114] Use Inline Assist across the board --- crates/quick_action_bar/src/quick_action_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index da5a8e6d72..de4e8828b3 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -96,7 +96,7 @@ impl View for QuickActionBar { 2, "icons/radix/magic-wand.svg", false, - ("Generate code".into(), Some(Box::new(InlineAssist))), + ("Inline Assist".into(), Some(Box::new(InlineAssist))), cx, move |this, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { From 16422a06ad96909160b5eae36c1c771fce4c45d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 18:21:23 +0200 Subject: [PATCH 072/114] Remember whether include conversation was toggled --- crates/ai/src/assistant.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ab60d108f0..4aca6ae626 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -399,6 +399,11 @@ impl AssistantPanel { InlineAssistantEvent::Dismissed => { self.hide_inline_assist(assist_id, cx); } + InlineAssistantEvent::IncludeConversationToggled { + include_conversation, + } => { + self.include_conversation_in_next_inline_assist = *include_conversation; + } } } @@ -488,8 +493,6 @@ impl AssistantPanel { include_conversation: bool, cx: &mut ViewContext, ) { - self.include_conversation_in_next_inline_assist = include_conversation; - let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { @@ -2833,6 +2836,9 @@ enum InlineAssistantEvent { }, Canceled, Dismissed, + IncludeConversationToggled { + include_conversation: bool, + }, } #[derive(Copy, Clone)] @@ -2955,6 +2961,9 @@ impl InlineAssistant { cx: &mut ViewContext, ) { self.include_conversation = !self.include_conversation; + cx.emit(InlineAssistantEvent::IncludeConversationToggled { + include_conversation: self.include_conversation, + }); cx.notify(); } } From 87e25c8c238b8d4f63431452c057a0a387f6d8ee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 18:21:35 +0200 Subject: [PATCH 073/114] Use model from conversation when available --- crates/ai/src/assistant.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4aca6ae626..46756ad569 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -581,9 +581,6 @@ impl AssistantPanel { None }; let language_name = language_name.as_deref(); - let model = settings::get::(cx) - .default_open_ai_model - .clone(); let mut prompt = String::new(); if let Some(language_name) = language_name { @@ -673,25 +670,30 @@ impl AssistantPanel { writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); writeln!(prompt, "Never make remarks about the output.").unwrap(); - let mut request = OpenAIRequest { - model: model.full_name().into(), - messages: Vec::new(), - stream: true, - }; + let mut messages = Vec::new(); + let mut model = settings::get::(cx) + .default_open_ai_model + .clone(); if let Some(conversation) = conversation { let conversation = conversation.read(cx); let buffer = conversation.buffer.read(cx); - request.messages.extend( + messages.extend( conversation .messages(cx) .map(|message| message.to_open_ai_message(buffer)), ); + model = conversation.model.clone(); } - request.messages.push(RequestMessage { + messages.push(RequestMessage { role: Role::User, content: prompt, }); + let request = OpenAIRequest { + model: model.full_name().into(), + messages, + stream: true, + }; let response = stream_completion(api_key, cx.background().clone(), request); let editor = editor.downgrade(); From 100870aa9c1636f977f1eb5e10b94593d3275c49 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Aug 2023 20:32:24 +0300 Subject: [PATCH 074/114] Do not blink the cursor if Zed window is focused away co-authored-by: Max --- crates/editor/src/blink_manager.rs | 13 ++++++++----- crates/editor/src/editor.rs | 10 ++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/blink_manager.rs b/crates/editor/src/blink_manager.rs index 24ea4774aa..fa5a3af0c6 100644 --- a/crates/editor/src/blink_manager.rs +++ b/crates/editor/src/blink_manager.rs @@ -37,10 +37,7 @@ impl BlinkManager { } pub fn pause_blinking(&mut self, cx: &mut ModelContext) { - if !self.visible { - self.visible = true; - cx.notify(); - } + self.show_cursor(cx); let epoch = self.next_blink_epoch(); let interval = self.blink_interval; @@ -82,7 +79,13 @@ impl BlinkManager { }) .detach(); } - } else if !self.visible { + } else { + self.show_cursor(cx); + } + } + + pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) { + if !self.visible { self.visible = true; cx.notify(); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fe8e4e338c..8a432a6e4f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1454,6 +1454,16 @@ impl Editor { cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe_global::(Self::settings_changed), + cx.observe_window_activation(|editor, active, cx| { + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.show_cursor(cx); + blink_manager.disable(cx); + } + }); + }), ], }; From 0e6c91818f33aa69da759e4195ef899616a7c720 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 29 Aug 2023 15:37:51 -0400 Subject: [PATCH 075/114] Woooooops, don't notify the language server until initialized --- crates/project/src/project.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 63b6786d8c..800e0ae01c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2901,6 +2901,8 @@ impl Project { }) .detach(); + let language_server = language_server.initialize(initialization_options).await?; + language_server .notify::( lsp::DidChangeConfigurationParams { @@ -2909,9 +2911,7 @@ impl Project { ) .ok(); - Ok(Some( - language_server.initialize(initialization_options).await?, - )) + Ok(Some(language_server)) } fn insert_newly_running_language_server( From 0a14e33dba0bc26da975c6f0de4d1094c4508fac Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 15:59:35 -0400 Subject: [PATCH 076/114] Pull toolbar into it's own styletree --- styles/src/style_tree/toolbar.ts | 20 ++++++++++++++++++++ styles/src/style_tree/workspace.ts | 14 ++------------ 2 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 styles/src/style_tree/toolbar.ts diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts new file mode 100644 index 0000000000..32fd4ab1b5 --- /dev/null +++ b/styles/src/style_tree/toolbar.ts @@ -0,0 +1,20 @@ +import { useTheme } from "../common" +import { toggleable_icon_button } from "../component/icon_button" +import { background, border } from "./components" + +export const toolbar = () => { + const theme = useTheme() + + return { + height: 42, + background: background(theme.highest), + border: border(theme.highest, { bottom: true }), + item_spacing: 8, + toggleable_tool: toggleable_icon_button(theme, { + margin: { left: 8 }, + variant: "ghost", + active_color: "accent", + }), + padding: { left: 8, right: 8 }, + } +} diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 43a6cec585..7feccfc8bc 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -13,6 +13,7 @@ import { interactive } from "../element" import { titlebar } from "./titlebar" import { useTheme } from "../theme" import { toggleable_icon_button } from "../component/icon_button" +import { toolbar } from "./toolbar" export default function workspace(): any { const theme = useTheme() @@ -128,18 +129,7 @@ export default function workspace(): any { }, status_bar: statusBar(), titlebar: titlebar(), - toolbar: { - height: 42, - background: background(theme.highest), - border: border(theme.highest, { bottom: true }), - item_spacing: 8, - toggleable_tool: toggleable_icon_button(theme, { - margin: { left: 8 }, - variant: "ghost", - active_color: "accent", - }), - padding: { left: 8, right: 8 }, - }, + toolbar: toolbar(), breadcrumb_height: 24, breadcrumbs: interactive({ base: { From 33c9f1485215e3c33a9998b3babc023332fa43fa Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:01:08 -0400 Subject: [PATCH 077/114] Don't require passing `theme` to `toggleable_icon_button` --- styles/src/component/icon_button.ts | 5 +---- styles/src/style_tree/collab_panel.ts | 2 +- styles/src/style_tree/titlebar.ts | 8 ++++---- styles/src/style_tree/toolbar.ts | 2 +- styles/src/style_tree/workspace.ts | 1 - 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 935909afdb..dda6be83b2 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -77,10 +77,7 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO }) } -export function toggleable_icon_button( - theme: Theme, - { color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions -) { +export function toggleable_icon_button({ color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions) { if (!color) color = "base" return toggleable({ diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 07f367c8af..c4ffee889c 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -61,7 +61,7 @@ export default function contacts_panel(): any { width: 14, } - const header_icon_button = toggleable_icon_button(theme, { + const header_icon_button = toggleable_icon_button({ variant: "ghost", size: "sm", active_layer: theme.lowest, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 0a0b69e596..0565982eea 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -34,7 +34,7 @@ function call_controls() { } return { - toggle_microphone_button: toggleable_icon_button(theme, { + toggle_microphone_button: toggleable_icon_button({ margin: { ...margin_y, left: space.group, @@ -43,7 +43,7 @@ function call_controls() { active_color: "negative", }), - toggle_speakers_button: toggleable_icon_button(theme, { + toggle_speakers_button: toggleable_icon_button({ margin: { ...margin_y, left: space.half_item, @@ -51,7 +51,7 @@ function call_controls() { }, }), - screen_share_button: toggleable_icon_button(theme, { + screen_share_button: toggleable_icon_button({ margin: { ...margin_y, left: space.half_item, @@ -263,7 +263,7 @@ export function titlebar(): any { ...call_controls(), - toggle_contacts_button: toggleable_icon_button(theme, { + toggle_contacts_button: toggleable_icon_button({ margin: { left: ITEM_SPACING, }, diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index 32fd4ab1b5..b3f323eb9e 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -10,7 +10,7 @@ export const toolbar = () => { background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, - toggleable_tool: toggleable_icon_button(theme, { + toggleable_tool: toggleable_icon_button({ margin: { left: 8 }, variant: "ghost", active_color: "accent", diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 7feccfc8bc..c24bc3f770 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -12,7 +12,6 @@ import tabBar from "./tab_bar" import { interactive } from "../element" import { titlebar } from "./titlebar" import { useTheme } from "../theme" -import { toggleable_icon_button } from "../component/icon_button" import { toolbar } from "./toolbar" export default function workspace(): any { From d91a9615b5d80c3255f450cd5803025cc5364511 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:02:04 -0400 Subject: [PATCH 078/114] Format --- styles/src/build_themes.ts | 9 +- styles/src/build_tokens.ts | 4 +- styles/src/component/button.ts | 55 ++++++---- styles/src/component/icon_button.ts | 31 ++++-- styles/src/component/indicator.ts | 8 +- styles/src/component/input.ts | 2 +- styles/src/component/margin.ts | 23 ++-- styles/src/component/padding.ts | 23 ++-- styles/src/component/tab.ts | 20 ++-- styles/src/component/tab_bar_button.ts | 67 ++++++------ styles/src/component/text_button.ts | 54 ++++++---- styles/src/style_tree/assistant.ts | 69 ++++++------ styles/src/style_tree/collab_modals.ts | 23 ++-- styles/src/style_tree/collab_panel.ts | 12 +-- styles/src/style_tree/component_test.ts | 9 +- styles/src/style_tree/contacts_popover.ts | 1 - styles/src/style_tree/editor.ts | 2 +- styles/src/style_tree/feedback.ts | 2 +- styles/src/style_tree/picker.ts | 2 +- styles/src/style_tree/project_panel.ts | 16 +-- styles/src/style_tree/search.ts | 121 ++++++++++++++++------ styles/src/style_tree/status_bar.ts | 8 +- styles/src/style_tree/tab_bar.ts | 6 +- styles/src/style_tree/titlebar.ts | 4 +- styles/src/theme/create_theme.ts | 17 +-- styles/src/theme/tokens/theme.ts | 6 +- styles/tsconfig.json | 4 +- 27 files changed, 354 insertions(+), 244 deletions(-) diff --git a/styles/src/build_themes.ts b/styles/src/build_themes.ts index 17575663a1..4d262f8146 100644 --- a/styles/src/build_themes.ts +++ b/styles/src/build_themes.ts @@ -21,9 +21,7 @@ function clear_themes(theme_directory: string) { } } -const all_themes: Theme[] = themes.map((theme) => - create_theme(theme) -) +const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) function write_themes(themes: Theme[], output_directory: string) { clear_themes(output_directory) @@ -34,10 +32,7 @@ function write_themes(themes: Theme[], output_directory: string) { const style_tree = app() const style_tree_json = JSON.stringify(style_tree, null, 2) const temp_path = path.join(temp_directory, `${theme.name}.json`) - const out_path = path.join( - output_directory, - `${theme.name}.json` - ) + const out_path = path.join(output_directory, `${theme.name}.json`) fs.writeFileSync(temp_path, style_tree_json) fs.renameSync(temp_path, out_path) console.log(`- ${out_path} created`) diff --git a/styles/src/build_tokens.ts b/styles/src/build_tokens.ts index fd6aa18ced..3c52b6d989 100644 --- a/styles/src/build_tokens.ts +++ b/styles/src/build_tokens.ts @@ -83,8 +83,6 @@ function write_tokens(themes: Theme[], tokens_directory: string) { console.log(`- ${METADATA_FILE} created`) } -const all_themes: Theme[] = themes.map((theme) => - create_theme(theme) -) +const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) write_tokens(all_themes, TOKENS_DIRECTORY) diff --git a/styles/src/component/button.ts b/styles/src/component/button.ts index 3b554ae37a..e0e831b082 100644 --- a/styles/src/component/button.ts +++ b/styles/src/component/button.ts @@ -5,7 +5,7 @@ import { TextStyle, background } from "../style_tree/components" // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Button { export type Options = { - layer: Layer, + layer: Layer background: keyof Theme["lowest"] color: keyof Theme["lowest"] variant: Button.Variant @@ -16,13 +16,13 @@ export namespace Button { bottom?: number left?: number right?: number - }, + } states: { - enabled?: boolean, - hovered?: boolean, - pressed?: boolean, - focused?: boolean, - disabled?: boolean, + enabled?: boolean + hovered?: boolean + pressed?: boolean + focused?: boolean + disabled?: boolean } } @@ -38,26 +38,26 @@ export namespace Button { export const CORNER_RADIUS = 6 export const variant = { - Default: 'filled', - Outline: 'outline', - Ghost: 'ghost' + Default: "filled", + Outline: "outline", + Ghost: "ghost", } as const - export type Variant = typeof variant[keyof typeof variant] + export type Variant = (typeof variant)[keyof typeof variant] export const shape = { - Rectangle: 'rectangle', - Square: 'square' + Rectangle: "rectangle", + Square: "square", } as const - export type Shape = typeof shape[keyof typeof shape] + export type Shape = (typeof shape)[keyof typeof shape] export const size = { Small: "sm", - Medium: "md" + Medium: "md", } as const - export type Size = typeof size[keyof typeof size] + export type Size = (typeof size)[keyof typeof size] export type BaseStyle = { corder_radius: number @@ -67,8 +67,8 @@ export namespace Button { bottom: number left: number right: number - }, - margin: Button.Options['margin'] + } + margin: Button.Options["margin"] button_height: number } @@ -81,15 +81,18 @@ export namespace Button { shape: Button.shape.Rectangle, states: { hovered: true, - pressed: true - } + pressed: true, + }, } ): BaseStyle => { const theme = useTheme() const layer = options.layer ?? theme.middle const color = options.color ?? "base" - const background_color = options.variant === Button.variant.Ghost ? null : background(layer, options.background ?? color) + const background_color = + options.variant === Button.variant.Ghost + ? null + : background(layer, options.background ?? color) const m = { top: options.margin?.top ?? 0, @@ -106,8 +109,14 @@ export namespace Button { padding: { top: padding, bottom: padding, - left: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding, - right: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding + left: + options.shape === Button.shape.Rectangle + ? padding + Button.RECTANGLE_PADDING + : padding, + right: + options.shape === Button.shape.Rectangle + ? padding + Button.RECTANGLE_PADDING + : padding, }, margin: m, button_height: 16, diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index dda6be83b2..38729b044c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -11,10 +11,7 @@ export type Margin = { } interface IconButtonOptions { - layer?: - | Theme["lowest"] - | Theme["middle"] - | Theme["highest"] + layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] color?: keyof Theme["lowest"] margin?: Partial variant?: Button.Variant @@ -26,15 +23,20 @@ type ToggleableIconButtonOptions = IconButtonOptions & { active_layer?: Layer } -export function icon_button({ color, margin, layer, variant, size }: IconButtonOptions = { - variant: Button.variant.Default, - size: Button.size.Medium, -}) { +export function icon_button( + { color, margin, layer, variant, size }: IconButtonOptions = { + variant: Button.variant.Default, + size: Button.size.Medium, + } +) { const theme = useTheme() if (!color) color = "base" - const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) + const background_color = + variant === Button.variant.Ghost + ? null + : background(layer ?? theme.lowest, color) const m = { top: margin?.top ?? 0, @@ -77,7 +79,14 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO }) } -export function toggleable_icon_button({ color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions) { +export function toggleable_icon_button({ + color, + active_color, + margin, + variant, + size, + active_layer, +}: ToggleableIconButtonOptions) { if (!color) color = "base" return toggleable({ @@ -87,7 +96,7 @@ export function toggleable_icon_button({ color, active_color, margin, variant, s color: active_color ? active_color : color, margin, layer: active_layer, - size + size, }), }, }) diff --git a/styles/src/component/indicator.ts b/styles/src/component/indicator.ts index 81a3b40da7..b3d2105f6a 100644 --- a/styles/src/component/indicator.ts +++ b/styles/src/component/indicator.ts @@ -1,7 +1,13 @@ import { foreground } from "../style_tree/components" import { Layer, StyleSets } from "../theme" -export const indicator = ({ layer, color }: { layer: Layer, color: StyleSets }) => ({ +export const indicator = ({ + layer, + color, +}: { + layer: Layer + color: StyleSets +}) => ({ corner_radius: 4, padding: 4, margin: { top: 12, left: 12 }, diff --git a/styles/src/component/input.ts b/styles/src/component/input.ts index cadfcc8d4a..5921210f88 100644 --- a/styles/src/component/input.ts +++ b/styles/src/component/input.ts @@ -18,6 +18,6 @@ export const input = () => { bottom: 3, left: 12, right: 8, - } + }, } } diff --git a/styles/src/component/margin.ts b/styles/src/component/margin.ts index f6262405f0..5bbdd646a8 100644 --- a/styles/src/component/margin.ts +++ b/styles/src/component/margin.ts @@ -16,19 +16,26 @@ export type MarginStyle = { export const margin_style = (options: MarginOptions): MarginStyle => { const { all, top, bottom, left, right } = options - if (all !== undefined) return { - top: all, - bottom: all, - left: all, - right: all - } + if (all !== undefined) + return { + top: all, + bottom: all, + left: all, + right: all, + } - if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value") + if ( + top === undefined && + bottom === undefined && + left === undefined && + right === undefined + ) + throw new Error("Margin must have at least one value") return { top: top || 0, bottom: bottom || 0, left: left || 0, - right: right || 0 + right: right || 0, } } diff --git a/styles/src/component/padding.ts b/styles/src/component/padding.ts index 96792bf766..b94e263922 100644 --- a/styles/src/component/padding.ts +++ b/styles/src/component/padding.ts @@ -16,19 +16,26 @@ export type PaddingStyle = { export const padding_style = (options: PaddingOptions): PaddingStyle => { const { all, top, bottom, left, right } = options - if (all !== undefined) return { - top: all, - bottom: all, - left: all, - right: all - } + if (all !== undefined) + return { + top: all, + bottom: all, + left: all, + right: all, + } - if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value") + if ( + top === undefined && + bottom === undefined && + left === undefined && + right === undefined + ) + throw new Error("Padding must have at least one value") return { top: top || 0, bottom: bottom || 0, left: left || 0, - right: right || 0 + right: right || 0, } } diff --git a/styles/src/component/tab.ts b/styles/src/component/tab.ts index 9938fb9311..6f73b6f3fb 100644 --- a/styles/src/component/tab.ts +++ b/styles/src/component/tab.ts @@ -9,7 +9,7 @@ type TabProps = { export const tab = ({ layer }: TabProps) => { const active_color = text(layer, "sans", "base").color const inactive_border: Border = { - color: '#FFFFFF00', + color: "#FFFFFF00", width: 1, bottom: true, left: false, @@ -27,7 +27,7 @@ export const tab = ({ layer }: TabProps) => { top: 8, left: 8, right: 8, - bottom: 6 + bottom: 6, }, border: inactive_border, } @@ -35,17 +35,17 @@ export const tab = ({ layer }: TabProps) => { const i = interactive({ state: { default: { - ...base + ...base, }, hovered: { ...base, - ...text(layer, "sans", "base", "hovered") + ...text(layer, "sans", "base", "hovered"), }, clicked: { ...base, - ...text(layer, "sans", "base", "pressed") + ...text(layer, "sans", "base", "pressed"), }, - } + }, }) return toggleable({ @@ -60,14 +60,14 @@ export const tab = ({ layer }: TabProps) => { hovered: { ...i, ...text(layer, "sans", "base", "hovered"), - border: active_border + border: active_border, }, clicked: { ...i, ...text(layer, "sans", "base", "pressed"), - border: active_border + border: active_border, }, - } - } + }, + }, }) } diff --git a/styles/src/component/tab_bar_button.ts b/styles/src/component/tab_bar_button.ts index 0c43e7010e..9e7f9acfc3 100644 --- a/styles/src/component/tab_bar_button.ts +++ b/styles/src/component/tab_bar_button.ts @@ -12,44 +12,47 @@ type TabBarButtonProps = TabBarButtonOptions & { state?: Partial>> } -export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) { +export function tab_bar_button( + theme: Theme, + { icon, color = "base" }: TabBarButtonProps +) { const button_spacing = 8 - return ( - interactive({ - base: { - icon: { - color: foreground(theme.middle, color), - asset: icon, - dimensions: { - width: 15, - height: 15, - }, + return interactive({ + base: { + icon: { + color: foreground(theme.middle, color), + asset: icon, + dimensions: { + width: 15, + height: 15, }, + }, + container: { + corner_radius: 4, + padding: { + top: 4, + bottom: 4, + left: 4, + right: 4, + }, + margin: { + left: button_spacing / 2, + right: button_spacing / 2, + }, + }, + }, + state: { + hovered: { container: { - corner_radius: 4, - padding: { - top: 4, bottom: 4, left: 4, right: 4 - }, - margin: { - left: button_spacing / 2, - right: button_spacing / 2, - }, + background: background(theme.middle, color, "hovered"), }, }, - state: { - hovered: { - container: { - background: background(theme.middle, color, "hovered"), - - } - }, - clicked: { - container: { - background: background(theme.middle, color, "pressed"), - } + clicked: { + container: { + background: background(theme.middle, color, "pressed"), }, }, - }) - ) + }, + }) } diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index ead017a803..b73d20ff1a 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -10,10 +10,7 @@ import { Button } from "./button" import { Margin } from "./icon_button" interface TextButtonOptions { - layer?: - | Theme["lowest"] - | Theme["middle"] - | Theme["highest"] + layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial @@ -36,7 +33,10 @@ export function text_button({ const theme = useTheme() if (!color) color = "base" - const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) + const background_color = + variant === Button.variant.Ghost + ? null + : background(layer ?? theme.lowest, color) const text_options: TextProperties = { size: "xs", @@ -67,20 +67,38 @@ export function text_button({ state: { default: { background: background_color, - color: - disabled - ? foreground(layer ?? theme.lowest, "disabled") - : foreground(layer ?? theme.lowest, color), - }, - hovered: - disabled ? {} : { - background: background(layer ?? theme.lowest, color, "hovered"), - color: foreground(layer ?? theme.lowest, color, "hovered"), - }, - clicked: disabled ? {} : { - background: background(layer ?? theme.lowest, color, "pressed"), - color: foreground(layer ?? theme.lowest, color, "pressed"), + color: disabled + ? foreground(layer ?? theme.lowest, "disabled") + : foreground(layer ?? theme.lowest, color), }, + hovered: disabled + ? {} + : { + background: background( + layer ?? theme.lowest, + color, + "hovered" + ), + color: foreground( + layer ?? theme.lowest, + color, + "hovered" + ), + }, + clicked: disabled + ? {} + : { + background: background( + layer ?? theme.lowest, + color, + "pressed" + ), + color: foreground( + layer ?? theme.lowest, + color, + "pressed" + ), + }, }, }) } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index cfc1f8d813..7df5434f91 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -8,50 +8,48 @@ type RoleCycleButton = TextStyle & { } // TODO: Replace these with zed types type RemainingTokens = TextStyle & { - background: string, - margin: { top: number, right: number }, + background: string + margin: { top: number; right: number } padding: { - right: number, - left: number, - top: number, - bottom: number, - }, - corner_radius: number, + right: number + left: number + top: number + bottom: number + } + corner_radius: number } export default function assistant(): any { const theme = useTheme() - const interactive_role = (color: StyleSets): Interactive => { - return ( - interactive({ - base: { + const interactive_role = ( + color: StyleSets + ): Interactive => { + return interactive({ + base: { + ...text(theme.highest, "sans", color, { size: "sm" }), + }, + state: { + hovered: { ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "hovered"), }, - state: { - hovered: { - ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "hovered"), - }, - clicked: { - ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "pressed"), - } + clicked: { + ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "pressed"), }, - }) - ) + }, + }) } const tokens_remaining = (color: StyleSets): RemainingTokens => { - return ( - { - ...text(theme.highest, "mono", color, { size: "xs" }), - background: background(theme.highest, "on", "default"), - margin: { top: 12, right: 20 }, - padding: { right: 4, left: 4, top: 1, bottom: 1 }, - corner_radius: 6, - } - ) + return { + ...text(theme.highest, "mono", color, { size: "xs" }), + background: background(theme.highest, "on", "default"), + margin: { top: 12, right: 20 }, + padding: { right: 4, left: 4, top: 1, bottom: 1 }, + corner_radius: 6, + } } return { @@ -93,7 +91,10 @@ export default function assistant(): any { base: { background: background(theme.middle), padding: { top: 4, bottom: 4 }, - border: border(theme.middle, "default", { top: true, overlay: true }), + border: border(theme.middle, "default", { + top: true, + overlay: true, + }), }, state: { hovered: { @@ -101,7 +102,7 @@ export default function assistant(): any { }, clicked: { background: background(theme.middle, "pressed"), - } + }, }, }), saved_at: { diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index 0f50e01a39..f9b22b6867 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -39,7 +39,12 @@ export default function channel_modal(): any { row_height: ITEM_HEIGHT, header: { background: background(theme.lowest), - border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }), + border: border(theme.middle, { + bottom: true, + top: false, + left: false, + right: false, + }), padding: { top: SPACING, left: SPACING - BUTTON_OFFSET, @@ -48,7 +53,7 @@ export default function channel_modal(): any { corner_radii: { top_right: 12, top_left: 12, - } + }, }, body: { background: background(theme.middle), @@ -57,12 +62,11 @@ export default function channel_modal(): any { left: SPACING, right: SPACING, bottom: SPACING, - }, corner_radii: { bottom_right: 12, bottom_left: 12, - } + }, }, modal: { background: background(theme.middle), @@ -74,7 +78,6 @@ export default function channel_modal(): any { right: 0, top: 0, }, - }, // FIXME: due to a bug in the picker's size calculation, this must be 600 max_height: 600, @@ -83,7 +86,7 @@ export default function channel_modal(): any { ...text(theme.middle, "sans", "on", { size: "lg" }), padding: { left: BUTTON_OFFSET, - } + }, }, picker: { empty_container: {}, @@ -108,8 +111,8 @@ export default function channel_modal(): any { background: background(theme.middle), padding: { left: 7, - right: 7 - } + right: 7, + }, }, cancel_invite_button: { ...text(theme.middle, "sans", { size: "xs" }), @@ -125,7 +128,7 @@ export default function channel_modal(): any { padding: { left: 4, right: 4, - } + }, }, contact_avatar: { corner_radius: 10, @@ -147,6 +150,6 @@ export default function channel_modal(): any { background: background(theme.middle, "disabled"), color: foreground(theme.middle, "disabled"), }, - } + }, } } diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index c4ffee889c..4d605d118c 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -27,7 +27,7 @@ export default function contacts_panel(): any { color: foreground(layer, "on"), icon_width: 14, button_width: 16, - corner_radius: 8 + corner_radius: 8, } const project_row = { @@ -275,7 +275,7 @@ export default function contacts_panel(): any { list_empty_label_container: { margin: { left: NAME_MARGIN, - } + }, }, list_empty_icon: { color: foreground(layer, "variant"), @@ -289,7 +289,7 @@ export default function contacts_panel(): any { top: SPACING / 2, bottom: SPACING / 2, left: SPACING, - right: SPACING + right: SPACING, }, }, state: { @@ -330,7 +330,7 @@ export default function contacts_panel(): any { right: 4, }, background: background(layer, "hovered"), - ...text(layer, "sans", "hovered", { size: "xs" }) + ...text(layer, "sans", "hovered", { size: "xs" }), }, contact_status_free: indicator({ layer, color: "positive" }), contact_status_busy: indicator({ layer, color: "negative" }), @@ -404,7 +404,7 @@ export default function contacts_panel(): any { channel_editor: { padding: { left: NAME_MARGIN, - } - } + }, + }, } } diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index e2bb0915c1..71057c67ea 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -1,4 +1,3 @@ - import { useTheme } from "../common" import { text_button } from "../component/text_button" import { icon_button } from "../component/icon_button" @@ -14,14 +13,14 @@ export default function contacts_panel(): any { base: text_button({}), state: { active: { - ...text_button({ color: "accent" }) - } - } + ...text_button({ color: "accent" }), + }, + }, }), disclosure: { ...text(theme.lowest, "sans", "base"), button: icon_button({ variant: "ghost" }), spacing: 4, - } + }, } } diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts index 0e76bbb38a..dcd84c3252 100644 --- a/styles/src/style_tree/contacts_popover.ts +++ b/styles/src/style_tree/contacts_popover.ts @@ -3,5 +3,4 @@ import { background, border } from "./components" export default function contacts_popover(): any { const theme = useTheme() - } diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9277a2e7a1..5a662098e8 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -307,7 +307,7 @@ export default function editor(): any { ? with_opacity(theme.ramps.green(0.5).hex(), 0.8) : with_opacity(theme.ramps.green(0.4).hex(), 0.8), }, - selections: foreground(layer, "accent") + selections: foreground(layer, "accent"), }, composition_mark: { underline: { diff --git a/styles/src/style_tree/feedback.ts b/styles/src/style_tree/feedback.ts index b1bd96e165..0349359533 100644 --- a/styles/src/style_tree/feedback.ts +++ b/styles/src/style_tree/feedback.ts @@ -37,7 +37,7 @@ export default function feedback(): any { ...text(theme.highest, "mono", "on", "disabled"), background: background(theme.highest, "on", "disabled"), border: border(theme.highest, "on", "disabled"), - } + }, }, }), button_margin: 8, diff --git a/styles/src/style_tree/picker.ts b/styles/src/style_tree/picker.ts index 28ae854787..317f600b1e 100644 --- a/styles/src/style_tree/picker.ts +++ b/styles/src/style_tree/picker.ts @@ -152,7 +152,7 @@ export default function picker(): any { 0.5 ), }, - } + }, }), } } diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index e239f9a840..51958af145 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -64,17 +64,17 @@ export default function project_panel(): any { const unselected_default_style = merge( base_properties, unselected?.default ?? {}, - {}, + {} ) const unselected_hovered_style = merge( base_properties, { background: background(theme.middle, "hovered") }, - unselected?.hovered ?? {}, + unselected?.hovered ?? {} ) const unselected_clicked_style = merge( base_properties, { background: background(theme.middle, "pressed") }, - unselected?.clicked ?? {}, + unselected?.clicked ?? {} ) const selected_default_style = merge( base_properties, @@ -82,7 +82,7 @@ export default function project_panel(): any { background: background(theme.lowest), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.default ?? {}, + selected_style?.default ?? {} ) const selected_hovered_style = merge( base_properties, @@ -90,7 +90,7 @@ export default function project_panel(): any { background: background(theme.lowest, "hovered"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.hovered ?? {}, + selected_style?.hovered ?? {} ) const selected_clicked_style = merge( base_properties, @@ -98,7 +98,7 @@ export default function project_panel(): any { background: background(theme.lowest, "pressed"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.clicked ?? {}, + selected_style?.clicked ?? {} ) return toggleable({ @@ -175,7 +175,7 @@ export default function project_panel(): any { default: { icon_color: foreground(theme.middle, "variant"), }, - }, + } ), cut_entry: entry( { @@ -190,7 +190,7 @@ export default function project_panel(): any { size: "sm", }), }, - }, + } ), filename_editor: { background: background(theme.middle, "on"), diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c37a4e4b9a..3afa1932a4 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -60,7 +60,8 @@ export default function search(): any { corner_radius: 2, margin: { right: 2 }, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, padding: { left: 4, @@ -74,14 +75,16 @@ export default function search(): any { ...text(theme.highest, "mono", "variant", "hovered"), background: background(theme.highest, "on", "hovered"), border: { - width: 1., color: background(theme.highest, "on", "hovered") + 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") + width: 1, + color: background(theme.highest, "on", "pressed"), }, }, }, @@ -96,11 +99,19 @@ export default function search(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered" + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed" + ), border: border(theme.highest, "accent", "pressed"), }, }, @@ -117,7 +128,8 @@ export default function search(): any { corner_radius: 2, margin: { right: 2 }, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, padding: { left: 4, @@ -131,14 +143,16 @@ export default function search(): any { ...text(theme.highest, "mono", "variant", "hovered"), background: background(theme.highest, "on", "hovered"), border: { - width: 1., color: background(theme.highest, "on", "hovered") + 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") + width: 1, + color: background(theme.highest, "on", "pressed"), }, }, }, @@ -153,11 +167,19 @@ export default function search(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered" + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed" + ), border: border(theme.highest, "accent", "pressed"), }, }, @@ -168,9 +190,20 @@ export default function search(): any { // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ state: { - inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }), - active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }) - } + inactive: text_button({ + variant: "ghost", + layer: theme.highest, + disabled: true, + margin: { right: SEARCH_ROW_SPACING }, + text_properties: { size: "sm" }, + }), + active: text_button({ + variant: "ghost", + layer: theme.highest, + margin: { right: SEARCH_ROW_SPACING }, + text_properties: { size: "sm" }, + }), + }, }), editor, invalid_editor: { @@ -216,12 +249,12 @@ export default function search(): any { dimensions: { width: 14, height: 14, - } + }, }, container: { margin: { right: 4 }, padding: { left: 1, right: 1 }, - } + }, }, // Toggle group buttons - Text | Regex | Semantic mode_button: toggleable({ @@ -233,7 +266,7 @@ export default function search(): any { border: { ...border(theme.highest, "on"), left: false, - right: false + right: false, }, margin: { top: 1, @@ -247,13 +280,25 @@ export default function search(): any { }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered", { size: "sm" }), - background: background(theme.highest, "variant", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered", { + size: "sm", + }), + background: background( + theme.highest, + "variant", + "hovered" + ), border: border(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }), - background: background(theme.highest, "variant", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed", { + size: "sm", + }), + background: background( + theme.highest, + "variant", + "pressed" + ), border: border(theme.highest, "on", "pressed"), }, }, @@ -262,15 +307,19 @@ export default function search(): any { active: { default: { ...text(theme.highest, "mono", "on", { size: "sm" }), - background: background(theme.highest, "on") + background: background(theme.highest, "on"), }, hovered: { - ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }), - background: background(theme.highest, "on", "hovered") + ...text(theme.highest, "mono", "on", "hovered", { + size: "sm", + }), + background: background(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }), - background: background(theme.highest, "on", "pressed") + ...text(theme.highest, "mono", "on", "pressed", { + size: "sm", + }), + background: background(theme.highest, "on", "pressed"), }, }, }, @@ -300,8 +349,8 @@ export default function search(): any { }, }, state: { - hovered: {} - } + hovered: {}, + }, }), active: interactive({ base: { @@ -325,22 +374,30 @@ export default function search(): any { state: { hovered: { ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), + background: background( + theme.highest, + "on", + "hovered" + ), border: border(theme.highest, "on", "hovered"), }, clicked: { ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), + background: background( + theme.highest, + "on", + "pressed" + ), border: border(theme.highest, "on", "pressed"), }, }, - }) - } + }), + }, }), search_bar_row_height: 34, search_row_spacing: 8, option_button_height: 22, modes_container: {}, - ...search_results() + ...search_results(), } } diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 2d3b81f7c2..3b5ebf2c88 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -34,9 +34,11 @@ export default function status_bar(): any { ...text(layer, "mono", "base", { size: "xs" }), }, active_language: text_button({ - color: "base" + color: "base", + }), + auto_update_progress_message: text(layer, "sans", "base", { + size: "xs", }), - auto_update_progress_message: text(layer, "sans", "base", { size: "xs" }), auto_update_done_message: text(layer, "sans", "base", { size: "xs" }), lsp_status: interactive({ base: { @@ -125,7 +127,7 @@ export default function status_bar(): any { }, clicked: { background: background(layer, "pressed"), - } + }, }, }), state: { diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 129bd17869..23ff03a6a3 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -93,7 +93,7 @@ export default function tab_bar(): any { border: border(theme.lowest, "on", { bottom: true, overlay: true, - }) + }), }, state: { hovered: { @@ -101,7 +101,7 @@ export default function tab_bar(): any { background: background(theme.highest, "on", "hovered"), }, disabled: { - color: foreground(theme.highest, "on", "disabled") + color: foreground(theme.highest, "on", "disabled"), }, }, }) @@ -162,6 +162,6 @@ export default function tab_bar(): any { right: false, }, }, - nav_button: nav_button + nav_button: nav_button, } } diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 0565982eea..e4e274684c 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -187,10 +187,10 @@ export function titlebar(): any { project_name_divider: text(theme.lowest, "sans", "variant"), project_menu_button: toggleable_text_button(theme, { - color: 'base', + color: "base", }), git_menu_button: toggleable_text_button(theme, { - color: 'variant', + color: "variant", }), // Collaborators diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index ab3c96f280..61471616fb 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -13,16 +13,16 @@ export interface Theme { is_light: boolean /** - * App background, other elements that should sit directly on top of the background. - */ + * App background, other elements that should sit directly on top of the background. + */ lowest: Layer /** - * Panels, tabs, other UI surfaces that sit on top of the background. - */ + * Panels, tabs, other UI surfaces that sit on top of the background. + */ middle: Layer /** - * Editors like code buffers, conversation editors, etc. - */ + * Editors like code buffers, conversation editors, etc. + */ highest: Layer ramps: RampSet @@ -206,7 +206,10 @@ function build_color_family(ramps: RampSet): ColorFamily { for (const ramp in ramps) { const ramp_value = ramps[ramp as keyof RampSet] - const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100] + const lightnessValues = [ + ramp_value(0).get("hsl.l") * 100, + ramp_value(1).get("hsl.l") * 100, + ] const low = Math.min(...lightnessValues) const high = Math.max(...lightnessValues) const range = high - low diff --git a/styles/src/theme/tokens/theme.ts b/styles/src/theme/tokens/theme.ts index f759bc8139..e2c3bb33d3 100644 --- a/styles/src/theme/tokens/theme.ts +++ b/styles/src/theme/tokens/theme.ts @@ -4,11 +4,7 @@ import { SingleOtherToken, TokenTypes, } from "@tokens-studio/types" -import { - Shadow, - SyntaxHighlightStyle, - ThemeSyntax, -} from "../create_theme" +import { Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../create_theme" import { LayerToken, layer_token } from "./layer" import { PlayersToken, players_token } from "./players" import { color_token } from "./token" diff --git a/styles/tsconfig.json b/styles/tsconfig.json index c7eaa50eed..940442e1b7 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -23,7 +23,5 @@ "skipLibCheck": true, "useUnknownInCatchVariables": false }, - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From 93cf52a719eb3778623e9db3d4130803bf55ae1c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:10:40 -0400 Subject: [PATCH 079/114] Update toolbar active state style --- styles/src/component/icon_button.ts | 22 +++++++++++++++------- styles/src/style_tree/titlebar.ts | 2 ++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 38729b044c..b20c81df68 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -13,6 +13,7 @@ export type Margin = { interface IconButtonOptions { layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] color?: keyof Theme["lowest"] + background_color?: keyof Theme["lowest"] margin?: Partial variant?: Button.Variant size?: Button.Size @@ -20,11 +21,13 @@ interface IconButtonOptions { type ToggleableIconButtonOptions = IconButtonOptions & { active_color?: keyof Theme["lowest"] + active_background_color?: keyof Theme["lowest"] active_layer?: Layer + active_variant?: Button.Variant } export function icon_button( - { color, margin, layer, variant, size }: IconButtonOptions = { + { color, background_color, margin, layer, variant, size }: IconButtonOptions = { variant: Button.variant.Default, size: Button.size.Medium, } @@ -33,10 +36,10 @@ export function icon_button( if (!color) color = "base" - const background_color = + const default_background = variant === Button.variant.Ghost ? null - : background(layer ?? theme.lowest, color) + : background(layer ?? theme.lowest, background_color ?? color) const m = { top: margin?.top ?? 0, @@ -64,15 +67,15 @@ export function icon_button( }, state: { default: { - background: background_color, + background: default_background, color: foreground(layer ?? theme.lowest, color), }, hovered: { - background: background(layer ?? theme.lowest, color, "hovered"), + background: background(layer ?? theme.lowest, background_color ?? color, "hovered"), color: foreground(layer ?? theme.lowest, color, "hovered"), }, clicked: { - background: background(layer ?? theme.lowest, color, "pressed"), + background: background(layer ?? theme.lowest, background_color ?? color, "pressed"), color: foreground(layer ?? theme.lowest, color, "pressed"), }, }, @@ -81,7 +84,10 @@ export function icon_button( export function toggleable_icon_button({ color, + background_color, active_color, + active_background_color, + active_variant, margin, variant, size, @@ -91,11 +97,13 @@ export function toggleable_icon_button({ return toggleable({ state: { - inactive: icon_button({ color, margin, variant, size }), + inactive: icon_button({ color, background_color, margin, variant, size }), active: icon_button({ color: active_color ? active_color : color, + background_color: active_background_color ? active_background_color : background_color, margin, layer: active_layer, + variant: active_variant || variant, size, }), }, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index e4e274684c..9fb439f618 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -41,6 +41,7 @@ function call_controls() { right: space.half_item, }, active_color: "negative", + active_background_color: "negative", }), toggle_speakers_button: toggleable_icon_button({ @@ -58,6 +59,7 @@ function call_controls() { right: space.group, }, active_color: "accent", + active_background_color: "accent", }), muted: foreground(theme.lowest, "negative"), From c9b12370790d679da816ea8365d9da550d22f67d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:20:19 -0400 Subject: [PATCH 080/114] Update titlebar size --- styles/src/component/icon_button.ts | 2 +- styles/src/style_tree/search.ts | 10 +++++----- styles/src/style_tree/toolbar.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index b20c81df68..5b7c61b17c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -60,7 +60,7 @@ export function icon_button( corner_radius: 6, padding: padding, margin: m, - icon_width: 12, + icon_width: 14, icon_height: 14, button_width: size === Button.size.Small ? 16 : 20, button_height: 14, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 3afa1932a4..a93aab4ea8 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -31,7 +31,7 @@ export default function search(): any { text: text(theme.highest, "mono", "default"), border: border(theme.highest), margin: { - right: 9, + right: SEARCH_ROW_SPACING, }, padding: { top: 4, @@ -48,7 +48,7 @@ export default function search(): any { } return { - padding: { top: 4, bottom: 4 }, + padding: { top: 0, bottom: 0 }, option_button: toggleable({ base: interactive({ @@ -273,8 +273,8 @@ export default function search(): any { bottom: 1, }, padding: { - left: 12, - right: 12, + left: 10, + right: 10, }, corner_radius: 6, }, @@ -394,7 +394,7 @@ export default function search(): any { }), }, }), - search_bar_row_height: 34, + search_bar_row_height: 32, search_row_spacing: 8, option_button_height: 22, modes_container: {}, diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index b3f323eb9e..39f11f58bf 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -6,15 +6,15 @@ export const toolbar = () => { const theme = useTheme() return { - height: 42, + height: 32, + padding: { left: 4, right: 4, top: 4, bottom: 4 }, background: background(theme.highest), border: border(theme.highest, { bottom: true }), - item_spacing: 8, + item_spacing: 4, toggleable_tool: toggleable_icon_button({ - margin: { left: 8 }, + margin: { left: 4 }, variant: "ghost", active_color: "accent", }), - padding: { left: 8, right: 8 }, } } From 2af5fc503061935d5ed8a316c26eea55bf15705e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:37:48 -0400 Subject: [PATCH 081/114] Move breadcrumb style to toolbar --- crates/breadcrumbs/src/breadcrumbs.rs | 8 ++++---- crates/theme/src/theme.rs | 4 ++-- styles/src/style_tree/toolbar.ts | 20 +++++++++++++++++++- styles/src/style_tree/workspace.ts | 17 ----------------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 615e238648..41985edb75 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -50,7 +50,7 @@ impl View for Breadcrumbs { let not_editor = active_item.downcast::().is_none(); let theme = theme::current(cx).clone(); - let style = &theme.workspace.breadcrumbs; + let style = &theme.workspace.toolbar.breadcrumbs; let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { Some(breadcrumbs) => breadcrumbs, @@ -60,7 +60,7 @@ impl View for Breadcrumbs { .map(|breadcrumb| { Text::new( breadcrumb.text, - theme.workspace.breadcrumbs.default.text.clone(), + theme.workspace.toolbar.breadcrumbs.default.text.clone(), ) .with_highlights(breadcrumb.highlights.unwrap_or_default()) .into_any() @@ -68,10 +68,10 @@ impl View for Breadcrumbs { let crumbs = Flex::row() .with_children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" 〉 ", style.default.text.clone()).into_any() + Label::new(" › ", style.default.text.clone()).into_any() })) .constrained() - .with_height(theme.workspace.breadcrumb_height) + .with_height(theme.workspace.toolbar.breadcrumb_height) .contained(); if not_editor || !self.pane_focused { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a5faba8eaf..423bb879d2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -88,8 +88,6 @@ pub struct Workspace { pub dock: Dock, pub status_bar: StatusBar, pub toolbar: Toolbar, - pub breadcrumb_height: f32, - pub breadcrumbs: Interactive, pub disconnected_overlay: ContainedText, pub modal: ContainerStyle, pub zoomed_panel_foreground: ContainerStyle, @@ -411,6 +409,8 @@ pub struct Toolbar { pub height: f32, pub item_spacing: f32, pub toggleable_tool: Toggleable>, + pub breadcrumb_height: f32, + pub breadcrumbs: Interactive, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index 39f11f58bf..7292a220a8 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -1,6 +1,7 @@ import { useTheme } from "../common" import { toggleable_icon_button } from "../component/icon_button" -import { background, border } from "./components" +import { interactive } from "../element" +import { background, border, foreground, text } from "./components" export const toolbar = () => { const theme = useTheme() @@ -16,5 +17,22 @@ export const toolbar = () => { variant: "ghost", active_color: "accent", }), + breadcrumb_height: 24, + breadcrumbs: interactive({ + base: { + ...text(theme.highest, "sans", "variant"), + corner_radius: 6, + padding: { + left: 6, + right: 6, + }, + }, + state: { + hovered: { + color: foreground(theme.highest, "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + }, + }, + }), } } diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index c24bc3f770..ba89c7b05f 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -129,23 +129,6 @@ export default function workspace(): any { status_bar: statusBar(), titlebar: titlebar(), toolbar: toolbar(), - breadcrumb_height: 24, - breadcrumbs: interactive({ - base: { - ...text(theme.highest, "sans", "variant"), - corner_radius: 6, - padding: { - left: 6, - right: 6, - }, - }, - state: { - hovered: { - color: foreground(theme.highest, "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - }, - }, - }), disconnected_overlay: { ...text(theme.lowest, "sans"), background: with_opacity(background(theme.lowest), 0.8), From 97d187bba7a33ccecf708287987e8b69bcca9f9d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 19:50:27 -0400 Subject: [PATCH 082/114] Remove project divider in titlebar --- crates/collab_ui/src/collab_titlebar_item.rs | 8 ---- styles/src/component/index.ts | 6 +++ styles/src/component/text_button.ts | 44 ++++++++++---------- styles/src/element/index.ts | 2 + styles/src/{component => element}/margin.ts | 0 styles/src/{component => element}/padding.ts | 0 styles/src/style_tree/titlebar.ts | 12 ++---- styles/src/theme/index.ts | 1 + 8 files changed, 35 insertions(+), 38 deletions(-) create mode 100644 styles/src/component/index.ts rename styles/src/{component => element}/margin.ts (100%) rename styles/src/{component => element}/padding.ts (100%) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 684ddca08d..bd94c85f95 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -213,7 +213,6 @@ impl CollabTitlebarItem { .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH)); let project_style = theme.titlebar.project_menu_button.clone(); let git_style = theme.titlebar.git_menu_button.clone(); - let divider_style = theme.titlebar.project_name_divider.clone(); let item_spacing = theme.titlebar.item_spacing; let mut ret = Flex::row().with_child( @@ -249,13 +248,6 @@ impl CollabTitlebarItem { if let Some(git_branch) = branch_prepended { ret = ret.with_child( Flex::row() - .with_child( - Label::new("/", divider_style.text) - .contained() - .with_style(divider_style.container) - .aligned() - .left(), - ) .with_child( Stack::new() .with_child( diff --git a/styles/src/component/index.ts b/styles/src/component/index.ts new file mode 100644 index 0000000000..f2cbc7b26a --- /dev/null +++ b/styles/src/component/index.ts @@ -0,0 +1,6 @@ +export * from "./icon_button" +export * from "./indicator" +export * from "./input" +export * from "./tab" +export * from "./tab_bar_button" +export * from "./text_button" diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index b73d20ff1a..8333d9e81a 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -74,31 +74,31 @@ export function text_button({ hovered: disabled ? {} : { - background: background( - layer ?? theme.lowest, - color, - "hovered" - ), - color: foreground( - layer ?? theme.lowest, - color, - "hovered" - ), - }, + background: background( + layer ?? theme.lowest, + color, + "hovered" + ), + color: foreground( + layer ?? theme.lowest, + color, + "hovered" + ), + }, clicked: disabled ? {} : { - background: background( - layer ?? theme.lowest, - color, - "pressed" - ), - color: foreground( - layer ?? theme.lowest, - color, - "pressed" - ), - }, + background: background( + layer ?? theme.lowest, + color, + "pressed" + ), + color: foreground( + layer ?? theme.lowest, + color, + "pressed" + ), + }, }, }) } diff --git a/styles/src/element/index.ts b/styles/src/element/index.ts index d41b4e2cc3..0586399fb1 100644 --- a/styles/src/element/index.ts +++ b/styles/src/element/index.ts @@ -1,4 +1,6 @@ import { interactive, Interactive } from "./interactive" import { toggleable, Toggleable } from "./toggle" +export * from "./padding" +export * from "./margin" export { interactive, Interactive, toggleable, Toggleable } diff --git a/styles/src/component/margin.ts b/styles/src/element/margin.ts similarity index 100% rename from styles/src/component/margin.ts rename to styles/src/element/margin.ts diff --git a/styles/src/component/padding.ts b/styles/src/element/padding.ts similarity index 100% rename from styles/src/component/padding.ts rename to styles/src/element/padding.ts diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 9fb439f618..672907b22c 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -1,8 +1,6 @@ -import { icon_button, toggleable_icon_button } from "../component/icon_button" -import { toggleable_text_button } from "../component/text_button" +import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component" import { interactive, toggleable } from "../element" -import { useTheme } from "../theme" -import { with_opacity } from "../theme/color" +import { useTheme, with_opacity } from "../theme" import { background, border, foreground, text } from "./components" const ITEM_SPACING = 8 @@ -185,12 +183,10 @@ export function titlebar(): any { height: 400, }, - // Project - project_name_divider: text(theme.lowest, "sans", "variant"), - project_menu_button: toggleable_text_button(theme, { - color: "base", + color: "base" }), + git_menu_button: toggleable_text_button(theme, { color: "variant", }), diff --git a/styles/src/theme/index.ts b/styles/src/theme/index.ts index ca8aaa461f..47110940f5 100644 --- a/styles/src/theme/index.ts +++ b/styles/src/theme/index.ts @@ -23,3 +23,4 @@ export * from "./create_theme" export * from "./ramps" export * from "./syntax" export * from "./theme_config" +export * from "./color" From 6e964acd92f055ad67504517e3b0b96ad34d5c2a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 20:04:15 -0400 Subject: [PATCH 083/114] Fix extra theme entry --- crates/theme/src/theme.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 423bb879d2..8574357777 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -118,7 +118,6 @@ pub struct Titlebar { pub height: f32, pub menu: TitlebarMenu, pub project_menu_button: Toggleable>, - pub project_name_divider: ContainedText, pub git_menu_button: Toggleable>, pub item_spacing: f32, pub face_pile_spacing: f32, From e3a0252b04e6db8ae327dc55df4e1a667e07c549 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 29 Aug 2023 20:42:13 -0400 Subject: [PATCH 084/114] Make multi-server completion requests not serial --- crates/project/src/project.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 800e0ae01c..c672a37cad 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4438,21 +4438,27 @@ impl Project { let position = position.to_point_utf16(buffer.read(cx)); let server_ids: Vec<_> = self .language_servers_for_buffer(buffer.read(cx), cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) .map(|(_, server)| server.server_id()) .collect(); let buffer = buffer.clone(); cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(server_ids.len()); + this.update(&mut cx, |this, cx| { + for server_id in server_ids { + tasks.push(this.request_lsp( + buffer.clone(), + server_id, + GetCompletions { position }, + cx, + )); + } + }); + let mut completions = Vec::new(); - - for server_id in server_ids { - let new_completions = this - .update(&mut cx, |this, cx| { - this.request_lsp(buffer.clone(), server_id, GetCompletions { position }, cx) - }) - .await; - - if let Ok(new_completions) = new_completions { + for task in tasks { + if let Ok(new_completions) = task.await { completions.extend_from_slice(&new_completions); } } From 5c498c86103c390b6c5930641699fc4923ebc3a8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 11:04:48 +0200 Subject: [PATCH 085/114] Show inline assistant errors --- crates/ai/src/assistant.rs | 160 +++++++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 35 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 46756ad569..d19730172a 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -40,7 +40,7 @@ use std::{ cell::{Cell, RefCell}, cmp, env, fmt::Write, - iter, + future, iter, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -55,7 +55,7 @@ use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, searchable::Direction, - Save, ToggleZoom, Toolbar, Workspace, + Save, Toast, ToggleZoom, Toolbar, Workspace, }; actions!( @@ -290,6 +290,7 @@ impl AssistantPanel { has_focus: false, include_conversation: self.include_conversation_in_next_inline_assist, measurements: measurements.clone(), + error: None, }; cx.focus_self(); assistant @@ -331,7 +332,7 @@ impl AssistantPanel { editor: editor.downgrade(), range, highlighted_ranges: Default::default(), - inline_assistant_block_id: Some(block_id), + inline_assistant: Some((block_id, inline_assistant.clone())), code_generation: Task::ready(None), transaction_id: None, _subscriptions: vec![ @@ -477,7 +478,7 @@ impl AssistantPanel { fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { if let Some(editor) = pending_assist.editor.upgrade(cx) { - if let Some(block_id) = pending_assist.inline_assistant_block_id.take() { + if let Some((block_id, _)) = pending_assist.inline_assistant.take() { editor.update(cx, |editor, cx| { editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); }); @@ -699,22 +700,17 @@ impl AssistantPanel { pending_assist.code_generation = cx.spawn(|this, mut cx| { async move { - let _cleanup = util::defer({ - let mut cx = cx.clone(); - let this = this.clone(); - move || { - let _ = this.update(&mut cx, |this, cx| { - this.close_inline_assist(inline_assist_id, false, cx) - }); - } - }); - let mut edit_start = range.start.to_offset(&snapshot); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { let chunks = strip_markdown_codeblock(response.await?.filter_map( - |message| async move { message.ok()?.choices.pop()?.delta.content }, + |message| async move { + match message { + Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)), + Err(error) => Some(Err(error)), + } + }, )); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -737,6 +733,7 @@ impl AssistantPanel { let mut new_text = String::new(); while let Some(chunk) = chunks.next().await { + let chunk = chunk?; if first_chunk && (chunk.starts_with(' ') || chunk.starts_with('\t')) { autoindent = false; } @@ -771,9 +768,17 @@ impl AssistantPanel { }); while let Some(hunks) = hunks_rx.next().await { - let editor = editor - .upgrade(&cx) - .ok_or_else(|| anyhow!("editor was dropped"))?; + let editor = if let Some(editor) = editor.upgrade(&cx) { + editor + } else { + break; + }; + + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; this.update(&mut cx, |this, cx| { let pending_assist = if let Some(pending_assist) = @@ -840,9 +845,42 @@ impl AssistantPanel { }); this.update_highlights_for_editor(&editor, cx); - })?; + }); + } + + if let Err(error) = diff.await { + this.update(&mut cx, |this, cx| { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + if let Some((_, inline_assistant)) = + pending_assist.inline_assistant.as_ref() + { + inline_assistant.update(cx, |inline_assistant, cx| { + inline_assistant.set_error(error, cx); + }); + } else if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + inline_assist_id, + format!("Inline assistant error: {}", error), + ), + cx, + ); + }) + } + })?; + } else { + let _ = this.update(&mut cx, |this, cx| { + this.close_inline_assist(inline_assist_id, false, cx) + }); } - diff.await?; anyhow::Ok(()) } @@ -2856,6 +2894,7 @@ struct InlineAssistant { has_focus: bool, include_conversation: bool, measurements: Rc>, + error: Option, } impl Entity for InlineAssistant { @@ -2868,17 +2907,42 @@ impl View for InlineAssistant { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum ErrorIcon {} let theme = theme::current(cx); Flex::row() .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) - .element() + Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned(), + ) + .with_children(if let Some(error) = self.error.as_ref() { + Some( + Svg::new("icons/circle_x_mark_12.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }) .aligned() .constrained() .dynamically({ @@ -2954,6 +3018,8 @@ impl InlineAssistant { include_conversation: self.include_conversation, }); self.confirmed = true; + self.error = None; + cx.notify(); } } @@ -2968,6 +3034,19 @@ impl InlineAssistant { }); cx.notify(); } + + fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext) { + self.error = Some(error); + self.confirmed = false; + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.set_field_editor_style( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + }); + cx.notify(); + } } // This wouldn't need to exist if we could pass parameters when rendering child views. @@ -2982,7 +3061,7 @@ struct PendingInlineAssist { editor: WeakViewHandle, range: Range, highlighted_ranges: Vec>, - inline_assistant_block_id: Option, + inline_assistant: Option<(BlockId, ViewHandle)>, code_generation: Task>, transaction_id: Option, _subscriptions: Vec, @@ -3010,23 +3089,29 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } -fn strip_markdown_codeblock(stream: impl Stream) -> impl Stream { +fn strip_markdown_codeblock( + stream: impl Stream>, +) -> impl Stream> { let mut first_line = true; let mut buffer = String::new(); let mut starts_with_fenced_code_block = false; stream.filter_map(move |chunk| { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => return future::ready(Some(Err(err))), + }; buffer.push_str(&chunk); if first_line { if buffer == "" || buffer == "`" || buffer == "``" { - return futures::future::ready(None); + return future::ready(None); } else if buffer.starts_with("```") { starts_with_fenced_code_block = true; if let Some(newline_ix) = buffer.find('\n') { buffer.replace_range(..newline_ix + 1, ""); first_line = false; } else { - return futures::future::ready(None); + return future::ready(None); } } } @@ -3050,10 +3135,10 @@ fn strip_markdown_codeblock(stream: impl Stream) -> impl Stream() .await, "Lorem ipsum dolor" ); assert_eq!( strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "```js\nLorem ipsum dolor\n```" ); assert_eq!( strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "``\nLorem ipsum dolor\n```" ); - fn chunks(text: &str, size: usize) -> impl Stream { + fn chunks(text: &str, size: usize) -> impl Stream> { stream::iter( text.chars() .collect::>() .chunks(size) - .map(|chunk| chunk.iter().collect::()) + .map(|chunk| Ok(chunk.iter().collect::())) .collect::>(), ) } From c6f439051131ddeca55f9c9c30a7cf3554cfbe00 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 11:30:51 +0200 Subject: [PATCH 086/114] Retain search history for inline assistants This only works in-memory for now. --- crates/ai/src/assistant.rs | 132 ++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index d19730172a..fd6e6c63eb 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -7,13 +7,13 @@ use crate::{ }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use collections::{hash_map, HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ display_map::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; @@ -106,6 +106,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(InlineAssistant::confirm); cx.add_action(InlineAssistant::cancel); cx.add_action(InlineAssistant::toggle_include_conversation); + cx.add_action(InlineAssistant::move_up); + cx.add_action(InlineAssistant::move_down); } #[derive(Debug)] @@ -139,10 +141,13 @@ pub struct AssistantPanel { pending_inline_assists: HashMap, pending_inline_assist_ids_by_editor: HashMap, Vec>, include_conversation_in_next_inline_assist: bool, + inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, } impl AssistantPanel { + const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; + pub fn load( workspace: WeakViewHandle, cx: AsyncAppContext, @@ -206,6 +211,7 @@ impl AssistantPanel { pending_inline_assists: Default::default(), pending_inline_assist_ids_by_editor: Default::default(), include_conversation_in_next_inline_assist: false, + inline_prompt_history: Default::default(), _watch_saved_conversations, }; @@ -269,29 +275,16 @@ impl AssistantPanel { } else { InlineAssistKind::Transform }; - let prompt_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); - let placeholder = match assist_kind { - InlineAssistKind::Transform => "Enter transformation prompt…", - InlineAssistKind::Generate => "Enter generation prompt…", - }; - editor.set_placeholder_text(placeholder, cx); - editor - }); let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { - let assistant = InlineAssistant { - id: inline_assist_id, - prompt_editor, - confirmed: false, - has_focus: false, - include_conversation: self.include_conversation_in_next_inline_assist, - measurements: measurements.clone(), - error: None, - }; + let assistant = InlineAssistant::new( + inline_assist_id, + assist_kind, + measurements.clone(), + self.include_conversation_in_next_inline_assist, + self.inline_prompt_history.clone(), + cx, + ); cx.focus_self(); assistant }); @@ -520,6 +513,10 @@ impl AssistantPanel { return; }; + self.inline_prompt_history.push_back(user_prompt.into()); + if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { + self.inline_prompt_history.pop_front(); + } let range = pending_assist.range.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selected_text = snapshot @@ -2895,6 +2892,10 @@ struct InlineAssistant { include_conversation: bool, measurements: Rc>, error: Option, + prompt_history: VecDeque, + prompt_history_ix: Option, + pending_prompt: String, + _subscription: Subscription, } impl Entity for InlineAssistant { @@ -2995,6 +2996,54 @@ impl View for InlineAssistant { } impl InlineAssistant { + fn new( + id: usize, + kind: InlineAssistKind, + measurements: Rc>, + include_conversation: bool, + prompt_history: VecDeque, + cx: &mut ViewContext, + ) -> Self { + let prompt_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + let placeholder = match kind { + InlineAssistKind::Transform => "Enter transformation prompt…", + InlineAssistKind::Generate => "Enter generation prompt…", + }; + editor.set_placeholder_text(placeholder, cx); + editor + }); + let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events); + Self { + id, + prompt_editor, + confirmed: false, + has_focus: false, + include_conversation, + measurements, + error: None, + prompt_history, + prompt_history_ix: None, + pending_prompt: String::new(), + _subscription: subscription, + } + } + + fn handle_prompt_editor_events( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + if let editor::Event::Edited = event { + self.pending_prompt = self.prompt_editor.read(cx).text(cx); + cx.notify(); + } + } + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { cx.emit(InlineAssistantEvent::Canceled); } @@ -3047,6 +3096,43 @@ impl InlineAssistant { }); cx.notify(); } + + fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix > 0 { + self.prompt_history_ix = Some(ix - 1); + let prompt = self.prompt_history[ix - 1].clone(); + self.set_prompt(&prompt, cx); + } + } else if !self.prompt_history.is_empty() { + self.prompt_history_ix = Some(self.prompt_history.len() - 1); + let prompt = self.prompt_history[self.prompt_history.len() - 1].clone(); + self.set_prompt(&prompt, cx); + } + } + + fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix < self.prompt_history.len() - 1 { + self.prompt_history_ix = Some(ix + 1); + let prompt = self.prompt_history[ix + 1].clone(); + self.set_prompt(&prompt, cx); + } else { + self.prompt_history_ix = None; + let pending_prompt = self.pending_prompt.clone(); + self.set_prompt(&pending_prompt, cx); + } + } + } + + fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext) { + self.prompt_editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + let len = buffer.len(cx); + buffer.edit([(0..len, prompt)], None, cx); + }); + }); + } } // This wouldn't need to exist if we could pass parameters when rendering child views. From 5f6562c21448e79971250352a395aee730b420d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 12:07:58 +0200 Subject: [PATCH 087/114] Detect indentation from GPT output --- crates/ai/src/assistant.rs | 44 ++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index fd6e6c63eb..7b360534ec 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -712,7 +712,7 @@ impl AssistantPanel { futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); - let indent_len; + let mut indent_len; let indent_text; if let Some(base_indent) = base_indent { indent_len = base_indent.len; @@ -725,30 +725,43 @@ impl AssistantPanel { indent_text = ""; }; - let mut autoindent = true; - let mut first_chunk = true; + let mut first_line_len = 0; + let mut first_line_non_whitespace_char_ix = None; + let mut first_line = true; let mut new_text = String::new(); while let Some(chunk) = chunks.next().await { let chunk = chunk?; - if first_chunk && (chunk.starts_with(' ') || chunk.starts_with('\t')) { - autoindent = false; - } - - if first_chunk && autoindent { - let first_line_indent = - indent_len.saturating_sub(selection_start.column) as usize; - new_text = indent_text.repeat(first_line_indent); - } let mut lines = chunk.split('\n'); - if let Some(first_line) = lines.next() { - new_text.push_str(first_line); + if let Some(mut line) = lines.next() { + if first_line { + if first_line_non_whitespace_char_ix.is_none() { + if let Some(mut char_ix) = + line.find(|ch: char| !ch.is_whitespace()) + { + line = &line[char_ix..]; + char_ix += first_line_len; + first_line_non_whitespace_char_ix = Some(char_ix); + let first_line_indent = char_ix + .saturating_sub(selection_start.column as usize) + as usize; + new_text.push_str(&indent_text.repeat(first_line_indent)); + indent_len = indent_len.saturating_sub(char_ix as u32); + } + } + first_line_len += line.len(); + } + + if first_line_non_whitespace_char_ix.is_some() { + new_text.push_str(line); + } } for line in lines { + first_line = false; new_text.push('\n'); - if !line.is_empty() && autoindent { + if !line.is_empty() { new_text.push_str(&indent_text.repeat(indent_len as usize)); } new_text.push_str(line); @@ -757,7 +770,6 @@ impl AssistantPanel { let hunks = diff.push_new(&new_text); hunks_tx.send(hunks).await?; new_text.clear(); - first_chunk = false; } hunks_tx.send(diff.finish()).await?; From bf67d3710a3d2a5406aa490ed97b89d0902a4f90 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 12:08:14 +0200 Subject: [PATCH 088/114] Remove trailing backticks when assistant ends with a trailing newline --- crates/ai/src/assistant.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 7b360534ec..2aaf75ae39 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -3216,7 +3216,8 @@ fn strip_markdown_codeblock( let text = if starts_with_fenced_code_block { buffer - .strip_suffix("\n```") + .strip_suffix("\n```\n") + .or_else(|| buffer.strip_suffix("\n```")) .or_else(|| buffer.strip_suffix("\n``")) .or_else(|| buffer.strip_suffix("\n`")) .or_else(|| buffer.strip_suffix('\n')) @@ -3636,6 +3637,13 @@ mod tests { .await, "Lorem ipsum dolor" ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); assert_eq!( strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) .map(|chunk| chunk.unwrap()) From 256949bee04b2cc18f6c0c1b4c5683be25b2cd91 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 30 Aug 2023 10:56:08 -0400 Subject: [PATCH 089/114] fmt --- crates/collab_ui/src/collab_titlebar_item.rs | 67 +++++++++----------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index bd94c85f95..95b9868937 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -247,42 +247,37 @@ impl CollabTitlebarItem { ); if let Some(git_branch) = branch_prepended { ret = ret.with_child( - Flex::row() - .with_child( - Stack::new() - .with_child( - MouseEventHandler::new::( - 0, - cx, - |mouse_state, cx| { - enum BranchPopoverTooltip {} - let style = git_style - .in_state(self.branch_popover.is_some()) - .style_for(mouse_state); - Label::new(git_branch, style.text.clone()) - .contained() - .with_style(style.container.clone()) - .with_margin_right(item_spacing) - .aligned() - .left() - .with_tooltip::( - 0, - "Recent branches", - Some(Box::new(ToggleVcsMenu)), - theme.tooltip.clone(), - cx, - ) - .into_any_named("title-project-branch") - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, move |_, this, cx| { - this.toggle_vcs_menu(&Default::default(), cx) - }) - .on_click(MouseButton::Left, move |_, _, _| {}), - ) - .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), - ), + Flex::row().with_child( + Stack::new() + .with_child( + MouseEventHandler::new::(0, cx, |mouse_state, cx| { + enum BranchPopoverTooltip {} + let style = git_style + .in_state(self.branch_popover.is_some()) + .style_for(mouse_state); + Label::new(git_branch, style.text.clone()) + .contained() + .with_style(style.container.clone()) + .with_margin_right(item_spacing) + .aligned() + .left() + .with_tooltip::( + 0, + "Recent branches", + Some(Box::new(ToggleVcsMenu)), + theme.tooltip.clone(), + cx, + ) + .into_any_named("title-project-branch") + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.toggle_vcs_menu(&Default::default(), cx) + }) + .on_click(MouseButton::Left, move |_, _, _| {}), + ) + .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), + ), ) } ret.into_any() From 1da3be06463dbd6a8aec7a527aa47cb406bf777f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 30 Aug 2023 11:14:50 -0400 Subject: [PATCH 090/114] Align diagnostic icons with other statusbar icons --- assets/icons/check_circle.svg | 4 ++-- assets/icons/error.svg | 4 ++-- assets/icons/warning.svg | 7 +++--- styles/src/style_tree/status_bar.ts | 34 +++++++++++++++-------------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg index 85ba2e1f37..b48fe34631 100644 --- a/assets/icons/check_circle.svg +++ b/assets/icons/check_circle.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/error.svg b/assets/icons/error.svg index 82b9401d08..593629beee 100644 --- a/assets/icons/error.svg +++ b/assets/icons/error.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 6b3d0fd41e..e581def0d0 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1,5 +1,6 @@ - - - + + + + diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 3b5ebf2c88..b279bbac14 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -75,34 +75,36 @@ export default function status_bar(): any { icon_color_error: foreground(layer, "negative"), container_ok: { corner_radius: 6, - padding: { top: 3, bottom: 3, left: 7, right: 7 }, - }, - container_warning: { - ...diagnostic_status_container, - background: background(layer, "warning"), - border: border(layer, "warning"), - }, - container_error: { - ...diagnostic_status_container, - background: background(layer, "negative"), - border: border(layer, "negative"), + padding: { top: 2, bottom: 2, left: 6, right: 6 }, }, + container_warning: diagnostic_status_container, + container_error: diagnostic_status_container }, state: { hovered: { icon_color_ok: foreground(layer, "on"), container_ok: { - background: background(layer, "on", "hovered"), + background: background(layer, "hovered") }, container_warning: { - background: background(layer, "warning", "hovered"), - border: border(layer, "warning", "hovered"), + background: background(layer, "hovered") }, container_error: { - background: background(layer, "negative", "hovered"), - border: border(layer, "negative", "hovered"), + background: background(layer, "hovered") }, }, + clicked: { + icon_color_ok: foreground(layer, "on"), + container_ok: { + background: background(layer, "pressed") + }, + container_warning: { + background: background(layer, "pressed") + }, + container_error: { + background: background(layer, "pressed") + } + } }, }), panel_buttons: { From 7e5735c8f1ecef5336b8f68ce6ad08b9a6880b8d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 30 Aug 2023 18:41:41 +0300 Subject: [PATCH 091/114] Reap overly long LSP requests with a 2m timeout Co-authored-by: Julia Risley --- crates/lsp/src/lsp.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index f39d97aeb5..6a9e48b481 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -4,7 +4,7 @@ pub use lsp_types::*; use anyhow::{anyhow, Context, Result}; use collections::HashMap; -use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; +use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt}; use gpui::{executor, AsyncAppContext, Task}; use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; @@ -26,12 +26,14 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, Weak, }, + time::{Duration, Instant}, }; use std::{path::Path, process::Stdio}; use util::{ResultExt, TryFutureExt}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; +const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; @@ -697,7 +699,7 @@ impl LanguageServer { outbound_tx: &channel::Sender, executor: &Arc, params: T::Params, - ) -> impl 'static + Future> + ) -> impl 'static + Future> where T::Result: 'static + Send, { @@ -738,10 +740,25 @@ impl LanguageServer { .try_send(message) .context("failed to write to language server's stdin"); + let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse(); + let started = Instant::now(); async move { handle_response?; send?; - rx.await? + + let method = T::METHOD; + futures::select! { + response = rx.fuse() => { + let elapsed = started.elapsed(); + log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}"); + response? + } + + _ = timeout => { + log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}"); + anyhow::bail!("LSP request timeout"); + } + } } } From 7204c245ea1c301be0be84bfc13bc92e68509ae2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 30 Aug 2023 13:41:09 -0400 Subject: [PATCH 092/114] v0.103.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e87ff3d991..d744f92ce8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9702,7 +9702,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.102.0" +version = "0.103.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2a97764647..66d55b38f0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.102.0" +version = "0.103.0" publish = false [lib] From feb7a8a0f44010dce20bae4472bd9e0befda1c91 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 30 Aug 2023 14:37:55 -0400 Subject: [PATCH 093/114] collab 0.19.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d744f92ce8..ca00e69416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,7 +1453,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8adc38615c..914e3f2dfb 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.18.0" +version = "0.19.0" publish = false [[bin]] From 46429426ef01d76f5a99ad7c13f9a5e1396d84ba Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 13:14:06 -0700 Subject: [PATCH 094/114] Avoid accidental gpui transitive dependency in collab * Make Fs depend on Text, not vise versa Co-authored-by: Joseph Co-authored-by: Mikayla --- Cargo.lock | 2 +- crates/collab/src/tests/integration_tests.rs | 4 +- .../src/tests/randomized_integration_tests.rs | 2 +- crates/copilot/src/copilot.rs | 2 +- crates/fs/Cargo.toml | 1 + crates/fs/src/fs.rs | 67 +------------------ crates/language/src/buffer.rs | 1 - crates/language/src/buffer_tests.rs | 2 +- crates/language/src/language.rs | 1 + crates/language/src/proto.rs | 12 ++-- crates/project/src/lsp_command.rs | 2 +- crates/project/src/project_tests.rs | 4 +- crates/project/src/worktree.rs | 4 +- crates/text/Cargo.toml | 1 - crates/text/src/text.rs | 67 ++++++++++++++++++- 15 files changed, 85 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca00e69416..91a8a12eac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2768,6 +2768,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", + "text", "time 0.3.27", "util", ] @@ -7636,7 +7637,6 @@ dependencies = [ "ctor", "digest 0.9.0", "env_logger 0.9.3", - "fs", "gpui", "lazy_static", "log", diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index f64a82e32e..2613d01131 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -9,7 +9,7 @@ use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, }; -use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; +use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, @@ -19,7 +19,7 @@ use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, OffsetRangeExt, Point, Rope, + LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 814f248b6d..e48753ed41 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -762,7 +762,7 @@ async fn apply_client_operation( client .fs() - .save(&path, &content.as_str().into(), fs::LineEnding::Unix) + .save(&path, &content.as_str().into(), text::LineEnding::Unix) .await .unwrap(); } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 427134894f..499ae2e808 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1188,7 +1188,7 @@ mod tests { _: u64, _: &clock::Global, _: language::RopeFingerprint, - _: ::fs::LineEnding, + _: language::LineEnding, _: std::time::SystemTime, _: &mut AppContext, ) { diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index b3ebd224b0..7584dec21a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -12,6 +12,7 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rope = { path = "../rope" } +text = { path = "../text" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } rpc = { path = "../rpc" } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index ec8a249ff4..ecaee4534e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -4,14 +4,10 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; use git2::Repository as LibGitRepository; -use lazy_static::lazy_static; use parking_lot::Mutex; -use regex::Regex; use repository::GitRepository; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; -use std::borrow::Cow; -use std::cmp; use std::io::Write; use std::sync::Arc; use std::{ @@ -22,6 +18,7 @@ use std::{ time::{Duration, SystemTime}, }; use tempfile::NamedTempFile; +use text::LineEnding; use util::ResultExt; #[cfg(any(test, feature = "test-support"))] @@ -33,66 +30,6 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; -lazy_static! { - static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum LineEnding { - Unix, - Windows, -} - -impl Default for LineEnding { - fn default() -> Self { - #[cfg(unix)] - return Self::Unix; - - #[cfg(not(unix))] - return Self::CRLF; - } -} - -impl LineEnding { - pub fn as_str(&self) -> &'static str { - match self { - LineEnding::Unix => "\n", - LineEnding::Windows => "\r\n", - } - } - - pub fn detect(text: &str) -> Self { - let mut max_ix = cmp::min(text.len(), 1000); - while !text.is_char_boundary(max_ix) { - max_ix -= 1; - } - - if let Some(ix) = text[..max_ix].find(&['\n']) { - if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { - Self::Windows - } else { - Self::Unix - } - } else { - Self::default() - } - } - - pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { - *text = replaced; - } - } - - pub fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } -} - #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; @@ -520,7 +457,7 @@ impl FakeFsState { } #[cfg(any(test, feature = "test-support"))] -lazy_static! { +lazy_static::lazy_static! { pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git"); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4310f84830..8adf6f6421 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -15,7 +15,6 @@ use crate::{ }; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; -use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use lsp::LanguageServerId; diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index db3749aa25..3bedf5b7a8 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -5,7 +5,6 @@ use crate::language_settings::{ use super::*; use clock::ReplicaId; use collections::BTreeMap; -use fs::LineEnding; use gpui::{AppContext, ModelHandle}; use indoc::indoc; use proto::deserialize_operation; @@ -20,6 +19,7 @@ use std::{ time::{Duration, Instant}, }; use text::network::Network; +use text::LineEnding; use unindent::Unindent as _; use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7a9e6b83ce..89d0592627 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -57,6 +57,7 @@ pub use diagnostic_set::DiagnosticEntry; pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo}; +pub use text::LineEnding; pub use tree_sitter::{Parser, Tree}; pub fn init(cx: &mut AppContext) { diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 09c5ec7fc3..cf5465a601 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -20,17 +20,17 @@ pub fn deserialize_fingerprint(fingerprint: &str) -> Result { .map_err(|error| anyhow!("invalid fingerprint: {}", error)) } -pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding { +pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding { match message { - proto::LineEnding::Unix => fs::LineEnding::Unix, - proto::LineEnding::Windows => fs::LineEnding::Windows, + proto::LineEnding::Unix => text::LineEnding::Unix, + proto::LineEnding::Windows => text::LineEnding::Windows, } } -pub fn serialize_line_ending(message: fs::LineEnding) -> proto::LineEnding { +pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding { match message { - fs::LineEnding::Unix => proto::LineEnding::Unix, - fs::LineEnding::Windows => proto::LineEnding::Windows, + text::LineEnding::Unix => proto::LineEnding::Unix, + text::LineEnding::Windows => proto::LineEnding::Windows, } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8239cf8690..6b10ed26c1 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -6,7 +6,6 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; -use fs::LineEnding; use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ @@ -19,6 +18,7 @@ use language::{ }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; +use text::LineEnding; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { lsp::FormattingOptions { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7c5983a0a9..397223c4bb 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,11 +1,11 @@ use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; -use fs::{FakeFs, LineEnding, RealFs}; +use fs::{FakeFs, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; use language::{ language_settings::{AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, - OffsetRangeExt, Point, ToPoint, + LineEnding, OffsetRangeExt, Point, ToPoint, }; use lsp::Url; use parking_lot::Mutex; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e6e0f37cc7..2de3671033 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -8,7 +8,7 @@ use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, - Fs, LineEnding, + Fs, }; use futures::{ channel::{ @@ -27,7 +27,7 @@ use language::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, serialize_version, }, - Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped, + Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped, }; use lsp::LanguageServerId; use parking_lot::Mutex; diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index adec2d4fd0..65e9b6fcec 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -14,7 +14,6 @@ test-support = ["rand"] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -fs = { path = "../fs" } rope = { path = "../rope" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 6a00ea12db..2fabb0f87f 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -14,17 +14,17 @@ pub use anchor::*; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; use collections::{HashMap, HashSet}; -use fs::LineEnding; use locator::Locator; use operation_queue::OperationQueue; pub use patch::Patch; use postage::{oneshot, prelude::*}; +use lazy_static::lazy_static; +use regex::Regex; pub use rope::*; pub use selection::*; -use util::ResultExt; - use std::{ + borrow::Cow, cmp::{self, Ordering, Reverse}, future::Future, iter::Iterator, @@ -37,10 +37,15 @@ pub use subscription::*; pub use sum_tree::Bias; use sum_tree::{FilterCursor, SumTree, TreeMap}; use undo_map::UndoMap; +use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; +lazy_static! { + static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); +} + pub type TransactionId = clock::Local; pub struct Buffer { @@ -2671,3 +2676,59 @@ impl FromAnchor for usize { snapshot.summary_for_anchor(anchor) } } + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineEnding { + Unix, + Windows, +} + +impl Default for LineEnding { + fn default() -> Self { + #[cfg(unix)] + return Self::Unix; + + #[cfg(not(unix))] + return Self::CRLF; + } +} + +impl LineEnding { + pub fn as_str(&self) -> &'static str { + match self { + LineEnding::Unix => "\n", + LineEnding::Windows => "\r\n", + } + } + + pub fn detect(text: &str) -> Self { + let mut max_ix = cmp::min(text.len(), 1000); + while !text.is_char_boundary(max_ix) { + max_ix -= 1; + } + + if let Some(ix) = text[..max_ix].find(&['\n']) { + if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { + Self::Windows + } else { + Self::Unix + } + } else { + Self::default() + } + } + + pub fn normalize(text: &mut String) { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { + *text = replaced; + } + } + + pub fn normalize_arc(text: Arc) -> Arc { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { + replaced.into() + } else { + text + } + } +} From e0bdd857f15a1320669baf7d88c55aef8f43ab76 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 14:14:18 -0700 Subject: [PATCH 095/114] Fix cursor colors of non-followed collaborators Co-authored-by: Mikayla --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 62f4c8c806..90fe6ccc52 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2251,7 +2251,7 @@ impl Element for EditorElement { let replica_id = if let Some(mapping) = &editor.replica_id_mapping { mapping.get(&replica_id).copied() } else { - None + Some(replica_id) }; // The local selections match the leader's selections. From 123bc85a8e8cca52fc8ce1365d8ed6ac6f3f4c7a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 14:23:12 -0700 Subject: [PATCH 096/114] collab panel: Make screen share row line up with shared projects Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 59 ++++++---------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0593bfcb1f..79e33c5048 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1106,23 +1106,17 @@ impl CollabPanel { ) -> AnyElement { enum OpenSharedScreen {} - let font_cache = cx.font_cache(); - let host_avatar_height = theme + let host_avatar_width = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; MouseEventHandler::new::( peer_id.as_u64() as usize, cx, - |mouse_state, _| { + |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -1130,49 +1124,20 @@ impl CollabPanel { .style_for(mouse_state); Flex::row() - .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - })) - .constrained() - .with_width(host_avatar_height), - ) + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) .with_child( Svg::new("icons/disable_screen_sharing_12.svg") - .with_color(row.icon.color) + .with_color(theme.channel_hash.color) .constrained() - .with_width(row.icon.width) + .with_width(theme.channel_hash.width) .aligned() - .left() - .contained() - .with_style(row.icon.container), + .left(), ) .with_child( Label::new("Screen", row.name.text.clone()) From f9dffc1734e28ccb90ecdd6b39b340e39fa86a3a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 14:35:02 -0700 Subject: [PATCH 097/114] Remove unnecessary ConstrainedBoxes in collab panel Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 79e33c5048..daaa483975 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2518,27 +2518,16 @@ impl View for CollabPanel { .with_child( Flex::column() .with_child( - Flex::row() - .with_child( - ChildView::new(&self.filter_editor, cx) - .contained() - .with_style(theme.user_query_editor.container) - .flex(1.0, true), - ) - .constrained() - .with_width(self.size(cx)), - ) - .with_child( - List::new(self.list_state.clone()) - .constrained() - .with_width(self.size(cx)) - .flex(1., true) - .into_any(), + Flex::row().with_child( + ChildView::new(&self.filter_editor, cx) + .contained() + .with_style(theme.user_query_editor.container) + .flex(1.0, true), + ), ) + .with_child(List::new(self.list_state.clone()).flex(1., true).into_any()) .contained() .with_style(theme.container) - .constrained() - .with_width(self.size(cx)) .into_any(), ) .with_children( From ab49f8c592db243e426b6104dcb0f402d1b8473a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 00:37:00 +0300 Subject: [PATCH 098/114] Rewrite inlay hint collab tests to remove races --- crates/collab/src/tests/integration_tests.rs | 179 +++++++------------ 1 file changed, 67 insertions(+), 112 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 2613d01131..8121b0ac91 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -33,7 +33,7 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::{ - atomic::{AtomicBool, AtomicU32, Ordering::SeqCst}, + atomic::{self, AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, }; @@ -7799,7 +7799,7 @@ async fn test_on_input_format_from_guest_to_host( }); } -#[gpui::test] +#[gpui::test(iterations = 10)] async fn test_mutual_editor_inlay_hint_cache_update( deterministic: Arc, cx_a: &mut TestAppContext, @@ -7913,30 +7913,27 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); // Set up the language server to return an additional inlay hint on each request. - let next_call_id = Arc::new(AtomicU32::new(0)); + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); fake_language_server .handle_request::(move |params, _| { - let task_next_call_id = Arc::clone(&next_call_id); + let task_edits_made = Arc::clone(&closure_edits_made); async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let call_count = task_next_call_id.fetch_add(1, SeqCst); - Ok(Some( - (0..=call_count) - .map(|ix| lsp::InlayHint { - position: lsp::Position::new(0, ix), - label: lsp::InlayHintLabel::String(ix.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }) - .collect(), - )) + let edits_made = task_edits_made.load(atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) } }) .next() @@ -7945,17 +7942,17 @@ async fn test_mutual_editor_inlay_hint_cache_update( deterministic.run_until_parked(); - let mut edits_made = 1; + let initial_edit = edits_made.load(atomic::Ordering::Acquire); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec!["0".to_string()], + vec![initial_edit.to_string()], extract_hint_labels(editor), "Host should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 1, "Host editor update the cache version after every cache/view change", ); }); @@ -7972,144 +7969,104 @@ async fn test_mutual_editor_inlay_hint_cache_update( deterministic.run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string()], + vec![initial_edit.to_string()], extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 1, "Guest editor update the cache version after every cache/view change" ); }); + let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; editor_b.update(cx_b, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); editor.handle_input(":", cx); cx.focus(&editor_b); - edits_made += 1; }); deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string() - ], + vec![after_client_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 2); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(), "2".to_string(),], + vec![after_client_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 2); }); + let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; editor_a.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.handle_input("a change to increment both buffers' versions", cx); cx.focus(&editor_a); - edits_made += 1; }); deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string() - ], + vec![after_host_edit.to_string()], extract_hint_labels(editor), - "Host should get hints from 3rd edit, 5th LSP query: \ -4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 3); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - ], + vec![after_host_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 3); }); + let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; fake_language_server .request::(()) .await .expect("inlay refresh request failed"); - edits_made += 1; deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - "6".to_string(), - ], + vec![after_special_edit_for_refresh.to_string()], extract_hint_labels(editor), - "Host should react to /refresh LSP request and get new hints from 7th LSP query" + "Host should react to /refresh LSP request" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 4, "Host should accepted all edits and bump its cache version every time" ); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - "6".to_string(), - "7".to_string(), - ], + vec![after_special_edit_for_refresh.to_string()], extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query" + "Guest should get a /refresh LSP request propagated by host" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 4, "Guest should accepted all edits and bump its cache version every time" ); }); } -#[gpui::test] +#[gpui::test(iterations = 10)] async fn test_inlay_hint_refresh_is_forwarded( deterministic: Arc, cx_a: &mut TestAppContext, @@ -8223,35 +8180,34 @@ async fn test_inlay_hint_refresh_is_forwarded( .downcast::() .unwrap(); + let other_hints = Arc::new(AtomicBool::new(false)); let fake_language_server = fake_language_servers.next().await.unwrap(); - let next_call_id = Arc::new(AtomicU32::new(0)); + let closure_other_hints = Arc::clone(&other_hints); fake_language_server .handle_request::(move |params, _| { - let task_next_call_id = Arc::clone(&next_call_id); + let task_other_hints = Arc::clone(&closure_other_hints); async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); - let mut new_hints = Vec::with_capacity(current_call_id as usize); - loop { - new_hints.push(lsp::InlayHint { - position: lsp::Position::new(0, current_call_id), - label: lsp::InlayHintLabel::String(current_call_id.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }); - if current_call_id == 0 { - break; - } - current_call_id -= 1; - } - Ok(Some(new_hints)) + let other_hints = task_other_hints.load(atomic::Ordering::Acquire); + let character = if other_hints { 0 } else { 2 }; + let label = if other_hints { + "other hint" + } else { + "initial hint" + }; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, character), + label: lsp::InlayHintLabel::String(label.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) } }) .next() @@ -8270,26 +8226,26 @@ async fn test_inlay_hint_refresh_is_forwarded( assert_eq!( inlay_cache.version(), 0, - "Host should not increment its cache version due to no changes", + "Turned off hints should not generate version updates" ); }); - let mut edits_made = 1; cx_b.foreground().run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string()], + vec!["initial hint".to_string()], extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, - "Guest editor update the cache version after every cache/view change" + 1, + "Should update cache verison after first hints" ); }); + other_hints.fetch_or(true, atomic::Ordering::Release); fake_language_server .request::(()) .await @@ -8304,22 +8260,21 @@ async fn test_inlay_hint_refresh_is_forwarded( assert_eq!( inlay_cache.version(), 0, - "Host should not increment its cache version due to no changes", + "Turned off hints should not generate version updates, again" ); }); - edits_made += 1; cx_b.foreground().run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(),], + vec!["other hint".to_string()], extract_hint_labels(editor), "Guest should get a /refresh LSP request propagated by host despite host hints are off" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 2, "Guest should accepted all edits and bump its cache version every time" ); }); From 4b5948e00404259eea7fd8ecc9c37844d84f8616 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 15:05:47 -0700 Subject: [PATCH 099/114] Disable save as prompt for channel notes --- crates/collab_ui/src/channel_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index a34f10b2db..5086cc8b37 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -213,7 +213,7 @@ impl Item for ChannelView { } fn is_singleton(&self, _cx: &AppContext) -> bool { - true + false } fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { From 54e7e2f59db54e053dbde4e8f2d3b6c932d5b676 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 30 Aug 2023 17:38:30 +0300 Subject: [PATCH 100/114] Capture language servers' stderr into server logs --- crates/language_tools/src/lsp_log.rs | 74 ++++++++++++++---------- crates/lsp/src/lsp.rs | 85 ++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 47 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 51bdb4c5ce..60c4e41666 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -12,6 +12,7 @@ use gpui::{ ViewHandle, WeakModelHandle, }; use language::{Buffer, LanguageServerId, LanguageServerName}; +use lsp::IoKind; use project::{Project, Worktree}; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; @@ -26,7 +27,7 @@ const RECEIVE_LINE: &str = "// Receive:\n"; pub struct LogStore { projects: HashMap, ProjectState>, - io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, bool, String)>, + io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, IoKind, String)>, } struct ProjectState { @@ -37,12 +38,12 @@ struct ProjectState { struct LanguageServerState { log_buffer: ModelHandle, rpc_state: Option, + _subscription: Option, } struct LanguageServerRpcState { buffer: ModelHandle, last_message_kind: Option, - _subscription: lsp::Subscription, } pub struct LspLogView { @@ -118,11 +119,11 @@ impl LogStore { io_tx, }; cx.spawn_weak(|this, mut cx| async move { - while let Some((project, server_id, is_output, mut message)) = io_rx.next().await { + while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { message.push('\n'); - this.on_io(project, server_id, is_output, &message, cx); + this.on_io(project, server_id, io_kind, &message, cx); }); } } @@ -168,22 +169,29 @@ impl LogStore { cx: &mut ModelContext, ) -> Option> { let project_state = self.projects.get_mut(&project.downgrade())?; - Some( - project_state - .servers - .entry(id) - .or_insert_with(|| { - cx.notify(); - LanguageServerState { - rpc_state: None, - log_buffer: cx - .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) - .clone(), - } - }) - .log_buffer - .clone(), - ) + let server_state = project_state.servers.entry(id).or_insert_with(|| { + cx.notify(); + LanguageServerState { + rpc_state: None, + log_buffer: cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) + .clone(), + _subscription: None, + } + }); + + let server = project.read(cx).language_server_for_id(id); + let weak_project = project.downgrade(); + let io_tx = self.io_tx.clone(); + server_state._subscription = server.map(|server| { + server.on_io(move |io_kind, message| { + io_tx + .unbounded_send((weak_project, id, io_kind, message.to_string())) + .ok(); + }) + }); + + Some(server_state.log_buffer.clone()) } fn add_language_server_log( @@ -230,7 +238,7 @@ impl LogStore { Some(server_state.log_buffer.clone()) } - pub fn enable_rpc_trace_for_language_server( + fn enable_rpc_trace_for_language_server( &mut self, project: &ModelHandle, server_id: LanguageServerId, @@ -239,9 +247,7 @@ impl LogStore { let weak_project = project.downgrade(); let project_state = self.projects.get_mut(&weak_project)?; let server_state = project_state.servers.get_mut(&server_id)?; - let server = project.read(cx).language_server_for_id(server_id)?; let rpc_state = server_state.rpc_state.get_or_insert_with(|| { - let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); cx.spawn_weak({ @@ -258,11 +264,6 @@ impl LogStore { LanguageServerRpcState { buffer, last_message_kind: None, - _subscription: server.on_io(move |is_received, json| { - io_tx - .unbounded_send((weak_project, server_id, is_received, json.to_string())) - .ok(); - }), } }); Some(rpc_state.buffer.clone()) @@ -285,10 +286,25 @@ impl LogStore { &mut self, project: WeakModelHandle, language_server_id: LanguageServerId, - is_received: bool, + io_kind: IoKind, message: &str, cx: &mut AppContext, ) -> Option<()> { + let is_received = match io_kind { + IoKind::StdOut => true, + IoKind::StdIn => false, + IoKind::StdErr => { + let project = project.upgrade(cx)?; + project.update(cx, |_, cx| { + cx.emit(project::Event::LanguageServerLog( + language_server_id, + format!("stderr: {}\n", message.trim()), + )) + }); + return Some(()); + } + }; + let state = self .projects .get_mut(&project)? diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d49dafff2f..2abe0baefa 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -35,7 +35,14 @@ const CONTENT_LEN_HEADER: &str = "Content-Length: "; type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; -type IoHandler = Box; +type IoHandler = Box; + +#[derive(Debug, Clone, Copy)] +pub enum IoKind { + StdOut, + StdIn, + StdErr, +} #[derive(Debug, Clone, Deserialize)] pub struct LanguageServerBinary { @@ -144,16 +151,18 @@ impl LanguageServer { .args(binary.arguments) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) + .stderr(Stdio::piped()) .kill_on_drop(true) .spawn()?; let stdin = server.stdin.take().unwrap(); - let stout = server.stdout.take().unwrap(); + let stdout = server.stdout.take().unwrap(); + let stderr = server.stderr.take().unwrap(); let mut server = Self::new_internal( server_id.clone(), stdin, - stout, + stdout, + stderr, Some(server), root_path, code_action_kinds, @@ -181,10 +190,11 @@ impl LanguageServer { Ok(server) } - fn new_internal( + fn new_internal( server_id: LanguageServerId, stdin: Stdin, stdout: Stdout, + stderr: Stderr, server: Option, root_path: &Path, code_action_kinds: Option>, @@ -194,7 +204,8 @@ impl LanguageServer { where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, - F: FnMut(AnyNotification) + 'static + Send, + Stderr: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send + Clone, { let (outbound_tx, outbound_rx) = channel::unbounded::(); let (output_done_tx, output_done_rx) = barrier::channel(); @@ -203,17 +214,26 @@ impl LanguageServer { let response_handlers = Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default()))); let io_handlers = Arc::new(Mutex::new(HashMap::default())); - let input_task = cx.spawn(|cx| { - Self::handle_input( - stdout, - on_unhandled_notification, - notification_handlers.clone(), - response_handlers.clone(), - io_handlers.clone(), - cx, - ) + + let stdout_input_task = cx.spawn(|cx| { + { + Self::handle_input( + stdout, + on_unhandled_notification.clone(), + notification_handlers.clone(), + response_handlers.clone(), + io_handlers.clone(), + cx, + ) + } .log_err() }); + let stderr_input_task = + cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err()); + let input_task = cx.spawn(|_| async move { + let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); + stdout.or(stderr) + }); let output_task = cx.background().spawn({ Self::handle_output( stdin, @@ -284,7 +304,7 @@ impl LanguageServer { if let Ok(message) = str::from_utf8(&buffer) { log::trace!("incoming message:{}", message); for handler in io_handlers.lock().values_mut() { - handler(true, message); + handler(IoKind::StdOut, message); } } @@ -327,6 +347,30 @@ impl LanguageServer { } } + async fn handle_stderr( + stderr: Stderr, + io_handlers: Arc>>, + ) -> anyhow::Result<()> + where + Stderr: AsyncRead + Unpin + Send + 'static, + { + let mut stderr = BufReader::new(stderr); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stderr.read_until(b'\n', &mut buffer).await?; + if let Ok(message) = str::from_utf8(&buffer) { + log::trace!("incoming stderr message:{message}"); + for handler in io_handlers.lock().values_mut() { + handler(IoKind::StdErr, message); + } + } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; + } + } + async fn handle_output( stdin: Stdin, outbound_rx: channel::Receiver, @@ -348,7 +392,7 @@ impl LanguageServer { while let Ok(message) = outbound_rx.recv().await { log::trace!("outgoing message:{}", message); for handler in io_handlers.lock().values_mut() { - handler(false, &message); + handler(IoKind::StdIn, &message); } content_len_buffer.clear(); @@ -532,7 +576,7 @@ impl LanguageServer { #[must_use] pub fn on_io(&self, f: F) -> Subscription where - F: 'static + Send + FnMut(bool, &str), + F: 'static + Send + FnMut(IoKind, &str), { let id = self.next_id.fetch_add(1, SeqCst); self.io_handlers.lock().insert(id, Box::new(f)); @@ -845,12 +889,16 @@ impl LanguageServer { ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); + // writers will be dropped after we exit, so readers will also be noop for the fake servers + let (_stderr_writer, stderr_reader) = async_pipe::pipe(); + let (_stderr_writer_2, stderr_reader_2) = async_pipe::pipe(); let (notifications_tx, notifications_rx) = channel::unbounded(); let server = Self::new_internal( LanguageServerId(0), stdin_writer, stdout_reader, + stderr_reader, None, Path::new("/"), None, @@ -862,6 +910,7 @@ impl LanguageServer { LanguageServerId(0), stdout_writer, stdin_reader, + stderr_reader_2, None, Path::new("/"), None, From 0f619e0b6750b1f08dee6cfd2ea6b108a2e59d65 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 01:00:37 +0300 Subject: [PATCH 101/114] Do not write TRACE logs into file for Lua --- crates/zed/src/languages/lua.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 7c5c7179d0..45d520df27 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -6,7 +6,7 @@ use futures::{io::BufReader, StreamExt}; use language::{LanguageServerName, LspAdapterDelegate}; use lsp::LanguageServerBinary; use smol::fs; -use std::{any::Any, env::consts, ffi::OsString, path::PathBuf}; +use std::{any::Any, env::consts, path::PathBuf}; use util::{ async_iife, github::{latest_github_release, GitHubLspBinaryVersion}, @@ -16,13 +16,6 @@ use util::{ #[derive(Copy, Clone)] pub struct LuaLspAdapter; -fn server_binary_arguments() -> Vec { - vec![ - "--logpath=~/lua-language-server.log".into(), - "--loglevel=trace".into(), - ] -} - #[async_trait] impl super::LspAdapter for LuaLspAdapter { async fn name(&self) -> LanguageServerName { @@ -83,7 +76,7 @@ impl super::LspAdapter for LuaLspAdapter { .await?; Ok(LanguageServerBinary { path: binary_path, - arguments: server_binary_arguments(), + arguments: Vec::new(), }) } @@ -127,7 +120,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option Date: Wed, 30 Aug 2023 21:14:39 -0400 Subject: [PATCH 102/114] Scope Tailwind in JS/TS to within string In some situations outside JSX elements Tailwind will never respond to a completion request, holding up the tsserver completions. Only submit the request to Tailwind when we wouldn't get tsserver completions anyway and don't submit to Tailwind when we know we won't get Tailwind completions Co-Authored-By: Kirill Bulatov --- crates/language/src/language.rs | 31 ++++++++++++++++++- crates/project/src/project.rs | 13 +++++++- .../zed/src/languages/javascript/config.toml | 2 ++ crates/zed/src/languages/tsx/config.toml | 2 ++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index a2d02ecd96..bb83beeeea 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -345,6 +345,8 @@ pub struct LanguageConfig { #[serde(default)] pub block_comment: Option<(Arc, Arc)>, #[serde(default)] + pub scope_opt_in_language_servers: Vec, + #[serde(default)] pub overrides: HashMap, #[serde(default)] pub word_characters: HashSet, @@ -377,6 +379,8 @@ pub struct LanguageConfigOverride { pub disabled_bracket_ixs: Vec, #[serde(default)] pub word_characters: Override>, + #[serde(default)] + pub opt_into_language_servers: Vec, } #[derive(Clone, Deserialize, Debug)] @@ -415,6 +419,7 @@ impl Default for LanguageConfig { autoclose_before: Default::default(), line_comment: Default::default(), block_comment: Default::default(), + scope_opt_in_language_servers: Default::default(), overrides: Default::default(), collapsed_placeholder: Default::default(), word_characters: Default::default(), @@ -1352,13 +1357,23 @@ impl Language { Ok(self) } - pub fn with_override_query(mut self, source: &str) -> Result { + pub fn with_override_query(mut self, source: &str) -> anyhow::Result { let query = Query::new(self.grammar_mut().ts_language, source)?; let mut override_configs_by_id = HashMap::default(); for (ix, name) in query.capture_names().iter().enumerate() { if !name.starts_with('_') { let value = self.config.overrides.remove(name).unwrap_or_default(); + for server_name in &value.opt_into_language_servers { + if !self + .config + .scope_opt_in_language_servers + .contains(server_name) + { + util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server"); + } + } + override_configs_by_id.insert(ix as u32, (name.clone(), value)); } } @@ -1597,6 +1612,20 @@ impl LanguageScope { c.is_whitespace() || self.language.config.autoclose_before.contains(c) } + pub fn language_allowed(&self, name: &LanguageServerName) -> bool { + let config = &self.language.config; + let opt_in_servers = &config.scope_opt_in_language_servers; + if opt_in_servers.iter().any(|o| *o == *name.0) { + if let Some(over) = self.config_override() { + over.opt_into_language_servers.iter().any(|o| *o == *name.0) + } else { + false + } + } else { + true + } + } + fn config_override(&self) -> Option<&LanguageConfigOverride> { let id = self.override_id?; let grammar = self.language.grammar.as_ref()?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c672a37cad..597deacd1a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4429,16 +4429,27 @@ impl Project { self.request_primary_lsp(buffer.clone(), GetHover { position }, cx) } - pub fn completions( + pub fn completions( &self, buffer: &ModelHandle, position: T, cx: &mut ModelContext, ) -> Task>> { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); let position = position.to_point_utf16(buffer.read(cx)); + + let scope = snapshot.language_scope_at(offset); + let server_ids: Vec<_> = self .language_servers_for_buffer(buffer.read(cx), cx) .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) .map(|(_, server)| server.server_id()) .collect(); diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index 7f6c6931e4..6f7ce49d3d 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -13,6 +13,7 @@ brackets = [ { start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, ] +scope_opt_in_language_servers = ["tailwindcss-language-server"] [overrides.element] line_comment = { remove = true } @@ -20,3 +21,4 @@ block_comment = ["{/* ", " */}"] [overrides.string] word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index 2f676f6710..3bdc638f73 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -12,6 +12,7 @@ brackets = [ { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, ] +scope_opt_in_language_servers = ["tailwindcss-language-server"] [overrides.element] line_comment = { remove = true } @@ -19,3 +20,4 @@ block_comment = ["{/* ", " */}"] [overrides.string] word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] From af665cc3d275e45d3f980547f824ef09b32cc8ad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 10:43:29 +0300 Subject: [PATCH 103/114] Use `ctrl-:` instead of `ctrl-shift-:` for inlay hints toggling The latter is not posible to press in Zed, since `:` is typed as `shift-;` with typical US keyboard layouts. In the end, it's the same buttons you have to press to toggle the inlay hints, but working this time. --- assets/keymaps/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7c8d7f01da..1b2d8ce419 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -522,7 +522,7 @@ // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", "cmd-alt-i": "zed::DebugElements", - "ctrl-shift-:": "editor::ToggleInlayHints", + "ctrl-:": "editor::ToggleInlayHints", } }, { From 18efc0d5e5346eed70de44516f32489cab8e75fd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 11:07:37 +0300 Subject: [PATCH 104/114] Fix the tests, by not requiring stderr for fake servers --- crates/lsp/src/lsp.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 2abe0baefa..51f48a66a0 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -162,7 +162,7 @@ impl LanguageServer { server_id.clone(), stdin, stdout, - stderr, + Some(stderr), Some(server), root_path, code_action_kinds, @@ -194,7 +194,7 @@ impl LanguageServer { server_id: LanguageServerId, stdin: Stdin, stdout: Stdout, - stderr: Stderr, + stderr: Option, server: Option, root_path: &Path, code_action_kinds: Option>, @@ -228,8 +228,9 @@ impl LanguageServer { } .log_err() }); - let stderr_input_task = - cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err()); + let stderr_input_task = stderr + .map(|stderr| cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err())) + .unwrap_or_else(|| Task::Ready(Some(None))); let input_task = cx.spawn(|_| async move { let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); stdout.or(stderr) @@ -889,16 +890,13 @@ impl LanguageServer { ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); - // writers will be dropped after we exit, so readers will also be noop for the fake servers - let (_stderr_writer, stderr_reader) = async_pipe::pipe(); - let (_stderr_writer_2, stderr_reader_2) = async_pipe::pipe(); let (notifications_tx, notifications_rx) = channel::unbounded(); let server = Self::new_internal( LanguageServerId(0), stdin_writer, stdout_reader, - stderr_reader, + None::, None, Path::new("/"), None, @@ -910,7 +908,7 @@ impl LanguageServer { LanguageServerId(0), stdout_writer, stdin_reader, - stderr_reader_2, + None::, None, Path::new("/"), None, From 9e12df43d0c2b1d4fd19bc251bfbb72e6ce99fb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 11:43:18 +0300 Subject: [PATCH 105/114] Post-rebase fixes --- Cargo.lock | 482 ++++++++++++++------------- crates/zed/src/languages/tailwind.rs | 1 - 2 files changed, 243 insertions(+), 240 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4bf4506e4..a185542c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,11 +36,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli 0.27.3", + "gimli 0.28.0", ] [[package]] @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -146,7 +146,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -157,7 +157,7 @@ dependencies = [ "alacritty_config", "alacritty_config_derive", "base64 0.13.1", - "bitflags 2.3.3", + "bitflags 2.4.0", "home", "libc", "log", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -274,9 +274,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -343,7 +343,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -357,7 +357,7 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -417,15 +417,15 @@ dependencies = [ "polling", "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -488,7 +488,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -511,7 +511,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "wasm-bindgen-futures", @@ -525,7 +525,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -536,7 +536,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -573,13 +573,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -592,7 +592,7 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tungstenite 0.16.0", ] @@ -687,12 +687,12 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.9", + "itoa", "matchit", "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -733,7 +733,7 @@ dependencies = [ "futures-util", "http", "mime", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "tokio", @@ -745,16 +745,16 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ - "addr2line 0.20.0", + "addr2line 0.21.0", "cc", "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", - "object 0.31.1", + "object 0.32.0", "rustc-demangle", ] @@ -837,7 +837,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.28", + "syn 2.0.29", "which", ] @@ -864,9 +864,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -1002,7 +1002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "serde", ] @@ -1163,11 +1163,12 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -1225,7 +1226,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", @@ -1293,9 +1294,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive 4.3.12", @@ -1304,9 +1305,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", @@ -1336,7 +1337,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1398,7 +1399,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", @@ -1505,7 +1506,7 @@ dependencies = [ "sqlx", "text", "theme", - "time 0.3.24", + "time 0.3.27", "tokio", "tokio-tungstenite", "toml 0.5.11", @@ -2047,7 +2048,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi 0.3.9", ] @@ -2068,9 +2069,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" dependencies = [ "cfg-if 1.0.0", "hashbrown 0.14.0", @@ -2128,9 +2129,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ "serde", ] @@ -2318,9 +2319,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "editor" @@ -2383,9 +2384,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if 1.0.0", ] @@ -2433,9 +2434,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" dependencies = [ "serde", ] @@ -2606,13 +2607,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "windows-sys", ] @@ -2624,9 +2625,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide 0.7.1", @@ -2768,7 +2769,7 @@ dependencies = [ "sum_tree", "tempfile", "text", - "time 0.3.24", + "time 0.3.27", "util", ] @@ -2906,7 +2907,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "waker-fn", ] @@ -2918,7 +2919,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2947,7 +2948,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "tokio-io", @@ -3025,9 +3026,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "git" @@ -3070,11 +3071,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "bstr", "fnv", "log", @@ -3161,7 +3162,7 @@ dependencies = [ "sqlez", "sum_tree", "taffy", - "time 0.3.24", + "time 0.3.27", "tiny-skia", "usvg", "util", @@ -3187,9 +3188,9 @@ checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes 1.4.0", "fnv", @@ -3383,7 +3384,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.9", + "itoa", ] [[package]] @@ -3394,7 +3395,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.4.0", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -3411,9 +3412,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human_bytes" @@ -3442,9 +3443,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.9", - "pin-project-lite 0.2.10", - "socket2", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -3458,7 +3459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tokio-io-timeout", ] @@ -3676,7 +3677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -3716,12 +3717,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.9" @@ -4148,9 +4143,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "serde", "value-bag", @@ -4280,9 +4275,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" [[package]] name = "memfd" @@ -4638,9 +4633,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -4796,9 +4791,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -4840,9 +4835,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -4861,7 +4856,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4872,9 +4867,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -5009,7 +5004,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -5101,12 +5096,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -5134,22 +5129,22 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5160,9 +5155,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -5214,7 +5209,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time 0.3.24", + "time 0.3.27", ] [[package]] @@ -5279,7 +5274,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "windows-sys", ] @@ -5329,7 +5324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5670,9 +5665,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -5905,13 +5900,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -5926,11 +5921,11 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", "regex-syntax 0.7.4", ] @@ -5979,9 +5974,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "20b9b67e2ca7dd9e9f9285b759de30ff538aab981abaaf7bc9bd90b84a0126c3" dependencies = [ "base64 0.21.2", "bytes 1.4.0", @@ -6000,7 +5995,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -6220,7 +6215,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.28", + "syn 2.0.29", "walkdir", ] @@ -6237,13 +6232,12 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.31.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ "arrayvec 0.7.4", "borsh", - "byteorder", "bytes 1.4.0", "num-traits", "rand 0.8.5", @@ -6291,7 +6285,7 @@ dependencies = [ "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes 0.5.3", - "itoa 1.0.9", + "itoa", "libc", "linux-raw-sys 0.0.42", "once_cell", @@ -6314,11 +6308,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.2", "libc", "linux-raw-sys 0.4.5", @@ -6520,7 +6514,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.24", + "time 0.3.27", "tracing", "url", "uuid 1.4.1", @@ -6548,7 +6542,7 @@ dependencies = [ "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6563,7 +6557,7 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6695,7 +6689,7 @@ dependencies = [ "smol", "tempdir", "theme", - "tiktoken-rs 0.5.0", + "tiktoken-rs 0.5.1", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6743,22 +6737,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6783,24 +6777,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] [[package]] name = "serde_json_lenient" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" +checksum = "29591aaa3a13f5ad0f2dd1a8a21bcddab11eaae7c3522b20ade2e85e9df52206" dependencies = [ - "indexmap 1.9.3", - "itoa 0.4.8", + "indexmap 2.0.0", + "itoa", "ryu", "serde", ] @@ -6813,7 +6807,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6832,7 +6826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] @@ -7044,9 +7038,9 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -7128,6 +7122,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -7227,7 +7231,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "indexmap 1.9.3", - "itoa 1.0.9", + "itoa", "libc", "libsqlite3-sys", "log", @@ -7250,7 +7254,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.24", + "time 0.3.27", "tokio-stream", "url", "uuid 1.4.1", @@ -7365,7 +7369,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7376,7 +7380,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7441,9 +7445,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -7538,14 +7542,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -7691,22 +7695,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7753,9 +7757,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a99d843674a3468b4a9200a565bbe909a0152f95e82a52feae71e6bf2d4b49d" +checksum = "2bf14cb08d8fda6e484c75ec2bfb6bcef48347d47abcd011fa9d56ee995a3da0" dependencies = [ "anyhow", "base64 0.21.2", @@ -7779,12 +7783,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.24" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" +checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" dependencies = [ "deranged", - "itoa 1.0.9", + "itoa", "serde", "time-core", "time-macros", @@ -7798,9 +7802,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" dependencies = [ "time-core", ] @@ -7849,20 +7853,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes 1.4.0", "libc", "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -7884,7 +7887,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7896,7 +7899,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7927,7 +7930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7953,7 +7956,7 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7967,7 +7970,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tracing", ] @@ -8056,7 +8059,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "rand 0.8.5", "slab", "tokio", @@ -8079,7 +8082,7 @@ dependencies = [ "http", "http-body", "http-range-header", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tower", "tower-layer", "tower-service", @@ -8105,7 +8108,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tracing-attributes", "tracing-core", ] @@ -8118,7 +8121,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -8203,9 +8206,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1bb73a4101c88775e4fefcd0543ee25e192034484a5bd45cb99eefb997dca9" +checksum = "30b03bdf218020057abee831581a74bff8c298323d6c6cd1a70556430ded9f4b" dependencies = [ "cc", "tree-sitter", @@ -8352,9 +8355,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47ebd9cac632764b2f4389b08517bf2ef895431dd163eb562e3d2062cc23a14" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" dependencies = [ "cc", "tree-sitter", @@ -8381,9 +8384,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" +checksum = "b0832309b0b2b6d33760ce5c0e818cb47e1d72b468516bfe4134408926fa7594" dependencies = [ "cc", "tree-sitter", @@ -8504,9 +8507,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -8912,7 +8915,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -8946,7 +8949,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9183,9 +9186,9 @@ dependencies = [ [[package]] name = "wast" -version = "62.0.1" +version = "63.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f" +checksum = "2560471f60a48b77fccefaf40796fda61c97ce1e790b59dfcec9dc3995c9f63a" dependencies = [ "leb128", "memchr", @@ -9195,11 +9198,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637" +checksum = "3bdc306c2c4c2f2bf2ba69e083731d0d2a77437fc6a350a19db139636e7e416c" dependencies = [ - "wast 62.0.1", + "wast 63.0.0", ] [[package]] @@ -9401,7 +9404,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9410,7 +9413,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9430,17 +9433,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -9451,9 +9454,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -9463,9 +9466,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -9475,9 +9478,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -9487,9 +9490,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -9499,9 +9502,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -9511,9 +9514,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -9523,26 +9526,27 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.2" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi 0.3.9", + "cfg-if 1.0.0", + "windows-sys", ] [[package]] @@ -9662,7 +9666,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.19", + "clap 4.3.24", "schemars", "serde_json", "theme", @@ -9845,7 +9849,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 1b7c271d10..12a0a4e3b8 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -66,7 +66,6 @@ impl LspAdapter for TailwindLspAdapter { let server_path = container_dir.join(SERVER_PATH); if fs::metadata(&server_path).await.is_err() { - dbg!(&container_dir, version.as_str()); self.node .npm_install_packages( &container_dir, From fff385a585f4e83db0bc96f403141f09ebdaf5f4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 12:16:54 +0300 Subject: [PATCH 106/114] Fix project tests --- crates/editor/src/editor_tests.rs | 8 +++++++- crates/project/src/project_tests.rs | 26 ++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a3688215db..d44b8728fd 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7712,7 +7712,13 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: ) .with_override_query("(jsx_self_closing_element) @element") .unwrap(), - Default::default(), + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, cx, ) .await; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 397223c4bb..b6adb371e1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2272,7 +2272,18 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_typescript::language_typescript()), ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -2358,7 +2369,18 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_typescript::language_typescript()), ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; let fs = FakeFs::new(cx.background()); fs.insert_tree( From 292af55ebc7ac98d26d1d3f2e8d365151b473db2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 13:41:20 +0300 Subject: [PATCH 107/114] Ensure all client LSP queries are forwarded via collab --- crates/project/src/project.rs | 106 +++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index edada5c630..091e1986f6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -156,6 +156,11 @@ struct DelayedDebounced { cancel_channel: Option>, } +enum LanguageServerToQuery { + Primary, + Other(LanguageServerId), +} + impl DelayedDebounced { fn new() -> DelayedDebounced { DelayedDebounced { @@ -4199,7 +4204,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetDefinition { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDefinition { position }, + cx, + ) } pub fn type_definition( @@ -4209,7 +4219,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetTypeDefinition { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetTypeDefinition { position }, + cx, + ) } pub fn references( @@ -4219,7 +4234,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetReferences { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetReferences { position }, + cx, + ) } pub fn document_highlights( @@ -4229,7 +4249,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetDocumentHighlights { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDocumentHighlights { position }, + cx, + ) } pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { @@ -4457,7 +4482,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetHover { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetHover { position }, + cx, + ) } pub fn completions( @@ -4491,7 +4521,7 @@ impl Project { for server_id in server_ids { tasks.push(this.request_lsp( buffer.clone(), - server_id, + LanguageServerToQuery::Other(server_id), GetCompletions { position }, cx, )); @@ -4628,7 +4658,12 @@ impl Project { ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); - self.request_primary_lsp(buffer_handle.clone(), GetCodeActions { range }, cx) + self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + GetCodeActions { range }, + cx, + ) } pub fn apply_code_action( @@ -4984,7 +5019,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer, PrepareRename { position }, cx) + self.request_lsp( + buffer, + LanguageServerToQuery::Primary, + PrepareRename { position }, + cx, + ) } pub fn perform_rename( @@ -4996,8 +5036,9 @@ impl Project { cx: &mut ModelContext, ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp( + self.request_lsp( buffer, + LanguageServerToQuery::Primary, PerformRename { position, new_name, @@ -5023,8 +5064,9 @@ impl Project { .tab_size, ) }); - self.request_primary_lsp( + self.request_lsp( buffer.clone(), + LanguageServerToQuery::Primary, OnTypeFormatting { position, trigger, @@ -5050,7 +5092,12 @@ impl Project { let lsp_request = InlayHints { range }; if self.is_local() { - let lsp_request_task = self.request_primary_lsp(buffer_handle.clone(), lsp_request, cx); + let lsp_request_task = self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + lsp_request, + cx, + ); cx.spawn(|_, mut cx| async move { buffer_handle .update(&mut cx, |buffer, _| { @@ -5483,28 +5530,10 @@ impl Project { .await; } - fn request_primary_lsp( - &self, - buffer_handle: ModelHandle, - request: R, - cx: &mut ModelContext, - ) -> Task> - where - ::Result: Send, - { - let buffer = buffer_handle.read(cx); - let server_id = match self.primary_language_server_for_buffer(buffer, cx) { - Some((_, server)) => server.server_id(), - None => return Task::ready(Ok(Default::default())), - }; - - self.request_lsp(buffer_handle, server_id, request, cx) - } - fn request_lsp( &self, buffer_handle: ModelHandle, - server_id: LanguageServerId, + server: LanguageServerToQuery, request: R, cx: &mut ModelContext, ) -> Task> @@ -5513,11 +5542,18 @@ impl Project { { let buffer = buffer_handle.read(cx); if self.is_local() { + let language_server = match server { + LanguageServerToQuery::Primary => { + match self.primary_language_server_for_buffer(buffer, cx) { + Some((_, server)) => Some(Arc::clone(server)), + None => return Task::ready(Ok(Default::default())), + } + } + LanguageServerToQuery::Other(id) => self + .language_server_for_buffer(buffer, id, cx) + .map(|(_, server)| Arc::clone(server)), + }; let file = File::from_dyn(buffer.file()).and_then(File::as_local); - let language_server = self - .language_server_for_buffer(buffer, server_id, cx) - .map(|(_, server)| server.clone()); - if let (Some(file), Some(language_server)) = (file, language_server) { let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx); return cx.spawn(|this, cx| async move { @@ -7212,7 +7248,7 @@ impl Project { let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version()); let response = this .update(&mut cx, |this, cx| { - this.request_primary_lsp(buffer_handle, request, cx) + this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx) }) .await?; this.update(&mut cx, |this, cx| { From 5bc5831032a84b3897330264c3e5fddf1e770358 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 14:31:43 +0300 Subject: [PATCH 108/114] Fix wrong assertion in the test --- crates/editor/src/editor_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d44b8728fd..ad97639d0b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7780,7 +7780,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-blue", "bg-yellow"] + &["bg-yellow"] ); } else { panic!("expected completion menu to be open"); From e682db7101edd69e64ae4cc133e74f7de935098d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 15:22:13 +0300 Subject: [PATCH 109/114] Route completion requests through remote protocol, if needed --- crates/project/src/project.rs | 129 +++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 58 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 091e1986f6..5cd13b8be8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4496,47 +4496,52 @@ impl Project { position: T, cx: &mut ModelContext, ) -> Task>> { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); let position = position.to_point_utf16(buffer.read(cx)); + if self.is_local() { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(offset); - let scope = snapshot.language_scope_at(offset); + let server_ids: Vec<_> = self + .language_servers_for_buffer(buffer.read(cx), cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect(); - let server_ids: Vec<_> = self - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) + let buffer = buffer.clone(); + cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(server_ids.len()); + this.update(&mut cx, |this, cx| { + for server_id in server_ids { + tasks.push(this.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + GetCompletions { position }, + cx, + )); + } + }); + + let mut completions = Vec::new(); + for task in tasks { + if let Ok(new_completions) = task.await { + completions.extend_from_slice(&new_completions); + } + } + + Ok(completions) }) - .map(|(_, server)| server.server_id()) - .collect(); - - let buffer = buffer.clone(); - cx.spawn(|this, mut cx| async move { - let mut tasks = Vec::with_capacity(server_ids.len()); - this.update(&mut cx, |this, cx| { - for server_id in server_ids { - tasks.push(this.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetCompletions { position }, - cx, - )); - } - }); - - let mut completions = Vec::new(); - for task in tasks { - if let Ok(new_completions) = task.await { - completions.extend_from_slice(&new_completions); - } - } - - Ok(completions) - }) + } else if let Some(project_id) = self.remote_id() { + self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx) + } else { + Task::ready(Ok(Default::default())) + } } pub fn apply_additional_edits_for_completion( @@ -5587,32 +5592,40 @@ impl Project { }); } } else if let Some(project_id) = self.remote_id() { - let rpc = self.client.clone(); - let message = request.to_proto(project_id, buffer); - return cx.spawn_weak(|this, cx| async move { - // Ensure the project is still alive by the time the task - // is scheduled. - this.upgrade(&cx) - .ok_or_else(|| anyhow!("project dropped"))?; - - let response = rpc.request(message).await?; - - let this = this - .upgrade(&cx) - .ok_or_else(|| anyhow!("project dropped"))?; - if this.read_with(&cx, |this, _| this.is_read_only()) { - Err(anyhow!("disconnected before completing request")) - } else { - request - .response_from_proto(response, this, buffer_handle, cx) - .await - } - }); + return self.send_lsp_proto_request(buffer_handle, project_id, request, cx); } Task::ready(Ok(Default::default())) } + fn send_lsp_proto_request( + &self, + buffer: ModelHandle, + project_id: u64, + request: R, + cx: &mut ModelContext<'_, Project>, + ) -> Task::Response>> { + let rpc = self.client.clone(); + let message = request.to_proto(project_id, buffer.read(cx)); + cx.spawn_weak(|this, cx| async move { + // Ensure the project is still alive by the time the task + // is scheduled. + this.upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + let response = rpc.request(message).await?; + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + if this.read_with(&cx, |this, _| this.is_read_only()) { + Err(anyhow!("disconnected before completing request")) + } else { + request + .response_from_proto(response, this, buffer, cx) + .await + } + }) + } + fn sort_candidates_and_open_buffers( mut matching_paths_rx: Receiver, cx: &mut ModelContext, From 5731ef51cd8f11aaa17606a0b36de5c4edb8e432 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 15:32:24 +0300 Subject: [PATCH 110/114] Fix plugin LSP adapter intefrace --- crates/zed/src/languages/language_plugin.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index b071936392..b2405d8bb8 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -70,6 +70,10 @@ impl LspAdapter for PluginLspAdapter { LanguageServerName(name.into()) } + fn short_name(&self) -> &'static str { + "PluginLspAdapter" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, From eecd4e39ccb876cf4bed056385165c5e1a36e772 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Aug 2023 11:09:36 -0700 Subject: [PATCH 111/114] Propagate Cancel action if there is no pending inline assist --- crates/ai/src/assistant.rs | 48 +++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 2aaf75ae39..5cde99af82 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -406,36 +406,30 @@ impl AssistantPanel { _: &editor::Cancel, cx: &mut ViewContext, ) { - let panel = if let Some(panel) = workspace.panel::(cx) { - panel - } else { - return; - }; - let editor = if let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - editor - } else { - return; - }; - - let handled = panel.update(cx, |panel, cx| { - if let Some(assist_id) = panel - .pending_inline_assist_ids_by_editor - .get(&editor.downgrade()) - .and_then(|assist_ids| assist_ids.last().copied()) + if let Some(panel) = workspace.panel::(cx) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) { - panel.close_inline_assist(assist_id, true, cx); - true - } else { - false + let handled = panel.update(cx, |panel, cx| { + if let Some(assist_id) = panel + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .and_then(|assist_ids| assist_ids.last().copied()) + { + panel.close_inline_assist(assist_id, true, cx); + true + } else { + false + } + }); + if handled { + return; + } } - }); - - if !handled { - cx.propagate_action(); } + + cx.propagate_action(); } fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { From 03f0365d4d39134aed4f83381348f84be940f7a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Aug 2023 15:52:16 -0700 Subject: [PATCH 112/114] Remove local timestamps from CRDT operations Use lamport timestamps for everything. --- crates/clock/src/clock.rs | 81 ++-------- crates/collab/src/db/queries/buffers.rs | 62 +++----- crates/collab/src/db/queries/users.rs | 2 - crates/language/src/buffer.rs | 10 +- crates/language/src/proto.rs | 118 +++++++------- crates/rpc/proto/zed.proto | 12 +- crates/text/Cargo.toml | 1 + crates/text/src/anchor.rs | 6 +- crates/text/src/text.rs | 196 +++++++++--------------- crates/text/src/undo_map.rs | 12 +- 10 files changed, 186 insertions(+), 314 deletions(-) diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index bc936fcb99..3cbf8d6594 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -2,70 +2,17 @@ use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, fmt, iter, - ops::{Add, AddAssign}, }; pub type ReplicaId = u16; pub type Seq = u32; -#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)] -pub struct Local { - pub replica_id: ReplicaId, - pub value: Seq, -} - #[derive(Clone, Copy, Default, Eq, Hash, PartialEq)] pub struct Lamport { pub replica_id: ReplicaId, pub value: Seq, } -impl Local { - pub const MIN: Self = Self { - replica_id: ReplicaId::MIN, - value: Seq::MIN, - }; - pub const MAX: Self = Self { - replica_id: ReplicaId::MAX, - value: Seq::MAX, - }; - - pub fn new(replica_id: ReplicaId) -> Self { - Self { - replica_id, - value: 1, - } - } - - pub fn tick(&mut self) -> Self { - let timestamp = *self; - self.value += 1; - timestamp - } - - pub fn observe(&mut self, timestamp: Self) { - if timestamp.replica_id == self.replica_id { - self.value = cmp::max(self.value, timestamp.value + 1); - } - } -} - -impl<'a> Add<&'a Self> for Local { - type Output = Local; - - fn add(self, other: &'a Self) -> Self::Output { - *cmp::max(&self, other) - } -} - -impl<'a> AddAssign<&'a Local> for Local { - fn add_assign(&mut self, other: &Self) { - if *self < *other { - *self = *other; - } - } -} - /// A vector clock #[derive(Clone, Default, Hash, Eq, PartialEq)] pub struct Global(SmallVec<[u32; 8]>); @@ -79,7 +26,7 @@ impl Global { self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq } - pub fn observe(&mut self, timestamp: Local) { + pub fn observe(&mut self, timestamp: Lamport) { if timestamp.value > 0 { let new_len = timestamp.replica_id as usize + 1; if new_len > self.0.len() { @@ -126,7 +73,7 @@ impl Global { self.0.resize(new_len, 0); } - pub fn observed(&self, timestamp: Local) -> bool { + pub fn observed(&self, timestamp: Lamport) -> bool { self.get(timestamp.replica_id) >= timestamp.value } @@ -178,16 +125,16 @@ impl Global { false } - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().enumerate().map(|(replica_id, seq)| Local { + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().enumerate().map(|(replica_id, seq)| Lamport { replica_id: replica_id as ReplicaId, value: *seq, }) } } -impl FromIterator for Global { - fn from_iter>(locals: T) -> Self { +impl FromIterator for Global { + fn from_iter>(locals: T) -> Self { let mut result = Self::new(); for local in locals { result.observe(local); @@ -212,6 +159,16 @@ impl PartialOrd for Lamport { } impl Lamport { + pub const MIN: Self = Self { + replica_id: ReplicaId::MIN, + value: Seq::MIN, + }; + + pub const MAX: Self = Self { + replica_id: ReplicaId::MAX, + value: Seq::MAX, + }; + pub fn new(replica_id: ReplicaId) -> Self { Self { value: 1, @@ -230,12 +187,6 @@ impl Lamport { } } -impl fmt::Debug for Local { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Local {{{}: {}}}", self.replica_id, self.value) - } -} - impl fmt::Debug for Lamport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 354accc01a..f120aea1c5 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,6 +1,6 @@ use super::*; use prost::Message; -use text::{EditOperation, InsertionTimestamp, UndoOperation}; +use text::{EditOperation, UndoOperation}; impl Database { pub async fn join_channel_buffer( @@ -182,7 +182,6 @@ impl Database { .await } - #[cfg(debug_assertions)] pub async fn get_channel_buffer_collaborators( &self, channel_id: ChannelId, @@ -370,7 +369,6 @@ fn operation_to_storage( operation.replica_id, operation.lamport_timestamp, storage::Operation { - local_timestamp: operation.local_timestamp, version: version_to_storage(&operation.version), is_undo: false, edit_ranges: operation @@ -389,7 +387,6 @@ fn operation_to_storage( operation.replica_id, operation.lamport_timestamp, storage::Operation { - local_timestamp: operation.local_timestamp, version: version_to_storage(&operation.version), is_undo: true, edit_ranges: Vec::new(), @@ -399,7 +396,7 @@ fn operation_to_storage( .iter() .map(|entry| storage::UndoCount { replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, + lamport_timestamp: entry.lamport_timestamp, count: entry.count, }) .collect(), @@ -427,7 +424,6 @@ fn operation_from_storage( Ok(if operation.is_undo { proto::operation::Variant::Undo(proto::operation::Undo { replica_id: row.replica_id as u32, - local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, counts: operation @@ -435,7 +431,7 @@ fn operation_from_storage( .iter() .map(|entry| proto::UndoCount { replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, + lamport_timestamp: entry.lamport_timestamp, count: entry.count, }) .collect(), @@ -443,7 +439,6 @@ fn operation_from_storage( } else { proto::operation::Variant::Edit(proto::operation::Edit { replica_id: row.replica_id as u32, - local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, ranges: operation @@ -483,10 +478,9 @@ fn version_from_storage(version: &Vec) -> Vec Option { match operation.variant? { proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { - timestamp: InsertionTimestamp { + timestamp: clock::Lamport { replica_id: edit.replica_id as text::ReplicaId, - local: edit.local_timestamp, - lamport: edit.lamport_timestamp, + value: edit.lamport_timestamp, }, version: version_from_wire(&edit.version), ranges: edit @@ -498,32 +492,26 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option Some(text::Operation::Undo { - lamport_timestamp: clock::Lamport { + proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo(UndoOperation { + timestamp: clock::Lamport { replica_id: undo.replica_id as text::ReplicaId, value: undo.lamport_timestamp, }, - undo: UndoOperation { - id: clock::Local { - replica_id: undo.replica_id as text::ReplicaId, - value: undo.local_timestamp, - }, - version: version_from_wire(&undo.version), - counts: undo - .counts - .into_iter() - .map(|c| { - ( - clock::Local { - replica_id: c.replica_id as text::ReplicaId, - value: c.local_timestamp, - }, - c.count, - ) - }) - .collect(), - }, - }), + version: version_from_wire(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Lamport { + replica_id: c.replica_id as text::ReplicaId, + value: c.lamport_timestamp, + }, + c.count, + ) + }) + .collect(), + })), _ => None, } } @@ -531,7 +519,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option clock::Global { let mut version = clock::Global::new(); for entry in message { - version.observe(clock::Local { + version.observe(clock::Lamport { replica_id: entry.replica_id as text::ReplicaId, value: entry.timestamp, }); @@ -546,8 +534,6 @@ mod storage { #[derive(Message)] pub struct Operation { - #[prost(uint32, tag = "1")] - pub local_timestamp: u32, #[prost(message, repeated, tag = "2")] pub version: Vec, #[prost(bool, tag = "3")] @@ -581,7 +567,7 @@ mod storage { #[prost(uint32, tag = "1")] pub replica_id: u32, #[prost(uint32, tag = "2")] - pub local_timestamp: u32, + pub lamport_timestamp: u32, #[prost(uint32, tag = "3")] pub count: u32, } diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bd7c3e9ffd..5cb1ef6ea3 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -241,7 +241,6 @@ impl Database { result } - #[cfg(debug_assertions)] pub async fn create_user_flag(&self, flag: &str) -> Result { self.transaction(|tx| async move { let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { @@ -257,7 +256,6 @@ impl Database { .await } - #[cfg(debug_assertions)] pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { self.transaction(|tx| async move { user_feature::Entity::insert(user_feature::ActiveModel { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1ded955cd7..38b2842c12 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -439,7 +439,7 @@ impl Buffer { operations.extend( text_operations .iter() - .filter(|(_, op)| !since.observed(op.local_timestamp())) + .filter(|(_, op)| !since.observed(op.timestamp())) .map(|(_, op)| proto::serialize_operation(&Operation::Buffer(op.clone()))), ); operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation); @@ -1304,7 +1304,7 @@ impl Buffer { pub fn wait_for_edits( &mut self, - edit_ids: impl IntoIterator, + edit_ids: impl IntoIterator, ) -> impl Future> { self.text.wait_for_edits(edit_ids) } @@ -1362,7 +1362,7 @@ impl Buffer { } } - pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option + pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option where T: Into>, { @@ -1375,7 +1375,7 @@ impl Buffer { edits_iter: I, autoindent_mode: Option, cx: &mut ModelContext, - ) -> Option + ) -> Option where I: IntoIterator, T)>, S: ToOffset, @@ -1412,7 +1412,7 @@ impl Buffer { .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); let edit_operation = self.text.edit(edits.iter().cloned()); - let edit_id = edit_operation.local_timestamp(); + let edit_id = edit_operation.timestamp(); if let Some((before_edit, mode)) = autoindent_request { let mut delta = 0isize; diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c88abc08ac..80eb972f42 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -41,24 +41,22 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { proto::operation::Variant::Edit(serialize_edit_operation(edit)) } - crate::Operation::Buffer(text::Operation::Undo { - undo, - lamport_timestamp, - }) => proto::operation::Variant::Undo(proto::operation::Undo { - replica_id: undo.id.replica_id as u32, - local_timestamp: undo.id.value, - lamport_timestamp: lamport_timestamp.value, - version: serialize_version(&undo.version), - counts: undo - .counts - .iter() - .map(|(edit_id, count)| proto::UndoCount { - replica_id: edit_id.replica_id as u32, - local_timestamp: edit_id.value, - count: *count, - }) - .collect(), - }), + crate::Operation::Buffer(text::Operation::Undo(undo)) => { + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: undo.timestamp.replica_id as u32, + lamport_timestamp: undo.timestamp.value, + version: serialize_version(&undo.version), + counts: undo + .counts + .iter() + .map(|(edit_id, count)| proto::UndoCount { + replica_id: edit_id.replica_id as u32, + lamport_timestamp: edit_id.value, + count: *count, + }) + .collect(), + }) + } crate::Operation::UpdateSelections { selections, @@ -101,8 +99,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit { proto::operation::Edit { replica_id: operation.timestamp.replica_id as u32, - local_timestamp: operation.timestamp.local, - lamport_timestamp: operation.timestamp.lamport, + lamport_timestamp: operation.timestamp.value, version: serialize_version(&operation.version), ranges: operation.ranges.iter().map(serialize_range).collect(), new_text: operation @@ -114,7 +111,7 @@ pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation:: } pub fn serialize_undo_map_entry( - (edit_id, counts): (&clock::Local, &[(clock::Local, u32)]), + (edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]), ) -> proto::UndoMapEntry { proto::UndoMapEntry { replica_id: edit_id.replica_id as u32, @@ -123,7 +120,7 @@ pub fn serialize_undo_map_entry( .iter() .map(|(undo_id, count)| proto::UndoCount { replica_id: undo_id.replica_id as u32, - local_timestamp: undo_id.value, + lamport_timestamp: undo_id.value, count: *count, }) .collect(), @@ -197,7 +194,7 @@ pub fn serialize_diagnostics<'a>( pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { proto::Anchor { replica_id: anchor.timestamp.replica_id as u32, - local_timestamp: anchor.timestamp.value, + timestamp: anchor.timestamp.value, offset: anchor.offset as u64, bias: match anchor.bias { Bias::Left => proto::Bias::Left as i32, @@ -218,32 +215,26 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { - crate::Operation::Buffer(text::Operation::Undo { - lamport_timestamp: clock::Lamport { + crate::Operation::Buffer(text::Operation::Undo(UndoOperation { + timestamp: clock::Lamport { replica_id: undo.replica_id as ReplicaId, value: undo.lamport_timestamp, }, - undo: UndoOperation { - id: clock::Local { - replica_id: undo.replica_id as ReplicaId, - value: undo.local_timestamp, - }, - version: deserialize_version(&undo.version), - counts: undo - .counts - .into_iter() - .map(|c| { - ( - clock::Local { - replica_id: c.replica_id as ReplicaId, - value: c.local_timestamp, - }, - c.count, - ) - }) - .collect(), - }, - }) + version: deserialize_version(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Lamport { + replica_id: c.replica_id as ReplicaId, + value: c.lamport_timestamp, + }, + c.count, + ) + }) + .collect(), + })) } proto::operation::Variant::UpdateSelections(message) => { let selections = message @@ -298,10 +289,9 @@ pub fn deserialize_operation(message: proto::Operation) -> Result EditOperation { EditOperation { - timestamp: InsertionTimestamp { + timestamp: clock::Lamport { replica_id: edit.replica_id as ReplicaId, - local: edit.local_timestamp, - lamport: edit.lamport_timestamp, + value: edit.lamport_timestamp, }, version: deserialize_version(&edit.version), ranges: edit.ranges.into_iter().map(deserialize_range).collect(), @@ -311,9 +301,9 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation pub fn deserialize_undo_map_entry( entry: proto::UndoMapEntry, -) -> (clock::Local, Vec<(clock::Local, u32)>) { +) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) { ( - clock::Local { + clock::Lamport { replica_id: entry.replica_id as u16, value: entry.local_timestamp, }, @@ -322,9 +312,9 @@ pub fn deserialize_undo_map_entry( .into_iter() .map(|undo_count| { ( - clock::Local { + clock::Lamport { replica_id: undo_count.replica_id as u16, - value: undo_count.local_timestamp, + value: undo_count.lamport_timestamp, }, undo_count.count, ) @@ -384,9 +374,9 @@ pub fn deserialize_diagnostics( pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { Some(Anchor { - timestamp: clock::Local { + timestamp: clock::Lamport { replica_id: anchor.replica_id as ReplicaId, - value: anchor.local_timestamp, + value: anchor.timestamp, }, offset: anchor.offset as usize, bias: match proto::Bias::from_i32(anchor.bias)? { @@ -500,12 +490,12 @@ pub fn deserialize_code_action(action: proto::CodeAction) -> Result pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { proto::Transaction { - id: Some(serialize_local_timestamp(transaction.id)), + id: Some(serialize_timestamp(transaction.id)), edit_ids: transaction .edit_ids .iter() .copied() - .map(serialize_local_timestamp) + .map(serialize_timestamp) .collect(), start: serialize_version(&transaction.start), } @@ -513,7 +503,7 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { pub fn deserialize_transaction(transaction: proto::Transaction) -> Result { Ok(Transaction { - id: deserialize_local_timestamp( + id: deserialize_timestamp( transaction .id .ok_or_else(|| anyhow!("missing transaction id"))?, @@ -521,21 +511,21 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result proto::LocalTimestamp { - proto::LocalTimestamp { +pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp { + proto::LamportTimestamp { replica_id: timestamp.replica_id as u32, value: timestamp.value, } } -pub fn deserialize_local_timestamp(timestamp: proto::LocalTimestamp) -> clock::Local { - clock::Local { +pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport { + clock::Lamport { replica_id: timestamp.replica_id as ReplicaId, value: timestamp.value, } @@ -555,7 +545,7 @@ pub fn deserialize_range(range: proto::Range) -> Range { pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global { let mut version = clock::Global::new(); for entry in message { - version.observe(clock::Local { + version.observe(clock::Lamport { replica_id: entry.replica_id as ReplicaId, value: entry.timestamp, }); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5e96ea043c..61c25f8f84 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -861,12 +861,12 @@ message ProjectTransaction { } message Transaction { - LocalTimestamp id = 1; - repeated LocalTimestamp edit_ids = 2; + LamportTimestamp id = 1; + repeated LamportTimestamp edit_ids = 2; repeated VectorClockEntry start = 3; } -message LocalTimestamp { +message LamportTimestamp { uint32 replica_id = 1; uint32 value = 2; } @@ -1280,7 +1280,7 @@ message Excerpt { message Anchor { uint32 replica_id = 1; - uint32 local_timestamp = 2; + uint32 timestamp = 2; uint64 offset = 3; Bias bias = 4; optional uint64 buffer_id = 5; @@ -1324,7 +1324,6 @@ message Operation { message Edit { uint32 replica_id = 1; - uint32 local_timestamp = 2; uint32 lamport_timestamp = 3; repeated VectorClockEntry version = 4; repeated Range ranges = 5; @@ -1333,7 +1332,6 @@ message Operation { message Undo { uint32 replica_id = 1; - uint32 local_timestamp = 2; uint32 lamport_timestamp = 3; repeated VectorClockEntry version = 4; repeated UndoCount counts = 5; @@ -1362,7 +1360,7 @@ message UndoMapEntry { message UndoCount { uint32 replica_id = 1; - uint32 local_timestamp = 2; + uint32 lamport_timestamp = 2; uint32 count = 3; } diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 65e9b6fcec..d1bc6cc8f8 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -31,6 +31,7 @@ regex.workspace = true [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true rand.workspace = true diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index b5f4fb24ec..084be0e336 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -8,7 +8,7 @@ use sum_tree::Bias; #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] pub struct Anchor { - pub timestamp: clock::Local, + pub timestamp: clock::Lamport, pub offset: usize, pub bias: Bias, pub buffer_id: Option, @@ -16,14 +16,14 @@ pub struct Anchor { impl Anchor { pub const MIN: Self = Self { - timestamp: clock::Local::MIN, + timestamp: clock::Lamport::MIN, offset: usize::MIN, bias: Bias::Left, buffer_id: None, }; pub const MAX: Self = Self { - timestamp: clock::Local::MAX, + timestamp: clock::Lamport::MAX, offset: usize::MAX, bias: Bias::Right, buffer_id: None, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 2fabb0f87f..c05ea1109c 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -46,18 +46,16 @@ lazy_static! { static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); } -pub type TransactionId = clock::Local; +pub type TransactionId = clock::Lamport; pub struct Buffer { snapshot: BufferSnapshot, history: History, deferred_ops: OperationQueue, deferred_replicas: HashSet, - replica_id: ReplicaId, - local_clock: clock::Local, pub lamport_clock: clock::Lamport, subscriptions: Topic, - edit_id_resolvers: HashMap>>, + edit_id_resolvers: HashMap>>, wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>, } @@ -85,7 +83,7 @@ pub struct HistoryEntry { #[derive(Clone, Debug)] pub struct Transaction { pub id: TransactionId, - pub edit_ids: Vec, + pub edit_ids: Vec, pub start: clock::Global, } @@ -97,8 +95,8 @@ impl HistoryEntry { struct History { base_text: Rope, - operations: TreeMap, - insertion_slices: HashMap>, + operations: TreeMap, + insertion_slices: HashMap>, undo_stack: Vec, redo_stack: Vec, transaction_depth: usize, @@ -107,7 +105,7 @@ struct History { #[derive(Clone, Debug)] struct InsertionSlice { - insertion_id: clock::Local, + insertion_id: clock::Lamport, range: Range, } @@ -129,18 +127,18 @@ impl History { } fn push(&mut self, op: Operation) { - self.operations.insert(op.local_timestamp(), op); + self.operations.insert(op.timestamp(), op); } fn start_transaction( &mut self, start: clock::Global, now: Instant, - local_clock: &mut clock::Local, + clock: &mut clock::Lamport, ) -> Option { self.transaction_depth += 1; if self.transaction_depth == 1 { - let id = local_clock.tick(); + let id = clock.tick(); self.undo_stack.push(HistoryEntry { transaction: Transaction { id, @@ -251,7 +249,7 @@ impl History { self.redo_stack.clear(); } - fn push_undo(&mut self, op_id: clock::Local) { + fn push_undo(&mut self, op_id: clock::Lamport) { assert_ne!(self.transaction_depth, 0); if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { let last_transaction = self.undo_stack.last_mut().unwrap(); @@ -412,37 +410,14 @@ impl Edit<(D1, D2)> { } } -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] -pub struct InsertionTimestamp { - pub replica_id: ReplicaId, - pub local: clock::Seq, - pub lamport: clock::Seq, -} - -impl InsertionTimestamp { - pub fn local(&self) -> clock::Local { - clock::Local { - replica_id: self.replica_id, - value: self.local, - } - } - - pub fn lamport(&self) -> clock::Lamport { - clock::Lamport { - replica_id: self.replica_id, - value: self.lamport, - } - } -} - #[derive(Eq, PartialEq, Clone, Debug)] pub struct Fragment { pub id: Locator, - pub insertion_timestamp: InsertionTimestamp, + pub timestamp: clock::Lamport, pub insertion_offset: usize, pub len: usize, pub visible: bool, - pub deletions: HashSet, + pub deletions: HashSet, pub max_undos: clock::Global, } @@ -470,29 +445,26 @@ impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary { #[derive(Eq, PartialEq, Clone, Debug)] struct InsertionFragment { - timestamp: clock::Local, + timestamp: clock::Lamport, split_offset: usize, fragment_id: Locator, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct InsertionFragmentKey { - timestamp: clock::Local, + timestamp: clock::Lamport, split_offset: usize, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Operation { Edit(EditOperation), - Undo { - undo: UndoOperation, - lamport_timestamp: clock::Lamport, - }, + Undo(UndoOperation), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct EditOperation { - pub timestamp: InsertionTimestamp, + pub timestamp: clock::Lamport, pub version: clock::Global, pub ranges: Vec>, pub new_text: Vec>, @@ -500,9 +472,9 @@ pub struct EditOperation { #[derive(Clone, Debug, Eq, PartialEq)] pub struct UndoOperation { - pub id: clock::Local, - pub counts: HashMap, + pub timestamp: clock::Lamport, pub version: clock::Global, + pub counts: HashMap, } impl Buffer { @@ -514,24 +486,21 @@ impl Buffer { let mut fragments = SumTree::new(); let mut insertions = SumTree::new(); - let mut local_clock = clock::Local::new(replica_id); let mut lamport_clock = clock::Lamport::new(replica_id); let mut version = clock::Global::new(); let visible_text = history.base_text.clone(); if !visible_text.is_empty() { - let insertion_timestamp = InsertionTimestamp { + let insertion_timestamp = clock::Lamport { replica_id: 0, - local: 1, - lamport: 1, + value: 1, }; - local_clock.observe(insertion_timestamp.local()); - lamport_clock.observe(insertion_timestamp.lamport()); - version.observe(insertion_timestamp.local()); + lamport_clock.observe(insertion_timestamp); + version.observe(insertion_timestamp); let fragment_id = Locator::between(&Locator::min(), &Locator::max()); let fragment = Fragment { id: fragment_id, - insertion_timestamp, + timestamp: insertion_timestamp, insertion_offset: 0, len: visible_text.len(), visible: true, @@ -557,8 +526,6 @@ impl Buffer { history, deferred_ops: OperationQueue::new(), deferred_replicas: HashSet::default(), - replica_id, - local_clock, lamport_clock, subscriptions: Default::default(), edit_id_resolvers: Default::default(), @@ -575,7 +542,7 @@ impl Buffer { } pub fn replica_id(&self) -> ReplicaId { - self.local_clock.replica_id + self.lamport_clock.replica_id } pub fn remote_id(&self) -> u64 { @@ -602,16 +569,12 @@ impl Buffer { .map(|(range, new_text)| (range, new_text.into())); self.start_transaction(); - let timestamp = InsertionTimestamp { - replica_id: self.replica_id, - local: self.local_clock.tick().value, - lamport: self.lamport_clock.tick().value, - }; + let timestamp = self.lamport_clock.tick(); let operation = Operation::Edit(self.apply_local_edit(edits, timestamp)); self.history.push(operation.clone()); - self.history.push_undo(operation.local_timestamp()); - self.snapshot.version.observe(operation.local_timestamp()); + self.history.push_undo(operation.timestamp()); + self.snapshot.version.observe(operation.timestamp()); self.end_transaction(); operation } @@ -619,7 +582,7 @@ impl Buffer { fn apply_local_edit>>( &mut self, edits: impl ExactSizeIterator, T)>, - timestamp: InsertionTimestamp, + timestamp: clock::Lamport, ) -> EditOperation { let mut edits_patch = Patch::default(); let mut edit_op = EditOperation { @@ -696,7 +659,7 @@ impl Buffer { .item() .map_or(&Locator::max(), |old_fragment| &old_fragment.id), ), - insertion_timestamp: timestamp, + timestamp, insertion_offset, len: new_text.len(), deletions: Default::default(), @@ -726,7 +689,7 @@ impl Buffer { intersection.insertion_offset += fragment_start - old_fragments.start().visible; intersection.id = Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp.local()); + intersection.deletions.insert(timestamp); intersection.visible = false; } if intersection.len > 0 { @@ -781,7 +744,7 @@ impl Buffer { self.subscriptions.publish_mut(&edits_patch); self.history .insertion_slices - .insert(timestamp.local(), insertion_slices); + .insert(timestamp, insertion_slices); edit_op } @@ -808,28 +771,23 @@ impl Buffer { fn apply_op(&mut self, op: Operation) -> Result<()> { match op { Operation::Edit(edit) => { - if !self.version.observed(edit.timestamp.local()) { + if !self.version.observed(edit.timestamp) { self.apply_remote_edit( &edit.version, &edit.ranges, &edit.new_text, edit.timestamp, ); - self.snapshot.version.observe(edit.timestamp.local()); - self.local_clock.observe(edit.timestamp.local()); - self.lamport_clock.observe(edit.timestamp.lamport()); - self.resolve_edit(edit.timestamp.local()); + self.snapshot.version.observe(edit.timestamp); + self.lamport_clock.observe(edit.timestamp); + self.resolve_edit(edit.timestamp); } } - Operation::Undo { - undo, - lamport_timestamp, - } => { - if !self.version.observed(undo.id) { + Operation::Undo(undo) => { + if !self.version.observed(undo.timestamp) { self.apply_undo(&undo)?; - self.snapshot.version.observe(undo.id); - self.local_clock.observe(undo.id); - self.lamport_clock.observe(lamport_timestamp); + self.snapshot.version.observe(undo.timestamp); + self.lamport_clock.observe(undo.timestamp); } } } @@ -849,7 +807,7 @@ impl Buffer { version: &clock::Global, ranges: &[Range], new_text: &[Arc], - timestamp: InsertionTimestamp, + timestamp: clock::Lamport, ) { if ranges.is_empty() { return; @@ -916,9 +874,7 @@ impl Buffer { // Skip over insertions that are concurrent to this edit, but have a lower lamport // timestamp. while let Some(fragment) = old_fragments.item() { - if fragment_start == range.start - && fragment.insertion_timestamp.lamport() > timestamp.lamport() - { + if fragment_start == range.start && fragment.timestamp > timestamp { new_ropes.push_fragment(fragment, fragment.visible); new_fragments.push(fragment.clone(), &None); old_fragments.next(&cx); @@ -955,7 +911,7 @@ impl Buffer { .item() .map_or(&Locator::max(), |old_fragment| &old_fragment.id), ), - insertion_timestamp: timestamp, + timestamp, insertion_offset, len: new_text.len(), deletions: Default::default(), @@ -986,7 +942,7 @@ impl Buffer { fragment_start - old_fragments.start().0.full_offset(); intersection.id = Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp.local()); + intersection.deletions.insert(timestamp); intersection.visible = false; insertion_slices.push(intersection.insertion_slice()); } @@ -1038,13 +994,13 @@ impl Buffer { self.snapshot.insertions.edit(new_insertions, &()); self.history .insertion_slices - .insert(timestamp.local(), insertion_slices); + .insert(timestamp, insertion_slices); self.subscriptions.publish_mut(&edits_patch) } fn fragment_ids_for_edits<'a>( &'a self, - edit_ids: impl Iterator, + edit_ids: impl Iterator, ) -> Vec<&'a Locator> { // Get all of the insertion slices changed by the given edits. let mut insertion_slices = Vec::new(); @@ -1105,7 +1061,7 @@ impl Buffer { let fragment_was_visible = fragment.visible; fragment.visible = fragment.is_visible(&self.undo_map); - fragment.max_undos.observe(undo.id); + fragment.max_undos.observe(undo.timestamp); let old_start = old_fragments.start().1; let new_start = new_fragments.summary().text.visible; @@ -1159,10 +1115,10 @@ impl Buffer { if self.deferred_replicas.contains(&op.replica_id()) { false } else { - match op { - Operation::Edit(edit) => self.version.observed_all(&edit.version), - Operation::Undo { undo, .. } => self.version.observed_all(&undo.version), - } + self.version.observed_all(match op { + Operation::Edit(edit) => &edit.version, + Operation::Undo(undo) => &undo.version, + }) } } @@ -1180,7 +1136,7 @@ impl Buffer { pub fn start_transaction_at(&mut self, now: Instant) -> Option { self.history - .start_transaction(self.version.clone(), now, &mut self.local_clock) + .start_transaction(self.version.clone(), now, &mut self.lamport_clock) } pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> { @@ -1209,7 +1165,7 @@ impl Buffer { &self.history.base_text } - pub fn operations(&self) -> &TreeMap { + pub fn operations(&self) -> &TreeMap { &self.history.operations } @@ -1289,16 +1245,13 @@ impl Buffer { } let undo = UndoOperation { - id: self.local_clock.tick(), + timestamp: self.lamport_clock.tick(), version: self.version(), counts, }; self.apply_undo(&undo)?; - let operation = Operation::Undo { - undo, - lamport_timestamp: self.lamport_clock.tick(), - }; - self.snapshot.version.observe(operation.local_timestamp()); + self.snapshot.version.observe(undo.timestamp); + let operation = Operation::Undo(undo); self.history.push(operation.clone()); Ok(operation) } @@ -1363,7 +1316,7 @@ impl Buffer { pub fn wait_for_edits( &mut self, - edit_ids: impl IntoIterator, + edit_ids: impl IntoIterator, ) -> impl 'static + Future> { let mut futures = Vec::new(); for edit_id in edit_ids { @@ -1435,7 +1388,7 @@ impl Buffer { self.wait_for_version_txs.clear(); } - fn resolve_edit(&mut self, edit_id: clock::Local) { + fn resolve_edit(&mut self, edit_id: clock::Lamport) { for mut tx in self .edit_id_resolvers .remove(&edit_id) @@ -1513,7 +1466,7 @@ impl Buffer { .insertions .get( &InsertionFragmentKey { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, split_offset: fragment.insertion_offset, }, &(), @@ -1996,7 +1949,7 @@ impl BufferSnapshot { let fragment = fragment_cursor.item().unwrap(); let overshoot = offset - *fragment_cursor.start(); Anchor { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, offset: fragment.insertion_offset + overshoot, bias, buffer_id: Some(self.remote_id), @@ -2188,15 +2141,14 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo break; } - let timestamp = fragment.insertion_timestamp.local(); let start_anchor = Anchor { - timestamp, + timestamp: fragment.timestamp, offset: fragment.insertion_offset, bias: Bias::Right, buffer_id: Some(self.buffer_id), }; let end_anchor = Anchor { - timestamp, + timestamp: fragment.timestamp, offset: fragment.insertion_offset + fragment.len, bias: Bias::Left, buffer_id: Some(self.buffer_id), @@ -2269,19 +2221,17 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo impl Fragment { fn insertion_slice(&self) -> InsertionSlice { InsertionSlice { - insertion_id: self.insertion_timestamp.local(), + insertion_id: self.timestamp, range: self.insertion_offset..self.insertion_offset + self.len, } } fn is_visible(&self, undos: &UndoMap) -> bool { - !undos.is_undone(self.insertion_timestamp.local()) - && self.deletions.iter().all(|d| undos.is_undone(*d)) + !undos.is_undone(self.timestamp) && self.deletions.iter().all(|d| undos.is_undone(*d)) } fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool { - (version.observed(self.insertion_timestamp.local()) - && !undos.was_undone(self.insertion_timestamp.local(), version)) + (version.observed(self.timestamp) && !undos.was_undone(self.timestamp, version)) && self .deletions .iter() @@ -2294,14 +2244,14 @@ impl sum_tree::Item for Fragment { fn summary(&self) -> Self::Summary { let mut max_version = clock::Global::new(); - max_version.observe(self.insertion_timestamp.local()); + max_version.observe(self.timestamp); for deletion in &self.deletions { max_version.observe(*deletion); } max_version.join(&self.max_undos); let mut min_insertion_version = clock::Global::new(); - min_insertion_version.observe(self.insertion_timestamp.local()); + min_insertion_version.observe(self.timestamp); let max_insertion_version = min_insertion_version.clone(); if self.visible { FragmentSummary { @@ -2378,7 +2328,7 @@ impl sum_tree::KeyedItem for InsertionFragment { impl InsertionFragment { fn new(fragment: &Fragment) -> Self { Self { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, split_offset: fragment.insertion_offset, fragment_id: fragment.id.clone(), } @@ -2501,10 +2451,10 @@ impl Operation { operation_queue::Operation::lamport_timestamp(self).replica_id } - pub fn local_timestamp(&self) -> clock::Local { + pub fn timestamp(&self) -> clock::Lamport { match self { - Operation::Edit(edit) => edit.timestamp.local(), - Operation::Undo { undo, .. } => undo.id, + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, } } @@ -2523,10 +2473,8 @@ impl Operation { impl operation_queue::Operation for Operation { fn lamport_timestamp(&self) -> clock::Lamport { match self { - Operation::Edit(edit) => edit.timestamp.lamport(), - Operation::Undo { - lamport_timestamp, .. - } => *lamport_timestamp, + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, } } } diff --git a/crates/text/src/undo_map.rs b/crates/text/src/undo_map.rs index ff1b241e73..f95809c02e 100644 --- a/crates/text/src/undo_map.rs +++ b/crates/text/src/undo_map.rs @@ -26,8 +26,8 @@ impl sum_tree::KeyedItem for UndoMapEntry { #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct UndoMapKey { - edit_id: clock::Local, - undo_id: clock::Local, + edit_id: clock::Lamport, + undo_id: clock::Lamport, } impl sum_tree::Summary for UndoMapKey { @@ -50,7 +50,7 @@ impl UndoMap { sum_tree::Edit::Insert(UndoMapEntry { key: UndoMapKey { edit_id: *edit_id, - undo_id: undo.id, + undo_id: undo.timestamp, }, undo_count: *count, }) @@ -59,11 +59,11 @@ impl UndoMap { self.0.edit(edits, &()); } - pub fn is_undone(&self, edit_id: clock::Local) -> bool { + pub fn is_undone(&self, edit_id: clock::Lamport) -> bool { self.undo_count(edit_id) % 2 == 1 } - pub fn was_undone(&self, edit_id: clock::Local, version: &clock::Global) -> bool { + pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool { let mut cursor = self.0.cursor::(); cursor.seek( &UndoMapKey { @@ -88,7 +88,7 @@ impl UndoMap { undo_count % 2 == 1 } - pub fn undo_count(&self, edit_id: clock::Local) -> u32 { + pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 { let mut cursor = self.0.cursor::(); cursor.seek( &UndoMapKey { From 1e604546434bbafa79b33087cc1ddb6eca42dc9e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Aug 2023 16:31:26 -0700 Subject: [PATCH 113/114] Renumber protobuf fields, bump protocol version --- crates/rpc/proto/zed.proto | 14 +++++++------- crates/rpc/src/rpc.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 61c25f8f84..92c85677f6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1324,17 +1324,17 @@ message Operation { message Edit { uint32 replica_id = 1; - uint32 lamport_timestamp = 3; - repeated VectorClockEntry version = 4; - repeated Range ranges = 5; - repeated string new_text = 6; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated Range ranges = 4; + repeated string new_text = 5; } message Undo { uint32 replica_id = 1; - uint32 lamport_timestamp = 3; - repeated VectorClockEntry version = 4; - repeated UndoCount counts = 5; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated UndoCount counts = 4; } message UpdateSelections { diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index bc9dd6f80b..d64cbae929 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 61; +pub const PROTOCOL_VERSION: u32 = 62; From d868ec920f4bb13132921c0d29f451a288befc75 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Sep 2023 09:15:29 +0200 Subject: [PATCH 114/114] Avoid duplicate entries in inline assistant's prompt history --- crates/ai/src/assistant.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 5cde99af82..9b384252fc 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -507,10 +507,13 @@ impl AssistantPanel { return; }; + self.inline_prompt_history + .retain(|prompt| prompt != user_prompt); self.inline_prompt_history.push_back(user_prompt.into()); if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { self.inline_prompt_history.pop_front(); } + let range = pending_assist.range.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selected_text = snapshot