From 7a67ec57436eb048f10b2febb3b0220f3a7d1fa0 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 15 Aug 2023 12:48:30 -0400 Subject: [PATCH 001/115] Add support for querying multiple language servers for completions --- crates/project/src/project.rs | 82 ++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1aa2a2dd40c2d01b98860d39420c6ec4ab894728..f85460770d9d55e2d1d47b9380101ddf47977bed 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,10 +5245,28 @@ impl Project { } } - // TODO: Wire this up to allow selecting a server? + 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> @@ -5236,10 +5276,11 @@ impl Project { 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/115] 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 0b10432a9f4747d93ff974ac72ddbbb6783fe676..ec5f9541f58fde77287e43436d1c9cce1f84bd54 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 0de3f704c7f29e4d1cff8f3fe371d0fad36d1f42..c463c7a01c481a2211431d865d00b0124fb2fda6 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 a8692257d8032fdca5667c2089249e806b241e34..ad5b63ae2a65809f1e7f5d64e501c9016562406b 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 f85460770d9d55e2d1d47b9380101ddf47977bed..7986d6faecc392298a0ccaf3cf60dd2a2f643526 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 a0b98372b10f05248e1c49fc14844b9de61274f9..2bc31dccc32cc5d6db8dbc812f7491172b0e026a 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 6b430d90e46072a6f885b60a4e912978ed26c6a2..3cb8b6bffa2ca1549ca854db39e46ef8fc8634a7 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/115] 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 eb31c08dd238d64595d7a0dc4c5b1760951d88a9..0f8699aff262381992555bb00532178bee3bc787 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 0000000000000000000000000000000000000000..79f65eb5788ecda20dcb890e0ba9d6bb830c2188 --- /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/115] 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 e0ae64d8069c08b12e11b8b12155892dc974ae0d..66ef33418b80dccd81fcf661255b84946a161989 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 7986d6faecc392298a0ccaf3cf60dd2a2f643526..4e16ee3da2478817fd9d33369a71412b4958f64f 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/115] 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 79f65eb5788ecda20dcb890e0ba9d6bb830c2188..0290bf33342d9bb7e649301934fd7f5b724c90ba 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/115] 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 1ff9981a6a0bd52e9d1ac24169fbc428db1ff0ce..4e1d0b53a6102df67ab2855a38c28c166d8061d2 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 47e0995c852ab49417bdfafe714123c85db3d223..c3ff37dc61bf5ca8f22f842eb7883481e7c65da1 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 66ef33418b80dccd81fcf661255b84946a161989..f39d97aeb56ae2ceaa919a59d6874a31e9328351 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 ad5b63ae2a65809f1e7f5d64e501c9016562406b..c1bb890d496b771b680084d0cc708d27655f2988 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/115] 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 4e1d0b53a6102df67ab2855a38c28c166d8061d2..e27b5756aa9f197a8aac1659ad6e26720a4b100d 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 c3ff37dc61bf5ca8f22f842eb7883481e7c65da1..653c25b7bb21de87b793b4b9a94e5e73dc79e1c0 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/115] 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 7cc5f08f7c1f30102308188865299ff8ee1af833..7874bb46a5826989c9d23eedc1a48df7d838c5cb 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 0000000000000000000000000000000000000000..e1b57680eec6efd704b10fb7d371de29f855d17d --- /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 0000000000000000000000000000000000000000..33213a585967d6d63b0243a5cd713794cdfaaf48 --- /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/115] 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 69285a1abff776cb2c0af3ac0f7f39efb6353dbf..f802d907390439aa22f32034998f995aacacfcdd 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 013565e14f449863f3b81be1d94bf4562b4323e0..bae20f7537c7442e29eeb7013e223b8d7ce422b2 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 7874bb46a5826989c9d23eedc1a48df7d838c5cb..511e7fddd7089263f60a767e6c7ba7e1f6733e44 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 e5026182ed2f0b9cc7516df9e31f73a2c53d8c5d..f134eeeeb69ab1cfc291ffa29e0679436cff7219 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 e1b57680eec6efd704b10fb7d371de29f855d17d..fc6cbdb8c435f86fa78e907c59f40871a713f818 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 33213a585967d6d63b0243a5cd713794cdfaaf48..0000000000000000000000000000000000000000 --- 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/115] 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 fc6cbdb8c435f86fa78e907c59f40871a713f818..1a1d02cf1f2c55e78ee500993aa8f2621b49ffe0 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/115] WIP --- Cargo.lock | 1 + crates/ai/Cargo.toml | 1 + crates/ai/src/refactor.rs | 347 ++++++++++++++++++++++++++------------ 3 files changed, 237 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f802d907390439aa22f32034998f995aacacfcdd..af16a88596217e1aaf0c04ce3f169f1b5a9051e9 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 bae20f7537c7442e29eeb7013e223b8d7ce422b2..5ef371e3425ce9e85da94d956a05b92b4a308100 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 1a1d02cf1f2c55e78ee500993aa8f2621b49ffe0..1923ef7845f7d43e37253c019c67f600145dfdba 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 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 - { - 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; + + 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; + } + + 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; } - } - 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); + 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); + }) + })?; } - } 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; + } + } + + // 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)); } - edits } 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/115] 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 256ef2284cd6c3ffbb6f7e9746ddfb8e75a4bdfd..9e24e56efeaa915e766866f7c4427dfd2105ea30 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 b99977a60eb45dc3a0a616169067063e6c4e691f..4a2b03bbdf15716b16a8aa989c302d102b88d575 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 f70436abeb45f248ee8c306613a220655313797f..5917b8b3bdbe9f9f97e89f5567196b0e23226280 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 8417c411f243938661afdd2756b1621f60507fb8..d4061f25dc6a22b8a3abd79ae0471c811e2b08e0 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 ec5f9541f58fde77287e43436d1c9cce1f84bd54..eff95460c41beac3b5b096a65160765ce7f98fd3 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 223f5679aed05c8a5fee158f3a14959133c9151e..82245d67ca0487adb33aac8ec124f658948c3509 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 4e16ee3da2478817fd9d33369a71412b4958f64f..4b3c80c08a639217fd62be176335644969afe2ac 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 71a0b70b81b7a6f08a2584064a55a07743096b2c..bfa34c04228017e836afafae127a9d593b2d4fa5 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 acf9d46ad3e66ff992ce717d00fbc39bd85bbd1b..1defee70da3e33502b8247e20e75554ff0f19bbc 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 d226c704108b6bd036c08f1700cbb7812a3c1c51..50bc049a3aa96d37ae9acce6a1505369333bf534 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 85e6eab6927a8370ea9646fa43baa383bf13e8b0..d0bcad36c22a5e8775418d571c46d5bf7ab38883 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 19acb949e25c66be9890f9931f49a8e82f8bb972..60dd2335551bc746ec6b26f2afe039b377e18442 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/115] 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 ba9660c4ed1f9b2de49558fc919e521d3f2f16f1..05de4be8a3fbe3b7e321ad6e3d335fbf7a1917c2 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 077a421ce1b0e0375b56d2fc8cac48a601e03a65..164e095cee9a71f62531fc576c44f40a1c4e9260 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/115] 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 0f8699aff262381992555bb00532178bee3bc787..2cdd540e8a26fe55a4ed4a3947541e49f863d4c8 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/115] 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 511e7fddd7089263f60a767e6c7ba7e1f6733e44..52f31d2f568ce4bb0f1ef6f0695d0b3ed3bbe08d 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 0000000000000000000000000000000000000000..b70aa40b62532383fc5de1f500bf469a22807b82 --- /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 1923ef7845f7d43e37253c019c67f600145dfdba..5bd1b5dcca64742af0b162a1ca3c528fd062b0c2 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/115] Integrate the new diff algorithm into the modal assistant --- crates/ai/src/diff.rs | 72 +++++++---- crates/ai/src/refactor.rs | 262 ++++++++------------------------------ 2 files changed, 100 insertions(+), 234 deletions(-) diff --git a/crates/ai/src/diff.rs b/crates/ai/src/diff.rs index b70aa40b62532383fc5de1f500bf469a22807b82..5e73c94ff89545f5a2e8ba18ee12066bb96e13a6 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 5bd1b5dcca64742af0b162a1ca3c528fd062b0c2..dcec04deefd76a3553ee4fc884c371f31cf15708 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 new_text = String::new(); - let mut messages = response.await?; - - 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; + 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 = crate::diff::Diff::new(selected_text); + + 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?; } - } - 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 - } + if let Some(hunk) = diff.finish() { + hunks_tx.send((vec![hunk], String::new())).await?; + } - 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, - ); + 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); - }) - })?; + 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/115] :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 5e73c94ff89545f5a2e8ba18ee12066bb96e13a6..1b5b4cbd20f5da7b401ddde701cb22406d7dcfe5 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 dcec04deefd76a3553ee4fc884c371f31cf15708..87f7495fcf5b812b0e925d3703647cd167daf19c 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/115] 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 82245d67ca0487adb33aac8ec124f658948c3509..58732355a55992e2831389853e89b4e7bb205ff6 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 4b3c80c08a639217fd62be176335644969afe2ac..63b6786d8c51ec1046f91a2a25464ee0d1f5626a 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 b7e4ab4ba7b32491bbbb8aa025cab543dde113af..225cea0e92064c25600fad240b5d772eb9e76bc3 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 0290bf33342d9bb7e649301934fd7f5b724c90ba..9a32f69e43a52179996a94ed063ebfb18e65f68d 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 0a47d365b598aa41df1c1fa50aedd7d718aceb87..b7e4438e1f71855c269a45d795f9ddabbd578772 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 b57c6f5699498706de0b1c91655fb15848b1a2c6..48d7a3cf877602705ccf7037e23c1f98e0c16190 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/115] 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 2cdd540e8a26fe55a4ed4a3947541e49f863d4c8..42b62cdfa96aef5f2eaf25419a9e5a34bb8c66ec 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 0000000000000000000000000000000000000000..51db8b8ab8dac4b8fac0985bdbd08a8d2e4faa3c --- /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/115] 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 42b62cdfa96aef5f2eaf25419a9e5a34bb8c66ec..8aaa11e1cd9d13c0ae9a429cc0bc7f3e34f2e39f 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/115] 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 | 67 +++++++++++++++++++++++++++++++++++---- crates/ai/src/refactor.rs | 7 ++-- 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af16a88596217e1aaf0c04ce3f169f1b5a9051e9..3283d32a94e374e9625698b2e1fac4bec4d6b4f2 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 5ef371e3425ce9e85da94d956a05b92b4a308100..db8772bcb1ef59155b1fbf8a66ba4f79c9adc2f2 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 52f31d2f568ce4bb0f1ef6f0695d0b3ed3bbe08d..bad153879f8fcbb53c07dce713cc7484b68f83e2 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 1b5b4cbd20f5da7b401ddde701cb22406d7dcfe5..355748ea3dfcebe3433ac7388c42d2186dcfc2b2 100644 --- a/crates/ai/src/diff.rs +++ b/crates/ai/src/diff.rs @@ -204,14 +204,67 @@ impl Diff { #[cfg(test)] mod tests { + use std::env; + use super::*; + use rand::prelude::*; + + #[gpui::test(iterations = 100)] + fn test_random_diffs(mut rng: StdRng) { + let old_text_len = env::var("OLD_TEXT_LEN") + .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) + .unwrap_or(10); + let new_text_len = env::var("NEW_TEXT_LEN") + .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable")) + .unwrap_or(10); + + let old = util::RandomCharIter::new(&mut rng) + .take(old_text_len) + .collect::(); + 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); - #[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()); + 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 87f7495fcf5b812b0e925d3703647cd167daf19c..d68d8ce4eda207b34da6ffed9bbe0fc4f0564a4b 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/115] 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 355748ea3dfcebe3433ac7388c42d2186dcfc2b2..c68f9b3d5db9dfb67d39106a536adf99150ebde5 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/115] 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 c68f9b3d5db9dfb67d39106a536adf99150ebde5..b6e25e062441e2c92c2b8faeeaf3b9b8f8a34fc5 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 d68d8ce4eda207b34da6ffed9bbe0fc4f0564a4b..245e59a464361d71066f20bc1d02bc50f82283e3 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/115] 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 245e59a464361d71066f20bc1d02bc50f82283e3..7b2f5a248a9733b4fbe0e9141f5c2d0a591af868 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/115] 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 7b2f5a248a9733b4fbe0e9141f5c2d0a591af868..58cd36018650fd071425e962b4d96a67b395ede2 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 80e823632a9c10613abbf1f3f5effee615c1cd15..a42a8932416f3d6d6ef67fbfeba6a56e88d8d0ea 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 cfc1f8d813648654a8fee608ea4d00dc30893b75..88efabee1e8c58976d05acb5bd8d15af3f1c6f4a 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/115] 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 b6e25e062441e2c92c2b8faeeaf3b9b8f8a34fc5..0f4f328602065a20136f5618b34b62d70740710d 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/115] :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 58cd36018650fd071425e962b4d96a67b395ede2..aab056fd3280765fe6b970780548109f69cbf7e1 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/115] 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 aab056fd3280765fe6b970780548109f69cbf7e1..1eb54d9373801ab054660ebfc3536610dabaa19c 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 8417c411f243938661afdd2756b1621f60507fb8..28b31ef097e6a3fb7d423ea2f55626c3486834c6 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 0b10432a9f4747d93ff974ac72ddbbb6783fe676..69668a97c6200c2f6536be8f3a805b6552f1a353 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 7c94f25e1eb73c125e61effef56565cb9597bd85..e47e20da0dc0933be2ef7f42393497f4e25181ce 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/115] 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 0f4f328602065a20136f5618b34b62d70740710d..3ba0d005e77d5e7c4e5ff93da930f7502962ef48 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/115] 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 1eb54d9373801ab054660ebfc3536610dabaa19c..f668f8d5ac481620557442846ad0c56a3faecc83 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/115] 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 3ba0d005e77d5e7c4e5ff93da930f7502962ef48..378206497bf5687c702696fceb6ae8bb579e65ae 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/115] :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 f668f8d5ac481620557442846ad0c56a3faecc83..2821a1e845d82b5b67c153275332ccfbd5282001 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/115] 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 2821a1e845d82b5b67c153275332ccfbd5282001..9b36d760b77f2aed7fa83b92785e805423c1c6a1 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 2bfb090bb204fce393b7ac816e1000413228c056..9c764c468e493013935bf9aa6552065e3e145b62 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/115] 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 3283d32a94e374e9625698b2e1fac4bec4d6b4f2..2a4c6c4f4394a7c3e13dd00b2178640528b8d034 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 db8772bcb1ef59155b1fbf8a66ba4f79c9adc2f2..b03405bb93fa72f6079a6ca32e661e7d37bf704d 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 378206497bf5687c702696fceb6ae8bb579e65ae..7c5af34ff540a0721a956ce5ce30b48165e76031 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/115] 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 9b36d760b77f2aed7fa83b92785e805423c1c6a1..82bebeb336f528986575a30a726ae8e9bc804b8b 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 28b31ef097e6a3fb7d423ea2f55626c3486834c6..88c66d5200e4d74dacab8e2c3932826c76fbb69b 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 69668a97c6200c2f6536be8f3a805b6552f1a353..e2154f498eb9ed8a22fab83ab4b37c9b147c6edf 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 e47e20da0dc0933be2ef7f42393497f4e25181ce..8f15535ccf7aa56c749449fd417a9eda28fa0b5d 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/115] 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 82bebeb336f528986575a30a726ae8e9bc804b8b..1cb370dbba1ffd2b16b7b4ba52da21d3663a191b 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/115] 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 bad153879f8fcbb53c07dce713cc7484b68f83e2..48f490c9c0597a5e66261a6a77fcd268d808b768 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 1cb370dbba1ffd2b16b7b4ba52da21d3663a191b..5562cb46063cdb5018e4947dd0dc1ba9a3e821ee 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 0000000000000000000000000000000000000000..2203acc921c02ff6764fd84d7f1fda26c3179ee0 --- /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 7c5af34ff540a0721a956ce5ce30b48165e76031..1e5189d4d889b05872f9df50b3a2a83db57efcac 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 88efabee1e8c58976d05acb5bd8d15af3f1c6f4a..a02d7eb40c39d76be2dbe175022f67b0c88c71a4 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/115] 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 1e5189d4d889b05872f9df50b3a2a83db57efcac..5425b75bbe8a62403dd490e2aa6d4a7ee119852c 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/115] 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 2203acc921c02ff6764fd84d7f1fda26c3179ee0..675e0fae99dc593f6d7dedf9b9090e0a7cabcf27 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 a42a8932416f3d6d6ef67fbfeba6a56e88d8d0ea..ebc959123933f9bb3f6e3ef8b7f6a9c1634f1c89 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 a02d7eb40c39d76be2dbe175022f67b0c88c71a4..ac91d1118de351d5dfda0ec7b3062d053cca3c5e 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/115] 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 0000000000000000000000000000000000000000..59b4a5a83950d40725fc3e9fd54ecf1e4674e2f8 --- /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/115] WIP --- todo.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/todo.md b/todo.md index 59b4a5a83950d40725fc3e9fd54ecf1e4674e2f8..e07d19bc95f8c8e606b1da9aa41af754458e35fb 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/115] 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 2a4c6c4f4394a7c3e13dd00b2178640528b8d034..e74068b2d2a0085dcea482788beac5de90fe2f93 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 3ec994335e3ce0d67c8ed115ae86ce5b60abe1ac..a81c18f7082b884c74de65d4be79fb12f236916a 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 b03405bb93fa72f6079a6ca32e661e7d37bf704d..4438f88108988e715d476a65fc2566d8a5f8e090 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 48f490c9c0597a5e66261a6a77fcd268d808b768..0b56fedb110ae5696c2a4525f9e8405b9f9b9350 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 f134eeeeb69ab1cfc291ffa29e0679436cff7219..58be0fe5849e2c5bf6c3b70465a1d591e70166af 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 5562cb46063cdb5018e4947dd0dc1ba9a3e821ee..0000000000000000000000000000000000000000 --- 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 675e0fae99dc593f6d7dedf9b9090e0a7cabcf27..0000000000000000000000000000000000000000 --- 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 5425b75bbe8a62403dd490e2aa6d4a7ee119852c..7ea7f6dacdb813e22df1a33fbc8ec48a54cb8e3e 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 904e77c9f0fdd3dccb84758b97e0005573dcaee2..0283b396f3f8f29d846f63a907b5554008d2cb8f 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 88c66d5200e4d74dacab8e2c3932826c76fbb69b..0990fdbcb75b41a20d606e6a67642d1c540c56c3 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 e2154f498eb9ed8a22fab83ab4b37c9b147c6edf..e8bbe29b47f75fb872fe6ba47d281c275c490cb3 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 8f15535ccf7aa56c749449fd417a9eda28fa0b5d..02f1be718f132b911b0b84312f3893d549e43e10 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 ebc959123933f9bb3f6e3ef8b7f6a9c1634f1c89..02d0de490513f68379a941fd6575c2c92643088f 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 ac91d1118de351d5dfda0ec7b3062d053cca3c5e..97bb3402b6b4600ad72ffbeaed049f431043fbfe 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/115] Allow generating code without editing it --- crates/ai/src/assistant.rs | 155 +++++++++++++++++++----------- crates/editor/src/multi_buffer.rs | 10 ++ 2 files changed, 110 insertions(+), 55 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 58be0fe5849e2c5bf6c3b70465a1d591e70166af..4c75506b7a945e8718ba35c651429876eb03b8e4 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 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; + } + } - 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); + 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 0990fdbcb75b41a20d606e6a67642d1c540c56c3..ac3b726b26817cefe6cf6b9750cec53b51b5b524 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/115] 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 4c75506b7a945e8718ba35c651429876eb03b8e4..e68d3ef7b8aaaf4d057ee946153a3050c371829f 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/115] 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 e68d3ef7b8aaaf4d057ee946153a3050c371829f..9311a00751b0732779a900171c4406b0a6991ec4 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/115] :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 9311a00751b0732779a900171c4406b0a6991ec4..b8458bb9ac84a6cdd32ea5a24820de1250759c1b 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/115] 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 cfe3da22b58374b033328bb5c0f7f667d4fb540f..4405712afe8d0300df5b4808dd2fdffb28fa8ec5 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/115] 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 4405712afe8d0300df5b4808dd2fdffb28fa8ec5..a1de7bf73624d16d2185f39f06fdc19527480ea8 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/115] 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 a1de7bf73624d16d2185f39f06fdc19527480ea8..326dbfc0461a422c9238a13c77bbcc6dd0faee7c 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/115] 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 326dbfc0461a422c9238a13c77bbcc6dd0faee7c..65b255d458cb6a871614547642b7fb7e61f80607 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/115] 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 97bb3402b6b4600ad72ffbeaed049f431043fbfe..bdca8a16e562e6e4ab755eb66813a63c5bde7ba8 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 e07d19bc95f8c8e606b1da9aa41af754458e35fb..8a9a8b5b3db9b82a10879e00af76db3f4409983b 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/115] 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 9e24e56efeaa915e766866f7c4427dfd2105ea30..bfa804c56ce9ff4515f91e3a1d413227987af258 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 5917b8b3bdbe9f9f97e89f5567196b0e23226280..d55b2a464f7c2f6ab5894dcaea69f39ab5389304 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 d4061f25dc6a22b8a3abd79ae0471c811e2b08e0..99dcbd189c8ec87baba7e64139bee1c72352978d 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 83aaa3b703491b5f5334d173ca8d90e731aeb5e8..a2a7d71dce7d6796d5c9c079d2f71a586c3deb26 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 eff95460c41beac3b5b096a65160765ce7f98fd3..44ee87079783b72376e4ba77f89f781aad894b34 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 58732355a55992e2831389853e89b4e7bb205ff6..ccc19370326d977fa11d6856f63b83fc44456470 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 85ae0885652d9054356ca19b9be6d5fd8642d16d..b925bc8f0d5ef290993fa0d49adcf221dd3570f6 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 bfa34c04228017e836afafae127a9d593b2d4fa5..c0ba5a609e789e456778eb9403482d68c75678e2 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 1defee70da3e33502b8247e20e75554ff0f19bbc..243653680b13f263d747dee585cc81f55f33b50f 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 50bc049a3aa96d37ae9acce6a1505369333bf534..1c9aa48951e6ffb5fe46035b6dfc690a501c8739 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 d0bcad36c22a5e8775418d571c46d5bf7ab38883..a6cc91d7bde94881ce7c91175c395699c4ccac1c 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 c23ddcd6e7957dfb6444fe9031482ff236f5b46e..7f6c6931e4a25f6e500c1d1872e10c617d9d9e06 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/115] 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 65b255d458cb6a871614547642b7fb7e61f80607..b99d4b4fac7b14f0db950a350ca7424179f85eaf 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 8a9a8b5b3db9b82a10879e00af76db3f4409983b..71ca5a7c7b2b937d2b83e4600a988be37e9d0208 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/115] Allow multiple inline assistant highlights at once --- crates/ai/src/assistant.rs | 218 +++++++++++++++++++++++------------- crates/editor/src/editor.rs | 1 + todo.md | 4 +- 3 files changed, 144 insertions(+), 79 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index b99d4b4fac7b14f0db950a350ca7424179f85eaf..c5bf027fcc928beaecefc0952b63239fb039108b 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(); - - 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, - ); + .ok_or_else(|| anyhow!("editor was dropped"))?; - buffer.end_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; + }; - 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) - }); - } - } + 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.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) }); - } - editor.highlight_text::( - highlights, - gpui::fonts::HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - 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) + }); + } + } + }); + + 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 b206f2ec8b4496e6935f1a2453c3dbce5ecf83ef..d5141a7f7df8d525e54394134a9c0fd4bc4cd178 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 71ca5a7c7b2b937d2b83e4600a988be37e9d0208..050770850274bfd5622d2948d4ac7ef62ea651a2 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/115] 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 c5bf027fcc928beaecefc0952b63239fb039108b..f7abcdf748ab6d6a8fc1a7a5bcf2d923280de4b8 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 d5141a7f7df8d525e54394134a9c0fd4bc4cd178..75fb6006c0d4c102ca6bc713599296d006f5a9d3 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 bc0c98bac70960d48912956ccd2c0a3c5c96ef07..7913685b7a53f19987507f4fe15fc47da4fb4649 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 bdca8a16e562e6e4ab755eb66813a63c5bde7ba8..8bef2ce16b4fb82f8cc696912a8f4a17cb3b5793 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/115] 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 f7abcdf748ab6d6a8fc1a7a5bcf2d923280de4b8..0333a723e9b832ddede28d98a9bbbd26db99904c 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(); - - 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'); - } - - // 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); - } - } + let mut new_text = indentation_text + .repeat(indentation_len.saturating_sub(selection_start.column) as usize); - 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), - ); - } + 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); + } - 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/115] 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 0333a723e9b832ddede28d98a9bbbd26db99904c..7803a89ea90a28c56ca1bf31d49e3d98eef0718d 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(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); + } - while let Some(message) = chunks.next().await { - let mut lines = message.split('\n'); + 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/115] 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 c232afa08176ec6251b2263db27f3dd167f237d7..84b8093be2425c1460db7325ca3990e9a49e6078 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 7803a89ea90a28c56ca1bf31d49e3d98eef0718d..193cba8db47e4e11839032c52b5f5510fc2d9d85 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 6953ac0e028262fef96ddb71a6897946a81de90e..1f8ec4e92b973e0b94c99fe8259aad2811bf7c8f 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 3055399c139ddff1c469d58c4be979943c77b345..a7734deac5bd7f36a483856491d36078e1552920 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 de05c259c81d7c63ff856ab93d4162a34f992511..6421818b6283cd3b812005db741259b0161d7040 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/115] 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 7d6587795ee8c895ffe80ff68832c273754a3728..da5a8e6d72488b24394a8f93d3d0be84ab3f8160 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/115] 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 3b444082c67a491345b144b943fee9c9db74e25d..4dad12ad08580fe3a37278a228a30fcd14c8b5ed 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 6396536b83f8976548e057ed1a0925d716903d60..fde280f8fec493c56f1f712ee536967e49cd857a 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/115] 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 050770850274bfd5622d2948d4ac7ef62ea651a2..0000000000000000000000000000000000000000 --- 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/115] 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 4dad12ad08580fe3a37278a228a30fcd14c8b5ed..952c924292d55e6edae84bf3dfd9391c5d6ccae9 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 c7c40289959149bfa5bf1dff7bed4e2d17356215..0c499c16c40e00d02e5380499f1e91baf3764ad3 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 2ed99d85266d8fa6558d148ab7378f72b66404c0..4310f84830b9d0c9438b7a5612159c35098fafa1 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 43bcef0825d8f45820d0724bce8025fd8423ef1f..6a00ea12dba32f6272f1780b6a536fb139cde846 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/115] :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 952c924292d55e6edae84bf3dfd9391c5d6ccae9..80c3771085a46199c2b6ed4b4eacf77eace31408 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/115] 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 bfa804c56ce9ff4515f91e3a1d413227987af258..a188a47e350393b54dfb92daddb0470dbe505892 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 ccc19370326d977fa11d6856f63b83fc44456470..a2d02ecd96b3059c5c2820d696e684f67cfba8ae 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 47aa2b739c3773fe701552a0ac17477c15ee963b..ad5a68f8dd722348d71ddba48bef0ef26b292cf2 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 51db8b8ab8dac4b8fac0985bdbd08a8d2e4faa3c..f2103050f3cc1fcb3306cc60fe1dff97f6535907 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 c32927e15cfc177fe8c4611f8b5de686ebaffed6..b166feda766ba547250adf89cdbbc0bb73ed080c 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 d7982f7bdb471d6b0a154d60094f6e206618f0b1..19b7013709420774e3533b346877df79f21d9c7b 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 ecc839fca6875e54f0a0bf4d0671c675123418ef..cfb6a5dde92469299fdd87cbd40de6b7d7479227 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 225cea0e92064c25600fad240b5d772eb9e76bc3..f7e8f874925265f2b61ce47f1aa5d59cc218c596 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 7c5c7179d019ee9c8c90e069fec55f297ceda2d5..ee6d0f8579a2063e8cb13e7d026062c9ff3e1ab1 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 6a01d00300997862532178f24cc6ad518c2c093b..73bb4b019cc04bca9dc4566dbefb72ce2a39caac 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 41ad28ba862e38e04b52ec5e4e1b77e87b183200..023bbab13f65e3ec84016fc4978aac3a2a9fd40c 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 358441352a7090084c46e2f6fd1d06b2aed8c68c..3890b90dbd7a48e99713d05ba391114004cf7b7b 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 3c7f84fec7dced7f8241ff7009160b0d748191f4..bf8ad00293f3a93f62a6e50793db717952bb8fbf 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 8416859f5a6093e091b5afb9d306d5c9ba979ac6..35665e864fadd2f481f5213af205f36a420c06c1 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 9a32f69e43a52179996a94ed063ebfb18e65f68d..d7c11f0c7395fef1f7916f12717b501eb1f1628b 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 b7e4438e1f71855c269a45d795f9ddabbd578772..e3bb0aae95150e1c398130a40e89e4cce97b33be 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 48d7a3cf877602705ccf7037e23c1f98e0c16190..21155cc2316c727e134f7d763ee785425e4fceb8 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/115] 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 8aaa11e1cd9d13c0ae9a429cc0bc7f3e34f2e39f..f0b8a1444acfb9a288a0952b83d017df0f70203e 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 d7c11f0c7395fef1f7916f12717b501eb1f1628b..1b7c271d10584983244c2f14800bffee241c67eb 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 234dc6b01326a8398b5ab5dfbdfec93a37d910ad..2f676f67107dad1e69e403288d1d3ef43fe7bfb7 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/115] 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 80c3771085a46199c2b6ed4b4eacf77eace31408..ae223fdb57516752e137133f81481f42234f6dfa 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 7913685b7a53f19987507f4fe15fc47da4fb4649..261933f057a354fd7996f14eef08cf40b3e55186 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 8bef2ce16b4fb82f8cc696912a8f4a17cb3b5793..e660bf078f299fbdb6926e70a35b4eef0cb60a3d 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/115] 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 7ea7f6dacdb813e22df1a33fbc8ec48a54cb8e3e..7399a7b4faf2629310bbf9e7ec573a651e52feaf 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/115] 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 ae223fdb57516752e137133f81481f42234f6dfa..62ff7212bf8f969337ec1519bb49e93aa2e3b351 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 e660bf078f299fbdb6926e70a35b4eef0cb60a3d..4a33ef9b19cffac5d8899c2aefd53b570e94cefa 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/115] Remove the ability to reply to specific message in assistant --- crates/ai/src/assistant.rs | 237 ++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 132 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 62ff7212bf8f969337ec1519bb49e93aa2e3b351..ab60d108f0491abe42b5b298dd9751e00d2b9af9 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() - }); - } + should_assist = true; + } + } - 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, - }; + if should_assist { + let Some(api_key) = self.api_key.borrow().clone() else { + return Default::default(); + }; - 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); - } + 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, + }; - 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(()) + 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); }); - } - smol::future::yield_now().await; - } + cx.emit(ConversationEvent::StreamedCompletion); - 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 + Some(()) }); - this.summarize(cx); - }); + } + smol::future::yield_now().await; + } - anyhow::Ok(()) - }; + 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); + }); - 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(), - ); - } + 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(); } - }); - } + cx.notify(); + } + }); } - })); - } - } + } + }); - if !tasks.is_empty() { 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/115] 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 a188a47e350393b54dfb92daddb0470dbe505892..ab2be13a25366643b9904b67407fdc9af514fd34 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 4766f636f37bbef734e364eaff95e0fb63e152ac..d692660738e3c446ab5416fdb723bfc07f3737a3 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 9ad008f38d4dd928af5b49c46b148df575b1c6a3..0b99b6fba6e15dc3afd127375e5a58b702ad8e40 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/115] 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 da5a8e6d72488b24394a8f93d3d0be84ab3f8160..de4e8828b39adf9db635700f2d280faf1aa6b677 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/115] 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 ab60d108f0491abe42b5b298dd9751e00d2b9af9..4aca6ae626990f94f5001ed518fceb72b33c0dfe 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/115] 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 4aca6ae626990f94f5001ed518fceb72b33c0dfe..46756ad5693b9b7a3928b4600c9f2892471d256a 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/115] 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 24ea4774aa5deda8c45154d7b8bf7c4f30a4709c..fa5a3af0c69717de9d5eb9d76374914644836479 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 fe8e4e338c6831e381ea788a68dd7315140529b0..8a432a6e4fec211b14ea120221779363fd7043a7 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/115] 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 63b6786d8c51ec1046f91a2a25464ee0d1f5626a..800e0ae01cebdb89cdf65118fd1b2a0a95a48eef 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/115] 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 0000000000000000000000000000000000000000..32fd4ab1b589e01bb61c7cc7bb65b2c59af0109e --- /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 43a6cec58537ebf3cefd6eb3ee3e5c44d0c2066d..7feccfc8bc589b2cee5454f3a9ee7174f4d149a2 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/115] 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 935909afdbb714dda754f003a01ed264f1f9721c..dda6be83b2099f87b2368b94ff1c5ec71baadd4a 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 07f367c8afe077fd3d0bbca3ab424ecd07006078..c4ffee889cbcc70fc95d17c070fece7e73ff31ba 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 0a0b69e5965900b296098ebe173ebfe70a4c3499..0565982eea8333c62bbf60a9a698f7c84bdcfe1c 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 32fd4ab1b589e01bb61c7cc7bb65b2c59af0109e..b3f323eb9e34c34c4a4f1d565eee75207807685e 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 7feccfc8bc589b2cee5454f3a9ee7174f4d149a2..c24bc3f770db0089c2b2fcbac6146bd7e47da5ca 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/115] 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 17575663a1f88b17870b1b146b47e7086bf3e2ba..4d262f8146ed907035e384dbefd28c0b838a467a 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 fd6aa18ced50af53b6bcf4c3c386d1774c7ab00d..3c52b6d989640a93025ac5eafe5a959b8bc83163 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 3b554ae37aa7db3e7e634d447cd14afe2d6f3a5c..e0e831b0826d4b3f44c006a746424eb72adae7e8 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 dda6be83b2099f87b2368b94ff1c5ec71baadd4a..38729b044cc901e3da2b4d795addd8e2ed6ab0f2 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 81a3b40da7b3115dc28aad472501e1347cdc6e0f..b3d2105f6ae0aa4b0f0d52075b6c2dbcf298576b 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 cadfcc8d4aba63a5a7d0a342e8b904df6f1c5645..5921210f88795ea7945a330de5a0818d05de8275 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 f6262405f0b150b06085a6e0b639b405991fe6f0..5bbdd646a8ef0c932c345bac9c8420dec6a6f9ef 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 96792bf7661263d39e058310c836c2e34aae5378..b94e2639222de7e11cb127706807afc36d275369 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 9938fb93113ba993cc13cfca76569aad518bfe02..6f73b6f3fbf8e45fa834e76e101002cb55512cf6 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 0c43e7010e5469c10f959e00f4df8d177963392f..9e7f9acfc314be75850128690d8d066dde520182 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, - }, + }, + 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: { - 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 ead017a80324d1600c2bb32600216ed7879d5285..b73d20ff1a713fbdc4b8c69c05e3474ddd048555 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 cfc1f8d813648654a8fee608ea4d00dc30893b75..7df5434f91010f778d47dfc9170ef80b909df0cf 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 0f50e01a390912a607d8d916906da9cc2857f80e..f9b22b686799d450a0d0abebd0df6243982dc96f 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 c4ffee889cbcc70fc95d17c070fece7e73ff31ba..4d605d118c370569b7fed3247aa35e781d65df4a 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 e2bb0915c172d946bfd882aa768f3db54e82cb63..71057c67eacb7623db1bd98fd9a52da5aea453c9 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 0e76bbb38a702cdc44542d305bc819ca87e9ff6a..dcd84c3252de0406011456da8c58a98c6434511a 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 9277a2e7a1966c7d4eeac6cc7d3129bd52244141..5a662098e823fabf5567960b2cbed43641189219 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 b1bd96e165466521804f86d547664c209c7c4671..0349359533041d4425df3d0c72d120ed18499a30 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 28ae85478794a22211dc67cb6ec56cce4bc805f1..317f600b1e2fca3f6b8756c3ba033fa22e6b0220 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 e239f9a84023088d988f74e709c6485ade8a9510..51958af145b50afd25510788ff0a530cc77a5a1d 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 c37a4e4b9a1bedec7c159c6f729dab0e9a882362..3afa1932a45debcf572b54022368a255370bbbdc 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 2d3b81f7c2fa42d779394a50a8ae4c0b79a09c1a..3b5ebf2c88d9d67375e8c3214788544689c7b997 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 129bd178690b1e219b50c972de2504b42e83d933..23ff03a6a317bbffddf340576b9a1fd46a1e9a4e 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 0565982eea8333c62bbf60a9a698f7c84bdcfe1c..e4e274684ce714394733223089f19f958fe0d927 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 ab3c96f28072ce726f0b1eea1468bb4941686c5d..61471616fbada3e948833a017cc76bb1ae14ab6e 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 f759bc813910416d88a2097134dc95654d6ab3b3..e2c3bb33d32b88ae7475696d621a1b647b35f643 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 c7eaa50eedd05d201a8eb4d99a562c418f9cdb08..940442e1b74a40f953bdde0ce00e5cf225f38480 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/115] 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 38729b044cc901e3da2b4d795addd8e2ed6ab0f2..b20c81df6857d7f67848e2a45fb27a6802d70a9c 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 e4e274684ce714394733223089f19f958fe0d927..9fb439f618e7bdf9e729fc2449d4bff95057ecd8 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/115] 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 b20c81df6857d7f67848e2a45fb27a6802d70a9c..5b7c61b17c040c9dade28e1f4516e9ea9c3226ae 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 3afa1932a45debcf572b54022368a255370bbbdc..a93aab4ea86ea660d4c33802abdc466e3ad76c7a 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 b3f323eb9e34c34c4a4f1d565eee75207807685e..39f11f58bf5613f8d60e7e679abe3da4ab034db4 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/115] 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 615e2386481ff34543b3940eedbdb815164fa4dc..41985edb75ce10785d0eb73e29ae1bf71bd8f480 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 a5faba8eaf2016af7875665b679aa96de2518674..423bb879d24f5477bb043af9ea412be75ecadcc0 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 39f11f58bf5613f8d60e7e679abe3da4ab034db4..7292a220a838ae441e6520e806408f6b3ad69d59 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 c24bc3f770db0089c2b2fcbac6146bd7e47da5ca..ba89c7b05ffe12e617c0570ef458c18f6a99d286 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/115] 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 684ddca08de742823dd0d3dac54bd4a89b256b70..bd94c85f959c55e80e5bec87504f7d40bad36da1 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 0000000000000000000000000000000000000000..f2cbc7b26a01f54ca2eef095ef27b290c8a1ffa6 --- /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 b73d20ff1a713fbdc4b8c69c05e3474ddd048555..8333d9e81a9188b0752f0fde433c9346e7b6e341 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 d41b4e2cc3bd429993b490203a7278a74eaad258..0586399fb1c4a0ba3aba32816620c56bdbfbba7b 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 9fb439f618e7bdf9e729fc2449d4bff95057ecd8..672907b22cc9d4359645494bea9dfc326054eb6e 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 ca8aaa461fa12204b35596f34056c02c52d740e4..47110940f550b1f8491d541ac800f7447bc4ec17 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/115] 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 423bb879d24f5477bb043af9ea412be75ecadcc0..8574357777af624a8a10c5106a560ad18e60403d 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/115] 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 800e0ae01cebdb89cdf65118fd1b2a0a95a48eef..c672a37cadbd2f7618f48d08fc3e1cde4cdf1534 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 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; + 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, + )); + } + }); - if let Ok(new_completions) = new_completions { + let mut completions = Vec::new(); + 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/115] Show inline assistant errors --- crates/ai/src/assistant.rs | 158 +++++++++++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 34 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 46756ad5693b9b7a3928b4600c9f2892471d256a..d19730172af752c0d4adbe1cdffe88288ba54798 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/115] 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 d19730172af752c0d4adbe1cdffe88288ba54798..fd6e6c63eb0b6e110883a8bf9905aa34d135e882 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/115] 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 fd6e6c63eb0b6e110883a8bf9905aa34d135e882..7b360534ec11d804001c2ba27e12f10bed72d50d 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/115] 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 7b360534ec11d804001c2ba27e12f10bed72d50d..2aaf75ae3979fc6b46f98e4c9f3b646ef3170093 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/115] 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 bd94c85f959c55e80e5bec87504f7d40bad36da1..95b98689374250c824b3c7e84d16aac45d7f6a11 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/115] 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 85ba2e1f37724edc819c58bbdf3009ca491f760d..b48fe346316e01bf1b1a2d79dfd06a1c58ec4b3a 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 82b9401d08dc8d682fcbbfda15795f6ec3d3de2e..593629beee2510012638795583327b7877938ba9 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 6b3d0fd41e979c0704a8f04502c16cfc58c9cb2f..e581def0d050727647ccaf06a53406b389d57e71 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 3b5ebf2c88d9d67375e8c3214788544689c7b997..b279bbac14c402404c7752eec0edeacaa05ce158 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/115] 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 f39d97aeb56ae2ceaa919a59d6874a31e9328351..6a9e48b481574d185464bfbe9d8dd96b6afee837 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/115] 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 e87ff3d9910515513111c5dc9ab7dde5dc2e40de..d744f92ce85a82848660a4c95f4eb30a52b1105c 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 2a977646470507565dbea2b5ac847d2546f16845..66d55b38f0189087624749f313202088df061fc8 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/115] 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 d744f92ce85a82848660a4c95f4eb30a52b1105c..ca00e694167e8e09cb61d5a7cdcf995e607d7614 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 8adc38615c3cab87d36a348323e0c3674f555d6c..914e3f2dfbda3dc128433776fca9cd01ee32225b 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/115] 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 ca00e694167e8e09cb61d5a7cdcf995e607d7614..91a8a12eacce67d1d282a256e4311a0f23c9265a 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 f64a82e32e938a573721ceecffbeb28e23604fda..2613d01131df9d9a065f2ee331bd456b7d452029 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 814f248b6dc722fa67f2af2dd70c66f54af3a57a..e48753ed41424622a71498f2445e9a1d20d06384 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 427134894f3a7383febb571357bf39083e9b06cc..499ae2e808cec3c3f7ac39ca6cbc57c8b2750b3f 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 b3ebd224b08a21d47f5636b6fb1da8357b65121c..7584dec21a9d006c65bacfb9cb76be52fd153613 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 ec8a249ff4c4626e420f072a148b268831e61ba7..ecaee4534e4b4d4ae11391c688d44caa1bdd0fa0 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 4310f84830b9d0c9438b7a5612159c35098fafa1..8adf6f642157e98ee00c9280e46b09ef7c92a8da 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 db3749aa251517c690c49d25167a640534941a21..3bedf5b7a8e7770f69e381a3bc107dc4469dde8d 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 7a9e6b83ceb48584211792239fe2a802ec2e886f..89d05926270fef0a31c24d96a98eb7c01fdbce44 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 09c5ec7fc3214290b39eeb3585455839a883dbde..cf5465a601a65a1e7e32bc52c3b8ff07cb8c4aa6 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 8239cf869067043d25549c5cfa74337e5211271d..6b10ed26c1d9d3bbfe9041c285e3bf090f3caaea 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 7c5983a0a90de924384c37871720246dce5f6983..397223c4bb520e47ed6bb99da84d52340a7f5e45 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 e6e0f37cc74b317c5d9de9adda90f9820c230777..2de36710333c70aa0c0554a2b829181a5cb52da7 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 adec2d4fd06d5709f563f8ea67bd7504301ec906..65e9b6fcec23deeeda1364afcd4d8931a6984bc7 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 6a00ea12dba32f6272f1780b6a536fb139cde846..2fabb0f87f3e540c872f7e919e53aed881bd045d 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/115] 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 62f4c8c8065e8eb24ef24e7b1c98e75168034f43..90fe6ccc52338fe5eb5df34b72b180852b39470b 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/115] 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 0593bfcb1f279be0ce9fd7fed4dd2672d1813cc4..79e33c5048e4be75daa615a4596a3a60d8aae0b4 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/115] 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 79e33c5048e4be75daa615a4596a3a60d8aae0b4..daaa4839757943317bff182778d5c0cf3345b318 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/115] 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 2613d01131df9d9a065f2ee331bd456b7d452029..8121b0ac91d5021e2830236c1694a24a20cff3b5 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/115] 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 a34f10b2db29f3f132e823fce27209d0a24a12c6..5086cc8b37739aa74710d84c7f90e4040bc02f15 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/115] 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 51bdb4c5cece790604a31d962fc7e2cef0297f98..60c4e41666bc4a05e5a1f08bb9ae74f80984c702 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 d49dafff2f99fd1c132c01349a363096cd63183a..2abe0baefac35e0fced6911d525692ccea601c39 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/115] 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 7c5c7179d019ee9c8c90e069fec55f297ceda2d5..45d520df27a4834c8c0fe5c2fd1322fb27eca7bd 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/115] 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 a2d02ecd96b3059c5c2820d696e684f67cfba8ae..bb83beeeeae37938f7203b3792e56d3661559f70 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 c672a37cadbd2f7618f48d08fc3e1cde4cdf1534..597deacd1a1aae6392b593bea3d158c0635cef13 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 7f6c6931e4a25f6e500c1d1872e10c617d9d9e06..6f7ce49d3da40bc709d422abdad3260b8ad42da5 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 2f676f67107dad1e69e403288d1d3ef43fe7bfb7..3bdc638f731902dda01f74b39eef3f669bc749c6 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/115] 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 7c8d7f01daeb37d3df9b8bcf30c27320adf9593f..1b2d8ce419a34bd4caa7e07299699743d81bf541 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/115] 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 2abe0baefac35e0fced6911d525692ccea601c39..51f48a66a023148459ea11e563e6312b4c8b775a 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/115] 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 f4bf4506e45f73abe793cd61005481c8cfd951cc..a185542c63d0226108eca3c22e769bfa10f37770 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 1b7c271d10584983244c2f14800bffee241c67eb..12a0a4e3b8f711f8fe05d7358030911466926f5f 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/115] 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 a3688215db84f432982cad444b8e93f91797d423..d44b8728fd60433a0ab2ddabbb8c1cbc7756f76a 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 397223c4bb520e47ed6bb99da84d52340a7f5e45..b6adb371e10d0cc5129df4e043a918e9325a329c 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/115] 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 edada5c6307024db5566619c411e9d4212a1e537..091e1986f65b56892a4107508df2361768cb8aa7 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/115] 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 d44b8728fd60433a0ab2ddabbb8c1cbc7756f76a..ad97639d0ba6158326c2d3f2331c4f32efc3d56a 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/115] Route completion requests through remote protocol, if needed --- crates/project/src/project.rs | 127 +++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 091e1986f65b56892a4107508df2361768cb8aa7..5cd13b8be8aca39d73741849d10376e08baf21c9 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 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 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 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 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); + let mut completions = Vec::new(); + for task in tasks { + if let Ok(new_completions) = task.await { + completions.extend_from_slice(&new_completions); + } } - } - Ok(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/115] 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 b0719363929232aa47721624a63a9b8bb4aacb5d..b2405d8bb83e58aa9c4e33ca6749b397ddc38191 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/115] 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 2aaf75ae3979fc6b46f98e4c9f3b646ef3170093..5cde99af823cff78aeec4310b85f313d6b3884a7 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/115] 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 bc936fcb9974ad84a5a221f5cec78e239e78cb29..3cbf8d659432287f4712e1f5f75e6bcd04cd74d6 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 354accc01a237057a6ea3bbeb9b4c1986b4ea391..f120aea1c58a1745484d9e2eed674b20c8b091f3 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 bd7c3e9ffd62dea8b0d283fb1c6e1c26e8958d2b..5cb1ef6ea39c6dbca8bb58131f36428580a0aa9d 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 1ded955cd7f2283535f9795e5a0e98d1634fb0c3..38b2842c127f2a6ade0d43787a24a0c76ff13374 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 c88abc08ac20dc0a88bf61f537f6a39351632dad..80eb972f421197ea5dc80c39d06f203c530b707b 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 5e96ea043cc251775012b763855dfb0daf3c41db..61c25f8f84db0ccfbcd89c9d8ea77d0ec9261217 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 65e9b6fcec23deeeda1364afcd4d8931a6984bc7..d1bc6cc8f8e6826c0f50f3f2fa15b28fe3ca8161 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 b5f4fb24ec66da36416d2ccf5b05fc192ee51ee6..084be0e336e5ddbdaa2e5bd683aac763e5ca1735 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 2fabb0f87f3e540c872f7e919e53aed881bd045d..c05ea1109c0665247e4b8e09e657658f52441bc6 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 ff1b241e73b40bfdde1f96fffadf73014cd8ef60..f95809c02e247293ca05250f12d671572aebb1c9 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/115] 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 61c25f8f84db0ccfbcd89c9d8ea77d0ec9261217..92c85677f6919aff06daef30b57e6482949d021f 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 bc9dd6f80ba039bb705e3d1518c737ba56c969b9..d64cbae92993ec2b092fcebdcf48d20f2c7449d6 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/115] 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 5cde99af823cff78aeec4310b85f313d6b3884a7..9b384252fc0dfea3fe7897c1152fbca18fbcd9e0 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 From 6d7949654bdcfed2cb60c6f3faa8c0850edc527c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 1 Sep 2023 11:14:27 -0600 Subject: [PATCH 115/115] Fix accidental visual selection on scroll As part of this fix partial page distance calculations to more closely match vim. --- crates/editor/src/editor.rs | 2 +- crates/editor/src/scroll/scroll_amount.rs | 2 +- crates/vim/src/normal/scroll.rs | 62 +++++++++++++++++++++-- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2ea2ec74535402c2d5c69c68ea4673638b1387db..d331b0a268eaeb57bb2b02eb7f7aab584bbda4b9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1654,7 +1654,7 @@ impl Editor { .excerpt_containing(self.selections.newest_anchor().head(), cx) } - fn style(&self, cx: &AppContext) -> EditorStyle { + pub fn style(&self, cx: &AppContext) -> EditorStyle { build_style( settings::get::(cx), self.get_field_editor_theme.as_deref(), diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index f9d09adcf50bfbadb67bd379a888aaaff7ce3bce..cadf37b31d9a2526e5a0771fa555e0deb4957494 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -39,7 +39,7 @@ impl ScrollAmount { .visible_line_count() // subtract one to leave an anchor line // round towards zero (so page-up and page-down are symmetric) - .map(|l| ((l - 1.) * count).trunc()) + .map(|l| (l * count).trunc() - count.signum()) .unwrap_or(0.), } } diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index a2bbab0478e304f59919663f61f828d77cf4906e..1b3dcee6adc4827d42bd6d71c298d49fa991fda9 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -67,7 +67,8 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex let top_anchor = editor.scroll_manager.anchor().anchor; editor.change_selections(None, cx, |s| { - s.move_heads_with(|map, head, goal| { + s.move_with(|map, selection| { + let head = selection.head(); let top = top_anchor.to_display_point(map); let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32; let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1; @@ -79,7 +80,11 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex } else { head }; - (new_head, goal) + if selection.is_empty() { + selection.collapse_to(new_head, selection.goal) + } else { + selection.set_head(new_head, selection.goal) + }; }) }); } @@ -90,12 +95,35 @@ mod test { use crate::{state::Mode, test::VimTestContext}; use gpui::geometry::vector::vec2f; use indoc::indoc; + use language::Point; #[gpui::test] async fn test_scroll(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.set_state(indoc! {"ˇa\nb\nc\nd\ne\n"}, Mode::Normal); + let window = cx.window; + let line_height = + cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + window.simulate_resize(vec2f(1000., 8.0 * line_height - 1.0), &mut cx); + + cx.set_state( + indoc!( + "ˇone + two + three + four + five + six + seven + eight + nine + ten + eleven + twelve + " + ), + Mode::Normal, + ); cx.update_editor(|editor, cx| { assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)) @@ -112,5 +140,33 @@ mod test { cx.update_editor(|editor, cx| { assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.)) }); + + // does not select in normal mode + cx.simulate_keystrokes(["g", "g"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)) + }); + cx.simulate_keystrokes(["ctrl-d"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0)); + assert_eq!( + editor.selections.newest(cx).range(), + Point::new(5, 0)..Point::new(5, 0) + ) + }); + + // does select in visual mode + cx.simulate_keystrokes(["g", "g"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)) + }); + cx.simulate_keystrokes(["v", "ctrl-d"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0)); + assert_eq!( + editor.selections.newest(cx).range(), + Point::new(0, 0)..Point::new(5, 1) + ) + }); } }