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

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

"#); + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 5917b8b3bd..d55b2a464f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -177,20 +177,20 @@ pub fn line_end( pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_preceding_boundary(map, point, |left, right| { - (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) + (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace()) || left == '\n' }) } pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_preceding_boundary(map, point, |left, right| { let is_word_start = - char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); + char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace(); let is_subword_start = left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start || left == '\n' @@ -199,19 +199,19 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_boundary(map, point, |left, right| { - (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) + (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace()) || right == '\n' }) } pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_boundary(map, point, |left, right| { let is_word_end = - (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); + (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace(); let is_subword_end = left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_end || is_subword_end || right == '\n' @@ -399,14 +399,14 @@ pub fn find_boundary_in_line( pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; - let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c)); + let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c)); let prev_char_kind = text .reversed_chars_at(ix) .next() - .map(|c| char_kind(language, c)); + .map(|c| char_kind(&scope, c)); prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d4061f25dc..99dcbd189c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1360,11 +1360,13 @@ impl MultiBuffer { return false; } - if char.is_alphanumeric() || char == '_' { + let snapshot = self.snapshot(cx); + let position = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(position); + if char_kind(&scope, char) == CharKind::Word { return true; } - let snapshot = self.snapshot(cx); let anchor = snapshot.anchor_before(position); anchor .buffer_id @@ -1866,8 +1868,8 @@ impl MultiBufferSnapshot { let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); - let language = self.language_at(start); - let kind = |c| char_kind(language, c); + let scope = self.language_scope_at(start); + let kind = |c| char_kind(&scope, c); let word_kind = cmp::max( prev_chars.peek().copied().map(kind), next_chars.peek().copied().map(kind), diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 83aaa3b703..a2a7d71dce 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -50,7 +50,7 @@ impl<'a> EditorLspTestContext<'a> { language .path_suffixes() .first() - .unwrap_or(&"txt".to_string()) + .expect("language must have a path suffix for EditorLspTestContext") ); let mut fake_servers = language diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index eff95460c4..44ee870797 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2175,8 +2175,8 @@ impl BufferSnapshot { let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); - let language = self.language_at(start); - let kind = |c| char_kind(language, c); + let scope = self.language_scope_at(start); + let kind = |c| char_kind(&scope, c); let word_kind = cmp::max( prev_chars.peek().copied().map(kind), next_chars.peek().copied().map(kind), @@ -2988,17 +2988,21 @@ pub fn contiguous_ranges( }) } -pub fn char_kind(language: Option<&Arc>, c: char) -> CharKind { +pub fn char_kind(scope: &Option, c: char) -> CharKind { if c.is_whitespace() { return CharKind::Whitespace; } else if c.is_alphanumeric() || c == '_' { return CharKind::Word; } - if let Some(language) = language { - if language.config.word_characters.contains(&c) { - return CharKind::Word; + + if let Some(scope) = scope { + if let Some(characters) = scope.word_characters() { + if characters.contains(&c) { + return CharKind::Word; + } } } + CharKind::Punctuation } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 58732355a5..ccc1937032 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -370,6 +370,8 @@ pub struct LanguageConfigOverride { pub block_comment: Override<(Arc, Arc)>, #[serde(skip_deserializing)] pub disabled_bracket_ixs: Vec, + #[serde(default)] + pub word_characters: Override>, } #[derive(Clone, Deserialize, Debug)] @@ -1557,6 +1559,13 @@ impl LanguageScope { .map(|e| (&e.0, &e.1)) } + pub fn word_characters(&self) -> Option<&HashSet> { + Override::as_option( + self.config_override().map(|o| &o.word_characters), + Some(&self.language.config.word_characters), + ) + } + pub fn brackets(&self) -> impl Iterator { let mut disabled_ids = self .config_override() diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index 85ae088565..b925bc8f0d 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e", - "version": "1.21.0" + "revision": "ce20dc083ee485524b802669890291c0d8090170", + "version": "1.22.1" } } ] diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index bfa34c0422..c0ba5a609e 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -204,15 +204,14 @@ impl SearchQuery { if self.as_str().is_empty() { return Default::default(); } - let language = buffer.language_at(0); + + let range_offset = subrange.as_ref().map(|r| r.start).unwrap_or(0); let rope = if let Some(range) = subrange { buffer.as_rope().slice(range) } else { buffer.as_rope().clone() }; - let kind = |c| char_kind(language, c); - let mut matches = Vec::new(); match self { Self::Text { @@ -228,6 +227,9 @@ impl SearchQuery { let mat = mat.unwrap(); if *whole_word { + let scope = buffer.language_scope_at(range_offset + mat.start()); + let kind = |c| char_kind(&scope, c); + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind); let start_kind = kind(rope.chars_at(mat.start()).next().unwrap()); let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap()); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 1defee70da..243653680b 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -439,12 +439,12 @@ pub(crate) fn next_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { let mut crossed_newline = false; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); let at_newline = right == '\n'; let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) @@ -464,12 +464,12 @@ fn next_word_end( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { *point.column_mut() += 1; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); @@ -495,13 +495,13 @@ fn previous_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { // This works even though find_preceding_boundary is called for every character in the line containing // cursor because the newline is checked only once. point = movement::find_preceding_boundary(map, point, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); (left_kind != right_kind && !right.is_whitespace()) || left == '\n' }); @@ -511,7 +511,7 @@ fn previous_word_start( fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { let mut last_point = DisplayPoint::new(from.row(), 0); - let language = map.buffer_snapshot.language_at(from.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { return from; @@ -519,7 +519,7 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi last_point = point; - if char_kind(language, ch) != CharKind::Whitespace { + if char_kind(&scope, ch) != CharKind::Whitespace { break; } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 50bc049a3a..1c9aa48951 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -82,19 +82,19 @@ fn expand_changed_word_selection( ignore_punctuation: bool, ) -> bool { if times.is_none() || times.unwrap() == 1 { - let language = map + let scope = map .buffer_snapshot - .language_at(selection.start.to_point(map)); + .language_scope_at(selection.start.to_point(map)); let in_word = map .chars_at(selection.head()) .next() - .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace) .unwrap_or_default(); if in_word { selection.end = movement::find_boundary(map, selection.end, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index d0bcad36c2..a6cc91d7bd 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -122,18 +122,20 @@ fn in_word( ignore_punctuation: bool, ) -> Option> { // Use motion::right so that we consider the character under the cursor when looking for the start - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }, ); let end = movement::find_boundary_in_line(map, relative_to, |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }); Some(start..end) @@ -156,11 +158,13 @@ fn around_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); let in_word = map .chars_at(relative_to) .next() - .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace) .unwrap_or(false); if in_word { @@ -184,21 +188,23 @@ fn around_next_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); // Get the start of the word let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }, ); let mut word_found = false; let end = movement::find_boundary(map, relative_to, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index c23ddcd6e7..7f6c6931e4 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -17,3 +17,6 @@ brackets = [ [overrides.element] line_comment = { remove = true } block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] From c8e5c3963b89187ff2997395bad60f6dba5e7b10 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Aug 2023 09:51:47 +0200 Subject: [PATCH 053/156] Clear selection when deploying inline assistant --- crates/ai/src/assistant.rs | 3 +++ todo.md | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 65b255d458..b99d4b4fac 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -276,6 +276,9 @@ impl AssistantPanel { assistant }); let block_id = editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_anchor_ranges([selection.head()..selection.head()]) + }); editor.highlight_background::( vec![range.clone()], |theme| theme.assistant.inline.pending_edit_background, diff --git a/todo.md b/todo.md index 8a9a8b5b3d..71ca5a7c7b 100644 --- a/todo.md +++ b/todo.md @@ -14,12 +14,8 @@ - [ ] Selection is cleared and cursor is moved to prompt input - [ ] Ability to highlight background multiple times for the same type - [x] Basic Styling -- [ ] Match lowest indentation level of selected lines when inserting an inline assist - [ ] Look into why insert prompts have a weird indentation sometimes - - - - Multicursor - Run the same prompt for every selection in parallel - Position the prompt editor at the newest cursor From 658d616b9661f086681b25e33b5e76620fd18b1d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Aug 2023 11:55:03 +0200 Subject: [PATCH 054/156] Allow multiple inline assistant highlights at once --- crates/ai/src/assistant.rs | 216 +++++++++++++++++++++++------------- crates/editor/src/editor.rs | 1 + todo.md | 4 +- 3 files changed, 143 insertions(+), 78 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index b99d4b4fac..c5bf027fcc 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -13,13 +13,14 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, ToOffset, ToPoint, + Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ actions, elements::*, + fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton}, Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, @@ -279,11 +280,6 @@ impl AssistantPanel { editor.change_selections(None, cx, |selections| { selections.select_anchor_ranges([selection.head()..selection.head()]) }); - editor.highlight_background::( - vec![range.clone()], - |theme| theme.assistant.inline.pending_edit_background, - cx, - ); editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, @@ -318,6 +314,7 @@ impl AssistantPanel { kind: assist_kind, editor: editor.downgrade(), range, + highlighted_ranges: Default::default(), inline_assistant_block_id: Some(block_id), code_generation: Task::ready(None), transaction_id: None, @@ -342,6 +339,7 @@ impl AssistantPanel { .entry(editor.downgrade()) .or_default() .push(id); + self.update_highlights_for_editor(&editor, cx); } fn handle_inline_assistant_event( @@ -416,10 +414,7 @@ impl AssistantPanel { } if let Some(editor) = pending_assist.editor.upgrade(cx) { - editor.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx); - editor.clear_text_highlights::(cx); - }); + self.update_highlights_for_editor(&editor, cx); if cancel { if let Some(transaction_id) = pending_assist.transaction_id { @@ -741,78 +736,75 @@ impl AssistantPanel { }); while let Some(hunks) = hunks_rx.next().await { - let this = this + let editor = editor .upgrade(&cx) - .ok_or_else(|| anyhow!("assistant was dropped"))?; - editor.update(&mut cx, |editor, cx| { - let mut highlights = Vec::new(); + .ok_or_else(|| anyhow!("editor was dropped"))?; - let transaction = editor.buffer().update(cx, |buffer, cx| { - // Avoid grouping assistant edits with user edits. - buffer.finalize_last_transaction(cx); + this.update(&mut cx, |this, cx| { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + pending_assist + } else { + return; + }; - buffer.start_transaction(cx); - buffer.edit( - hunks.into_iter().filter_map(|hunk| match hunk { - Hunk::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - Some((edit_start..edit_start, text)) - } - Hunk::Remove { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start = edit_end; - Some((edit_range, String::new())) - } - Hunk::Keep { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start += len; - highlights.push(edit_range); - None - } - }), - None, - cx, - ); + pending_assist.highlighted_ranges.clear(); + editor.update(cx, |editor, cx| { + let transaction = editor.buffer().update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); - buffer.end_transaction(cx) + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start += len; + pending_assist.highlighted_ranges.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + if let Some(first_transaction) = pending_assist.transaction_id { + // Group all assistant edits into the first transaction. + editor.buffer().update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + pending_assist.transaction_id = Some(transaction); + editor.buffer().update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } }); - if let Some(transaction) = transaction { - this.update(cx, |this, cx| { - if let Some(pending_assist) = - this.pending_inline_assists.get_mut(&inline_assist_id) - { - if let Some(first_transaction) = pending_assist.transaction_id { - // Group all assistant edits into the first transaction. - editor.buffer().update(cx, |buffer, cx| { - buffer.merge_transactions( - transaction, - first_transaction, - cx, - ) - }); - } else { - pending_assist.transaction_id = Some(transaction); - editor.buffer().update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx) - }); - } - } - }); - } - - editor.highlight_text::( - highlights, - gpui::fonts::HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); + this.update_highlights_for_editor(&editor, cx); })?; } diff.await?; @@ -823,6 +815,55 @@ impl AssistantPanel { }); } + fn update_highlights_for_editor( + &self, + editor: &ViewHandle, + cx: &mut ViewContext, + ) { + let mut background_ranges = Vec::new(); + let mut foreground_ranges = Vec::new(); + let empty_inline_assist_ids = Vec::new(); + let inline_assist_ids = self + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .unwrap_or(&empty_inline_assist_ids); + + for inline_assist_id in inline_assist_ids { + if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { + background_ranges.push(pending_assist.range.clone()); + foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned()); + } + } + + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + merge_ranges(&mut background_ranges, &snapshot); + merge_ranges(&mut foreground_ranges, &snapshot); + editor.update(cx, |editor, cx| { + if background_ranges.is_empty() { + editor.clear_background_highlights::(cx); + } else { + editor.highlight_background::( + background_ranges, + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + } + + if foreground_ranges.is_empty() { + editor.clear_text_highlights::(cx); + } else { + editor.highlight_text::( + foreground_ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + } + }); + } + fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { let editor = cx.add_view(|cx| { ConversationEditor::new( @@ -2842,12 +2883,35 @@ struct PendingInlineAssist { kind: InlineAssistKind, editor: WeakViewHandle, range: Range, + highlighted_ranges: Vec>, inline_assistant_block_id: Option, code_generation: Task>, transaction_id: Option, _subscriptions: Vec, } +fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { + ranges.sort_unstable_by(|a, b| { + a.start + .cmp(&b.start, buffer) + .then_with(|| b.end.cmp(&a.end, buffer)) + }); + + let mut ix = 0; + while ix + 1 < ranges.len() { + let b = ranges[ix + 1].clone(); + let a = &mut ranges[ix]; + if a.end.cmp(&b.start, buffer).is_gt() { + if a.end.cmp(&b.end, buffer).is_lt() { + a.end = b.end; + } + ranges.remove(ix + 1); + } else { + ix += 1; + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b206f2ec8b..d5141a7f7d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7592,6 +7592,7 @@ impl Editor { } results } + pub fn background_highlights_in_range_for( &self, search_range: Range, diff --git a/todo.md b/todo.md index 71ca5a7c7b..0507708502 100644 --- a/todo.md +++ b/todo.md @@ -11,8 +11,8 @@ - When cursor is inside a prompt - [x] Escape cancels/undoes - [x] Enter confirms -- [ ] Selection is cleared and cursor is moved to prompt input -- [ ] Ability to highlight background multiple times for the same type +- [x] Selection is cleared and cursor is moved to prompt input +- [x] Ability to highlight background multiple times for the same type - [x] Basic Styling - [ ] Look into why insert prompts have a weird indentation sometimes From 55bf45d2657278da8c1a4d70ae4baca2099b510d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Aug 2023 12:07:03 +0200 Subject: [PATCH 055/156] Add disabled style for prompt editor after confirming --- crates/ai/src/assistant.rs | 11 +++++++++-- crates/editor/src/editor.rs | 9 +++++++++ crates/theme/src/theme.rs | 1 + styles/src/style_tree/assistant.ts | 10 +++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index c5bf027fcc..f7abcdf748 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -2871,8 +2871,15 @@ impl InlineAssistant { cx.emit(InlineAssistantEvent::Dismissed); } else { let prompt = self.prompt_editor.read(cx).text(cx); - self.prompt_editor - .update(cx, |editor, _| editor.set_read_only(true)); + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + }); cx.emit(InlineAssistantEvent::Confirmed { prompt }); self.confirmed = true; } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d5141a7f7d..75fb6006c0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1606,6 +1606,15 @@ impl Editor { self.read_only = read_only; } + pub fn set_field_editor_style( + &mut self, + style: Option>, + cx: &mut ViewContext, + ) { + self.get_field_editor_theme = style; + cx.notify(); + } + pub fn replica_id_map(&self) -> Option<&HashMap> { self.replica_id_mapping.as_ref() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bc0c98bac7..7913685b7a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1158,6 +1158,7 @@ pub struct InlineAssistantStyle { #[serde(flatten)] pub container: ContainerStyle, pub editor: FieldEditor, + pub disabled_editor: FieldEditor, pub pending_edit_background: Color, } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index bdca8a16e5..8bef2ce16b 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -68,9 +68,17 @@ export default function assistant(): any { }), editor: { text: text(theme.highest, "mono", "default", { size: "sm" }), - placeholder_text: text(theme.lowest, "sans", "on", "disabled"), + placeholder_text: text(theme.highest, "sans", "on", "disabled"), selection: theme.players[0], }, + disabled_editor: { + text: text(theme.highest, "mono", "disabled", { size: "sm" }), + placeholder_text: text(theme.highest, "sans", "on", "disabled"), + selection: { + cursor: text(theme.highest, "mono", "disabled").color, + selection: theme.players[0].selection, + }, + }, pending_edit_background: background(theme.highest, "positive"), }, message_header: { From 937aabfdfdd435807368068f6e47f7d03981919c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 11:24:55 +0200 Subject: [PATCH 056/156] Extract a `strip_markdown_codeblock` function --- crates/ai/src/assistant.rs | 197 +++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 87 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index f7abcdf748..0333a723e9 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -16,7 +16,7 @@ use editor::{ Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; -use futures::{channel::mpsc, SinkExt, StreamExt}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{ actions, elements::*, @@ -620,7 +620,10 @@ impl AssistantPanel { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { - let mut messages = response.await?; + let chunks = strip_markdown_codeblock(response.await?.filter_map( + |message| async move { message.ok()?.choices.pop()?.delta.content }, + )); + futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); let indentation_len; @@ -636,93 +639,21 @@ impl AssistantPanel { indentation_text = ""; }; - let mut inside_first_line = true; - let mut starts_with_fenced_code_block = None; - let mut has_pending_newline = false; - let mut new_text = String::new(); + let mut new_text = indentation_text + .repeat(indentation_len.saturating_sub(selection_start.column) as usize); - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(mut choice) = message.choices.pop() { - if has_pending_newline { - has_pending_newline = false; - choice - .delta - .content - .get_or_insert(String::new()) - .insert(0, '\n'); - } + while let Some(message) = chunks.next().await { + let mut lines = message.split('\n'); + if let Some(first_line) = lines.next() { + new_text.push_str(first_line); + } - // Buffer a trailing codeblock fence. Note that we don't stop - // right away because this may be an inner fence that we need - // to insert into the editor. - if starts_with_fenced_code_block.is_some() - && choice.delta.content.as_deref() == Some("\n```") - { - new_text.push_str("\n```"); - continue; - } - - // If this was the last completion and we started with a codeblock - // fence and we ended with another codeblock fence, then we can - // stop right away. Otherwise, whatever text we buffered will be - // processed normally. - if choice.finish_reason.is_some() - && starts_with_fenced_code_block.unwrap_or(false) - && new_text == "\n```" - { - break; - } - - if let Some(text) = choice.delta.content { - // Never push a newline if there's nothing after it. This is - // useful to detect if the newline was pushed because of a - // trailing codeblock fence. - let text = if let Some(prefix) = text.strip_suffix('\n') { - has_pending_newline = true; - prefix - } else { - text.as_str() - }; - - if text.is_empty() { - continue; - } - - let mut lines = text.split('\n'); - if let Some(line) = lines.next() { - if starts_with_fenced_code_block.is_none() { - starts_with_fenced_code_block = - Some(line.starts_with("```")); - } - - // Avoid pushing the first line if it's the start of a fenced code block. - if !inside_first_line || !starts_with_fenced_code_block.unwrap() - { - new_text.push_str(&line); - } - } - - for line in lines { - if inside_first_line && starts_with_fenced_code_block.unwrap() { - // If we were inside the first line and that line was the - // start of a fenced code block, we just need to push the - // leading indentation of the original selection. - new_text.push_str(&indentation_text.repeat( - indentation_len.saturating_sub(selection_start.column) - as usize, - )); - } else { - // Otherwise, we need to push a newline and the base indentation. - new_text.push('\n'); - new_text.push_str( - &indentation_text.repeat(indentation_len as usize), - ); - } - - new_text.push_str(line); - inside_first_line = false; - } + for line in lines { + new_text.push('\n'); + if !line.is_empty() { + new_text + .push_str(&indentation_text.repeat(indentation_len as usize)); + new_text.push_str(line); } } @@ -2919,10 +2850,58 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } +fn strip_markdown_codeblock(stream: impl Stream) -> impl Stream { + let mut first_line = true; + let mut buffer = String::new(); + let mut starts_with_fenced_code_block = false; + stream.filter_map(move |chunk| { + buffer.push_str(&chunk); + + if first_line { + if buffer == "" || buffer == "`" || buffer == "``" { + return futures::future::ready(None); + } else if buffer.starts_with("```") { + starts_with_fenced_code_block = true; + if let Some(newline_ix) = buffer.find('\n') { + buffer.replace_range(..newline_ix + 1, ""); + first_line = false; + } else { + return futures::future::ready(None); + } + } + } + + let text = if starts_with_fenced_code_block { + buffer + .strip_suffix("\n```") + .or_else(|| buffer.strip_suffix("\n``")) + .or_else(|| buffer.strip_suffix("\n`")) + .or_else(|| buffer.strip_suffix('\n')) + .unwrap_or(&buffer) + } else { + &buffer + }; + + if text.contains('\n') { + first_line = false; + } + + let remainder = buffer.split_off(text.len()); + let result = if buffer.is_empty() { + None + } else { + Some(buffer.clone()) + }; + buffer = remainder; + futures::future::ready(result) + }) +} + #[cfg(test)] mod tests { use super::*; use crate::MessageId; + use futures::stream; use gpui::AppContext; #[gpui::test] @@ -3291,6 +3270,50 @@ mod tests { ); } + #[gpui::test] + async fn test_strip_markdown_codeblock() { + assert_eq!( + strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) + .collect::() + .await, + "```js\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .collect::() + .await, + "``\nLorem ipsum dolor\n```" + ); + + fn chunks(text: &str, size: usize) -> impl Stream { + stream::iter( + text.chars() + .collect::>() + .chunks(size) + .map(|chunk| chunk.iter().collect::()) + .collect::>(), + ) + } + } + fn messages( conversation: &ModelHandle, cx: &AppContext, From d804afcfa96e44044ad64a89d2b72fe07b2bec9b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 11:57:02 +0200 Subject: [PATCH 057/156] Don't auto-indent when the assistant starts responding with indentation --- crates/ai/src/assistant.rs | 81 +++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 0333a723e9..7803a89ea9 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -472,48 +472,48 @@ impl AssistantPanel { .text_for_range(range.start..range.end) .collect::(); - let mut base_indentation: Option = None; let selection_start = range.start.to_point(&snapshot); let selection_end = range.end.to_point(&snapshot); + + let mut base_indent: Option = None; let mut start_row = selection_start.row; if snapshot.is_line_blank(start_row) { if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { start_row = prev_non_blank_row; } } - for row in start_row..=selection_end.row { if snapshot.is_line_blank(row) { continue; } - let line_indentation = snapshot.indent_size_for_line(row); - if let Some(base_indentation) = base_indentation.as_mut() { - if line_indentation.len < base_indentation.len { - *base_indentation = line_indentation; + let line_indent = snapshot.indent_size_for_line(row); + if let Some(base_indent) = base_indent.as_mut() { + if line_indent.len < base_indent.len { + *base_indent = line_indent; } } else { - base_indentation = Some(line_indentation); + base_indent = Some(line_indent); } } let mut normalized_selected_text = selected_text.clone(); - if let Some(base_indentation) = base_indentation { + if let Some(base_indent) = base_indent { for row in selection_start.row..=selection_end.row { let selection_row = row - selection_start.row; let line_start = normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); - let indentation_len = if row == selection_start.row { - base_indentation.len.saturating_sub(selection_start.column) + let indent_len = if row == selection_start.row { + base_indent.len.saturating_sub(selection_start.column) } else { let line_len = normalized_selected_text.line_len(selection_row); - cmp::min(line_len, base_indentation.len) + cmp::min(line_len, base_indent.len) }; - let indentation_end = cmp::min( - line_start + indentation_len as usize, + let indent_end = cmp::min( + line_start + indent_len as usize, normalized_selected_text.len(), ); - normalized_selected_text.replace(line_start..indentation_end, ""); + normalized_selected_text.replace(line_start..indent_end, ""); } } @@ -581,7 +581,11 @@ impl AssistantPanel { "Assume the cursor is located where the `<|>` marker is." ) .unwrap(); - writeln!(prompt, "Assume your answer will be inserted at the cursor.").unwrap(); + writeln!( + prompt, + "Code can't be replaced, so assume your answer will be inserted at the cursor." + ) + .unwrap(); writeln!( prompt, "Complete the code given the user prompt: {user_prompt}" @@ -591,7 +595,11 @@ impl AssistantPanel { } writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!(prompt, "Never make remarks, always output code.").unwrap(); + writeln!( + prompt, + "Never make remarks about the output, always output just code." + ) + .unwrap(); let request = OpenAIRequest { model: model.full_name().into(), @@ -626,40 +634,51 @@ impl AssistantPanel { futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); - let indentation_len; - let indentation_text; - if let Some(base_indentation) = base_indentation { - indentation_len = base_indentation.len; - indentation_text = match base_indentation.kind { + let indent_len; + let indent_text; + if let Some(base_indent) = base_indent { + indent_len = base_indent.len; + indent_text = match base_indent.kind { language::IndentKind::Space => " ", language::IndentKind::Tab => "\t", }; } else { - indentation_len = 0; - indentation_text = ""; + indent_len = 0; + indent_text = ""; }; - let mut new_text = indentation_text - .repeat(indentation_len.saturating_sub(selection_start.column) as usize); + let mut autoindent = true; + let mut first_chunk = true; + let mut new_text = String::new(); - while let Some(message) = chunks.next().await { - let mut lines = message.split('\n'); + while let Some(chunk) = chunks.next().await { + if first_chunk && (chunk.starts_with(' ') || chunk.starts_with('\t')) { + autoindent = false; + } + + if first_chunk && autoindent { + let first_line_indent = + indent_len.saturating_sub(selection_start.column) as usize; + new_text = indent_text.repeat(first_line_indent); + } + + let mut lines = chunk.split('\n'); if let Some(first_line) = lines.next() { new_text.push_str(first_line); } for line in lines { new_text.push('\n'); - if !line.is_empty() { - new_text - .push_str(&indentation_text.repeat(indentation_len as usize)); - new_text.push_str(line); + if !line.is_empty() && autoindent { + new_text.push_str(&indent_text.repeat(indent_len as usize)); } + new_text.push_str(line); } let hunks = diff.push_new(&new_text); hunks_tx.send(hunks).await?; new_text.clear(); + first_chunk = false; } hunks_tx.send(diff.finish()).await?; From 1fb7ce0f4a3dfa492cbc73a95db56c6671bcd23c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 12:13:44 +0200 Subject: [PATCH 058/156] Show icon to toggle inline assist --- Cargo.lock | 1 + crates/ai/src/assistant.rs | 6 ++++- crates/quick_action_bar/Cargo.toml | 1 + .../quick_action_bar/src/quick_action_bar.rs | 23 +++++++++++++++++-- crates/zed/src/zed.rs | 5 ++-- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c232afa081..84b8093be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5647,6 +5647,7 @@ dependencies = [ name = "quick_action_bar" version = "0.1.0" dependencies = [ + "ai", "editor", "gpui", "search", diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 7803a89ea9..193cba8db4 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -215,7 +215,11 @@ impl AssistantPanel { }) } - fn inline_assist(workspace: &mut Workspace, _: &InlineAssist, cx: &mut ViewContext) { + pub fn inline_assist( + workspace: &mut Workspace, + _: &InlineAssist, + cx: &mut ViewContext, + ) { let this = if let Some(this) = workspace.panel::(cx) { if this .update(cx, |assistant, cx| assistant.load_api_key(cx)) diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 6953ac0e02..1f8ec4e92b 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -9,6 +9,7 @@ path = "src/quick_action_bar.rs" doctest = false [dependencies] +ai = { path = "../ai" } editor = { path = "../editor" } gpui = { path = "../gpui" } search = { path = "../search" } diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 3055399c13..a7734deac5 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -1,25 +1,29 @@ +use ai::{assistant::InlineAssist, AssistantPanel}; use editor::Editor; use gpui::{ elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg}, platform::{CursorStyle, MouseButton}, Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle, + WeakViewHandle, }; use search::{buffer_search, BufferSearchBar}; -use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; +use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace}; pub struct QuickActionBar { buffer_search_bar: ViewHandle, active_item: Option>, _inlay_hints_enabled_subscription: Option, + workspace: WeakViewHandle, } impl QuickActionBar { - pub fn new(buffer_search_bar: ViewHandle) -> Self { + pub fn new(buffer_search_bar: ViewHandle, workspace: &Workspace) -> Self { Self { buffer_search_bar, active_item: None, _inlay_hints_enabled_subscription: None, + workspace: workspace.weak_handle(), } } @@ -86,6 +90,21 @@ impl View for QuickActionBar { )); } + bar.add_child(render_quick_action_bar_button( + 2, + "icons/radix/magic-wand.svg", + false, + ("Generate code...".into(), Some(Box::new(InlineAssist))), + cx, + move |this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + AssistantPanel::inline_assist(workspace, &Default::default(), cx); + }); + } + }, + )); + bar.into_any() } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index de05c259c8..6421818b62 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -264,8 +264,9 @@ pub fn initialize_workspace( toolbar.add_item(breadcrumbs, cx); let buffer_search_bar = cx.add_view(BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - let quick_action_bar = - cx.add_view(|_| QuickActionBar::new(buffer_search_bar)); + let quick_action_bar = cx.add_view(|_| { + QuickActionBar::new(buffer_search_bar, workspace) + }); toolbar.add_item(quick_action_bar, cx); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); toolbar.add_item(project_search_bar, cx); From c587cf66ce5cb6f38d3cee843e049adf3830babe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 12:24:01 +0200 Subject: [PATCH 059/156] Remove ellipsis from tooltip --- crates/quick_action_bar/src/quick_action_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 7d6587795e..da5a8e6d72 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -96,7 +96,7 @@ impl View for QuickActionBar { 2, "icons/radix/magic-wand.svg", false, - ("Generate code...".into(), Some(Box::new(InlineAssist))), + ("Generate code".into(), Some(Box::new(InlineAssist))), cx, move |this, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { From 8c4d2ccf80a066d9344702950de9986fc3d1f636 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:23:42 +0200 Subject: [PATCH 060/156] Close inline assist when the associated transaction is undone --- crates/ai/src/assistant.rs | 34 ++++++++++++++++++++++++++-------- crates/editor/src/editor.rs | 6 ++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 3b444082c6..4dad12ad08 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -249,7 +249,7 @@ impl AssistantPanel { } fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { - let id = post_inc(&mut self.next_inline_assist_id); + let inline_assist_id = post_inc(&mut self.next_inline_assist_id); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selection = editor.read(cx).selections.newest_anchor().clone(); let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); @@ -272,7 +272,7 @@ impl AssistantPanel { }); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant { - id, + id: inline_assist_id, prompt_editor, confirmed: false, has_focus: false, @@ -313,7 +313,7 @@ impl AssistantPanel { }); self.pending_inline_assists.insert( - id, + inline_assist_id, PendingInlineAssist { kind: assist_kind, editor: editor.downgrade(), @@ -326,12 +326,30 @@ impl AssistantPanel { cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), cx.subscribe(editor, { let inline_assistant = inline_assistant.downgrade(); - move |_, editor, event, cx| { + move |this, editor, event, cx| { if let Some(inline_assistant) = inline_assistant.upgrade(cx) { - if let editor::Event::SelectionsChanged { local } = event { - if *local && inline_assistant.read(cx).has_focus { - cx.focus(&editor); + match event { + editor::Event::SelectionsChanged { local } => { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); + } } + editor::Event::TransactionUndone { + transaction_id: tx_id, + } => { + if let Some(pending_assist) = + this.pending_inline_assists.get(&inline_assist_id) + { + if pending_assist.transaction_id == Some(*tx_id) { + this.close_inline_assist( + inline_assist_id, + false, + cx, + ); + } + } + } + _ => {} } } } @@ -342,7 +360,7 @@ impl AssistantPanel { self.pending_inline_assist_ids_by_editor .entry(editor.downgrade()) .or_default() - .push(id); + .push(inline_assist_id); self.update_highlights_for_editor(&editor, cx); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6396536b83..fde280f8fe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4975,6 +4975,9 @@ impl Editor { self.unmark_text(cx); self.refresh_copilot_suggestions(true, cx); cx.emit(Event::Edited); + cx.emit(Event::TransactionUndone { + transaction_id: tx_id, + }); } } @@ -8404,6 +8407,9 @@ pub enum Event { local: bool, autoscroll: bool, }, + TransactionUndone { + transaction_id: TransactionId, + }, Closed, } From b9df85e01fa5d6934e67da4c1238173f002db5cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:25:01 +0200 Subject: [PATCH 061/156] Remove todo.md --- todo.md | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 todo.md diff --git a/todo.md b/todo.md deleted file mode 100644 index 0507708502..0000000000 --- a/todo.md +++ /dev/null @@ -1,48 +0,0 @@ -- Hit ctrl-enter deploys an edit prompt - - Empty selection (cursor) => append text - - On end of line: Edit prompt on end of line. - - [x] Middle of line: Edit prompt near cursor head on a different line - - Non-empty selection => refactor - - [x] Edit prompt near cursor head on a different line - - [x] What was selected when you hit ctrl-enter is colored. -- [x] Add placeholder text - - If non-empty selection: Enter prompt to transform selected text - - If empty selection: Enter prompt to generate text -- When cursor is inside a prompt - - [x] Escape cancels/undoes - - [x] Enter confirms -- [x] Selection is cleared and cursor is moved to prompt input -- [x] Ability to highlight background multiple times for the same type -- [x] Basic Styling -- [ ] Look into why insert prompts have a weird indentation sometimes - -- Multicursor - - Run the same prompt for every selection in parallel - - Position the prompt editor at the newest cursor -- Follow up ship: Marks - - Global across all buffers - - Select text, hit a binding - - That text gets added to the marks - - Simplest: Marks are a set, and you add to them with this binding. - - Could this be a stack? That might be too much. - - When you hit ctrl-enter to generate / transform text, we include the marked text in the context. - -- During inference, always send marked text. -- During inference, send as much context as possible given the user's desired generation length. - -- This would assume a convenient binding for setting the generation length. - - -~~~~~~~~~ - -Dial up / dial down how much context we send -Dial up / down your max generation length. - - -------- (merge to main) - -- Text in the prompt should soft wrap - ------------ (maybe pause) - -- Excurse outside of the editor without dismissing it... kind of like a message in the assistant. From 52e1e014ad08ecba01843c36f4ff6dac905933c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:42:41 +0200 Subject: [PATCH 062/156] Allow redoing edits performed by inline assistant after cancelling it --- crates/ai/src/assistant.rs | 2 +- crates/editor/src/multi_buffer.rs | 24 ++++++++++++++++++------ crates/language/src/buffer.rs | 4 ++-- crates/text/src/text.rs | 29 +++++++++++++++++++++-------- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4dad12ad08..952c924292 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -442,7 +442,7 @@ impl AssistantPanel { if let Some(transaction_id) = pending_assist.transaction_id { editor.update(cx, |editor, cx| { editor.buffer().update(cx, |buffer, cx| { - buffer.undo_and_forget(transaction_id, cx) + buffer.undo_transaction(transaction_id, cx) }); }); } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c7c4028995..0c499c16c4 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -824,13 +824,15 @@ impl MultiBuffer { None } - pub fn undo_and_forget(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { + pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| buffer.undo_and_forget(transaction_id, cx)); - } else if let Some(transaction) = self.history.forget(transaction_id) { - for (buffer_id, transaction_id) in transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(&buffer_id) { - buffer.update(cx, |buffer, cx| buffer.undo_and_forget(transaction_id, cx)); + buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { + for (buffer_id, transaction_id) in &transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.undo_transaction(*transaction_id, cx) + }); } } } @@ -3454,6 +3456,16 @@ impl History { } } + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { + let ix = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id)?; + let transaction = self.undo_stack.remove(ix); + self.redo_stack.push(transaction); + self.redo_stack.last() + } + fn group(&mut self) -> Option { let mut count = 0; let mut transactions = self.undo_stack.iter(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2ed99d8526..4310f84830 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1668,14 +1668,14 @@ impl Buffer { } } - pub fn undo_and_forget( + pub fn undo_transaction( &mut self, transaction_id: TransactionId, cx: &mut ModelContext, ) -> bool { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); - if let Some(operation) = self.text.undo_and_forget(transaction_id) { + if let Some(operation) = self.text.undo_transaction(transaction_id) { self.send_operation(Operation::Buffer(operation), cx); self.did_edit(&old_version, was_dirty, cx); true diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 43bcef0825..6a00ea12db 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -264,7 +264,19 @@ impl History { } } - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> { + assert_eq!(self.transaction_depth, 0); + + let entry_ix = self + .undo_stack + .iter() + .rposition(|entry| entry.transaction.id == transaction_id)?; + let entry = self.undo_stack.remove(entry_ix); + self.redo_stack.push(entry); + self.redo_stack.last() + } + + fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { assert_eq!(self.transaction_depth, 0); let redo_stack_start_len = self.redo_stack.len(); @@ -1207,19 +1219,20 @@ impl Buffer { } } - pub fn undo_and_forget(&mut self, transaction_id: TransactionId) -> Option { - if let Some(transaction) = self.history.forget(transaction_id) { - self.undo_or_redo(transaction).log_err() - } else { - None - } + pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option { + let transaction = self + .history + .remove_from_undo(transaction_id)? + .transaction + .clone(); + self.undo_or_redo(transaction).log_err() } #[allow(clippy::needless_collect)] pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { let transactions = self .history - .remove_from_undo(transaction_id) + .remove_from_undo_until(transaction_id) .iter() .map(|entry| entry.transaction.clone()) .collect::>(); From ccec59337a6dea4f2daed7ed5fc2cc6350241970 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 14:46:05 +0200 Subject: [PATCH 063/156] :memo: --- crates/ai/src/assistant.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 952c924292..80c3771085 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -341,6 +341,9 @@ impl AssistantPanel { this.pending_inline_assists.get(&inline_assist_id) { if pending_assist.transaction_id == Some(*tx_id) { + // Notice we are supplying `undo: false` here. This + // is because there's no need to undo the transaction + // because the user just did so. this.close_inline_assist( inline_assist_id, false, @@ -421,7 +424,7 @@ impl AssistantPanel { } } - fn close_inline_assist(&mut self, assist_id: usize, cancel: bool, cx: &mut ViewContext) { + fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { self.hide_inline_assist(assist_id, cx); if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { @@ -438,7 +441,7 @@ impl AssistantPanel { if let Some(editor) = pending_assist.editor.upgrade(cx) { self.update_highlights_for_editor(&editor, cx); - if cancel { + if undo { if let Some(transaction_id) = pending_assist.transaction_id { editor.update(cx, |editor, cx| { editor.buffer().update(cx, |buffer, cx| { From ded6decb29d04b1bc56a723ee48127558e1e1b63 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 28 Aug 2023 11:27:45 -0400 Subject: [PATCH 064/156] Initial unstyled language server short name in completions Co-Authored-By: Kirill Bulatov --- crates/editor/src/editor.rs | 61 ++++++++++++++++++++------ crates/language/src/language.rs | 9 ++++ crates/zed/src/languages/c.rs | 4 ++ crates/zed/src/languages/css.rs | 4 ++ crates/zed/src/languages/elixir.rs | 4 ++ crates/zed/src/languages/go.rs | 4 ++ crates/zed/src/languages/html.rs | 4 ++ crates/zed/src/languages/json.rs | 4 ++ crates/zed/src/languages/lua.rs | 4 ++ crates/zed/src/languages/php.rs | 4 ++ crates/zed/src/languages/python.rs | 4 ++ crates/zed/src/languages/ruby.rs | 4 ++ crates/zed/src/languages/rust.rs | 4 ++ crates/zed/src/languages/svelte.rs | 4 ++ crates/zed/src/languages/tailwind.rs | 4 ++ crates/zed/src/languages/typescript.rs | 8 ++++ crates/zed/src/languages/yaml.rs | 4 ++ 17 files changed, 121 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bfa804c56c..a188a47e35 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -810,6 +810,7 @@ struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, + project: Option>, completions: Arc<[Completion]>, match_candidates: Vec, matches: Arc<[StringMatch]>, @@ -853,6 +854,26 @@ impl CompletionsMenu { fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { enum CompletionTag {} + let language_servers = self.project.as_ref().map(|project| { + project + .read(cx) + .language_servers_for_buffer(self.buffer.read(cx), cx) + .map(|(adapter, server)| (server.server_id(), format!("{}: ", adapter.short_name))) + .collect::>() + }); + let get_server_name = move |lookup_server_id: lsp::LanguageServerId| -> Option { + language_servers + .iter() + .flatten() + .find_map(|(server_id, server_name)| { + if *server_id == lookup_server_id { + Some(server_name.clone()) + } else { + None + } + }) + }; + let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; @@ -879,19 +900,31 @@ impl CompletionsMenu { style.autocomplete.item }; - Text::new(completion.label.text.clone(), style.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - style.text.color.into(), - styled_runs_for_code_label( - &completion.label, - &style.syntax, - ), - &mat.positions, - )) - .contained() - .with_style(item_style) + let completion_label = + Text::new(completion.label.text.clone(), style.text.clone()) + .with_soft_wrap(false) + .with_highlights( + combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + style.text.color.into(), + styled_runs_for_code_label( + &completion.label, + &style.syntax, + ), + &mat.positions, + ), + ); + + if let Some(server_name) = get_server_name(completion.server_id) { + Flex::row() + .with_child(Text::new(server_name, style.text.clone())) + .with_child(completion_label) + .into_any() + } else { + completion_label.into_any() + } + .contained() + .with_style(item_style) }, ) .with_cursor_style(CursorStyle::PointingHand) @@ -2850,6 +2883,7 @@ impl Editor { }); let id = post_inc(&mut self.next_completion_id); + let project = self.project.clone(); let task = cx.spawn(|this, mut cx| { async move { let menu = if let Some(completions) = completions.await.log_err() { @@ -2868,6 +2902,7 @@ impl Editor { }) .collect(), buffer, + project, completions: completions.into(), matches: Vec::new().into(), selected_item: 0, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ccc1937032..a2d02ecd96 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -90,6 +90,7 @@ pub struct LanguageServerName(pub Arc); /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, + pub short_name: &'static str, pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, @@ -100,6 +101,7 @@ pub struct CachedLspAdapter { impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; + let short_name = adapter.short_name(); let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = @@ -108,6 +110,7 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, + short_name, initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, @@ -216,6 +219,8 @@ pub trait LspAdapterDelegate: Send + Sync { pub trait LspAdapter: 'static + Send + Sync { async fn name(&self) -> LanguageServerName; + fn short_name(&self) -> &'static str; + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -1696,6 +1701,10 @@ impl LspAdapter for Arc { LanguageServerName(self.name.into()) } + fn short_name(&self) -> &'static str { + "FakeLspAdapter" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 47aa2b739c..ad5a68f8dd 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -19,6 +19,10 @@ impl super::LspAdapter for CLspAdapter { LanguageServerName("clangd".into()) } + fn short_name(&self) -> &'static str { + "clangd" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/css.rs b/crates/zed/src/languages/css.rs index 51db8b8ab8..f2103050f3 100644 --- a/crates/zed/src/languages/css.rs +++ b/crates/zed/src/languages/css.rs @@ -37,6 +37,10 @@ impl LspAdapter for CssLspAdapter { LanguageServerName("vscode-css-language-server".into()) } + fn short_name(&self) -> &'static str { + "css" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index c32927e15c..b166feda76 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -27,6 +27,10 @@ impl LspAdapter for ElixirLspAdapter { LanguageServerName("elixir-ls".into()) } + fn short_name(&self) -> &'static str { + "elixir-ls" + } + fn will_start_server( &self, delegate: &Arc, diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index d7982f7bdb..19b7013709 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -37,6 +37,10 @@ impl super::LspAdapter for GoLspAdapter { LanguageServerName("gopls".into()) } + fn short_name(&self) -> &'static str { + "gopls" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index ecc839fca6..cfb6a5dde9 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -37,6 +37,10 @@ impl LspAdapter for HtmlLspAdapter { LanguageServerName("vscode-html-language-server".into()) } + fn short_name(&self) -> &'static str { + "html" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 225cea0e92..f7e8f87492 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -43,6 +43,10 @@ impl LspAdapter for JsonLspAdapter { LanguageServerName("json-language-server".into()) } + fn short_name(&self) -> &'static str { + "json" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 7c5c7179d0..ee6d0f8579 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -29,6 +29,10 @@ impl super::LspAdapter for LuaLspAdapter { LanguageServerName("lua-language-server".into()) } + fn short_name(&self) -> &'static str { + "lua" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index 6a01d00300..73bb4b019c 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -41,6 +41,10 @@ impl LspAdapter for IntelephenseLspAdapter { LanguageServerName("intelephense".into()) } + fn short_name(&self) -> &'static str { + "php" + } + async fn fetch_latest_server_version( &self, _delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 41ad28ba86..023bbab13f 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -35,6 +35,10 @@ impl LspAdapter for PythonLspAdapter { LanguageServerName("pyright".into()) } + fn short_name(&self) -> &'static str { + "pyright" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs index 358441352a..3890b90dbd 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/zed/src/languages/ruby.rs @@ -12,6 +12,10 @@ impl LspAdapter for RubyLanguageServer { LanguageServerName("solargraph".into()) } + fn short_name(&self) -> &'static str { + "solargraph" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3c7f84fec7..bf8ad00293 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -22,6 +22,10 @@ impl LspAdapter for RustLspAdapter { LanguageServerName("rust-analyzer".into()) } + fn short_name(&self) -> &'static str { + "rust" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/svelte.rs b/crates/zed/src/languages/svelte.rs index 8416859f5a..35665e864f 100644 --- a/crates/zed/src/languages/svelte.rs +++ b/crates/zed/src/languages/svelte.rs @@ -36,6 +36,10 @@ impl LspAdapter for SvelteLspAdapter { LanguageServerName("svelte-language-server".into()) } + fn short_name(&self) -> &'static str { + "svelte" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 9a32f69e43..d7c11f0c73 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -41,6 +41,10 @@ impl LspAdapter for TailwindLspAdapter { LanguageServerName("tailwindcss-language-server".into()) } + fn short_name(&self) -> &'static str { + "tailwind" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index b7e4438e1f..e3bb0aae95 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -56,6 +56,10 @@ impl LspAdapter for TypeScriptLspAdapter { LanguageServerName("typescript-language-server".into()) } + fn short_name(&self) -> &'static str { + "tsserver" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -218,6 +222,10 @@ impl LspAdapter for EsLintLspAdapter { LanguageServerName("eslint".into()) } + fn short_name(&self) -> &'static str { + "eslint" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 48d7a3cf87..21155cc231 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -40,6 +40,10 @@ impl LspAdapter for YamlLspAdapter { LanguageServerName("yaml-language-server".into()) } + fn short_name(&self) -> &'static str { + "yaml" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, From 04354675ca2d2b9bedebcd4f2a5191289aaad9de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 10:35:23 -0700 Subject: [PATCH 065/156] Remove search dismiss button --- crates/search/src/buffer_search.rs | 9 +-------- crates/search/src/project_search.rs | 11 ----------- crates/search/src/search_bar.rs | 28 ---------------------------- 3 files changed, 1 insertion(+), 47 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e708781eca..01d4b7693f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -214,7 +214,7 @@ impl View for BufferSearchBar { let icon_style = theme.search.editor_icon.clone(); let nav_column = Flex::row() - .with_child(self.render_action_button("Select All", cx)) + .with_child(self.render_action_button("all", cx)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) .with_child(Flex::row().with_children(match_count)) @@ -268,13 +268,6 @@ impl View for BufferSearchBar { .contained() .with_style(theme.search.modes_container), ) - .with_child(super::search_bar::render_close_button( - "Dismiss Buffer Search", - &theme.search, - cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), - )) .constrained() .with_height(theme.search.search_bar_row_height) .aligned() diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6364183877..8542124a25 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1597,17 +1597,6 @@ impl View for ProjectSearchBar { .contained() .with_style(theme.search.modes_container), ) - .with_child(super::search_bar::render_close_button( - "Dismiss Project Search", - &theme.search, - cx, - |_, this, cx| { - if let Some(search) = this.active_project_search.as_mut() { - search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) - } - }, - None, - )) .constrained() .with_height(theme.search.search_bar_row_height) .aligned() diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 7d3c5261ea..4676b8f027 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -13,34 +13,6 @@ use crate::{ SelectNextMatch, SelectPrevMatch, }; -pub(super) fn render_close_button( - tooltip: &'static str, - theme: &theme::Search, - cx: &mut ViewContext, - on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - dismiss_action: Option>, -) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum CloseButton {} - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.search_bar_row_height) - }) - .on_click(MouseButton::Left, on_click) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::(0, tooltip.to_string(), dismiss_action, tooltip_style, cx) - .into_any() -} - pub(super) fn render_nav_button( icon: &'static str, direction: Direction, From 35b7787e02c1b544fd8cfb94551382543f69da11 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 28 Aug 2023 11:56:44 -0400 Subject: [PATCH 066/156] Add Tailwind server to TSX --- crates/zed/src/languages.rs | 2 +- crates/zed/src/languages/tailwind.rs | 2 ++ crates/zed/src/languages/tsx/config.toml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 8aaa11e1cd..f0b8a1444a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -104,6 +104,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( @@ -112,7 +113,6 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index d7c11f0c73..1b7c271d10 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -103,6 +103,7 @@ impl LspAdapter for TailwindLspAdapter { "html": "html", "css": "css", "javascript": "javascript", + "typescriptreact": "typescriptreact", }, })) } @@ -122,6 +123,7 @@ impl LspAdapter for TailwindLspAdapter { ("HTML".to_string(), "html".to_string()), ("CSS".to_string(), "css".to_string()), ("JavaScript".to_string(), "javascript".to_string()), + ("TSX".to_string(), "typescriptreact".to_string()), ] .into_iter(), ) diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index 234dc6b013..2f676f6710 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -16,3 +16,6 @@ brackets = [ [overrides.element] line_comment = { remove = true } block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] From 9521f6da42c30291660d6294b07ddb1349161d94 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:16:21 -0700 Subject: [PATCH 067/156] Simplify implementation of flex with spacing --- crates/gpui/src/elements/flex.rs | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index d43a152a64..80dfb0625c 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -88,8 +88,7 @@ impl Flex { cx: &mut LayoutContext, ) { let cross_axis = self.axis.invert(); - let last = self.children.len() - 1; - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { if let Some(metadata) = child.metadata::() { if let Some((flex, expanded)) = metadata.flex { if expanded != layout_expanded { @@ -101,10 +100,6 @@ impl Flex { } else { let space_per_flex = *remaining_space / *remaining_flex; space_per_flex * flex - } - if ix == 0 || ix == last { - self.spacing / 2. - } else { - self.spacing }; let child_min = if expanded { child_max } else { 0. }; let child_constraint = match self.axis { @@ -144,13 +139,12 @@ impl Element for Flex { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let mut total_flex = None; - let mut fixed_space = 0.0; + let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing; let mut contains_float = false; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; - let last = self.children.len().saturating_sub(1); - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { let metadata = child.metadata::(); contains_float |= metadata.map_or(false, |metadata| metadata.float); @@ -168,12 +162,7 @@ impl Element for Flex { ), }; let size = child.layout(child_constraint, view, cx); - fixed_space += size.along(self.axis) - + if ix == 0 || ix == last { - self.spacing / 2. - } else { - self.spacing - }; + fixed_space += size.along(self.axis); cross_axis_max = cross_axis_max.max(size.along(cross_axis)); } } @@ -333,8 +322,7 @@ impl Element for Flex { } } - let last = self.children.len().saturating_sub(1); - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -372,11 +360,9 @@ impl Element for Flex { child.paint(scene, aligned_child_origin, visible_bounds, view, cx); - let spacing = if ix == last { 0. } else { self.spacing }; - match self.axis { - Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0), - Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing), + Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing), } } From bb448b91d5404fdab1b86bda34d7ad68882b2001 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:16:41 -0700 Subject: [PATCH 068/156] Don't add a quick actions toolbar item for non-editor views Rather than adding primary toolbar item that renders as empty, don't add an item at all. This prevents spurious spacing from being added after other primary toolbar items. --- crates/quick_action_bar/src/quick_action_bar.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 1804c2b1fc..8595645e59 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -152,9 +152,10 @@ impl ToolbarItemView for QuickActionBar { cx.notify(); } })); + ToolbarItemLocation::PrimaryRight { flex: None } + } else { + ToolbarItemLocation::Hidden } - - ToolbarItemLocation::PrimaryRight { flex: None } } None => { self.active_item = None; From 78f9a1f280a3dc77f7f26941c77ec57319e7a073 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:18:30 -0700 Subject: [PATCH 069/156] Remove padding from workspace toolbar, increase its content height to compensate The padding makes it difficult to layout toolbar items correctly when they are more than one row tall. --- styles/src/style_tree/workspace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index ecfb572f7e..43a6cec585 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -129,7 +129,7 @@ export default function workspace(): any { status_bar: statusBar(), titlebar: titlebar(), toolbar: { - height: 34, + height: 42, background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, @@ -138,7 +138,7 @@ export default function workspace(): any { variant: "ghost", active_color: "accent", }), - padding: { left: 8, right: 8, top: 4, bottom: 4 }, + padding: { left: 8, right: 8 }, }, breadcrumb_height: 24, breadcrumbs: interactive({ From 3eee282a6b72dc15971ed8bfbe2f1c29ed8caa4e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:20:09 -0700 Subject: [PATCH 070/156] Overhaul search bar layout * Use a single row, instead of centering the search bar within a double-row toolbar. * Search query controls on the left, navigation on the right * Semantic is the final mode, for greater stability between buffer and project search. * Prevent query editor from moving when toggling path filters --- crates/search/src/buffer_search.rs | 74 +++++----- crates/search/src/mode.rs | 23 --- crates/search/src/project_search.rs | 216 ++++++++++++++-------------- crates/search/src/search_bar.rs | 27 ++-- crates/theme/src/theme.rs | 2 +- crates/workspace/src/toolbar.rs | 13 +- styles/src/style_tree/search.ts | 41 +----- 7 files changed, 164 insertions(+), 232 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 01d4b7693f..26ae86f375 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::{next_mode, SearchMode}, + mode::{next_mode, SearchMode, Side}, search_bar::{render_nav_button, render_search_mode_button}, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -156,11 +156,12 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = self.current_mode == mode; render_search_mode_button( mode, + side, is_active, move |_, this, cx| { this.activate_search_mode(mode, cx); @@ -212,20 +213,11 @@ impl View for BufferSearchBar { ) }; - let icon_style = theme.search.editor_icon.clone(); - let nav_column = Flex::row() - .with_child(self.render_action_button("all", cx)) - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(match_count)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let query = Flex::row() + let query_column = Flex::row() .with_child( - Svg::for_style(icon_style.icon) + Svg::for_style(theme.search.editor_icon.clone().icon) .contained() - .with_style(icon_style.container), + .with_style(theme.search.editor_icon.clone().container), ) .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child( @@ -244,42 +236,45 @@ impl View for BufferSearchBar { .contained(), ) .align_children_center() - .flex(1., true); - let editor_column = Flex::row() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) .contained() + .with_style(query_container_style) .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) .with_height(theme.search.search_bar_row_height) .flex(1., false); + let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + Some(Side::Right), + cx, + )) + .contained() + .with_style(theme.search.modes_container) + .constrained() + .with_height(theme.search.search_bar_row_height); + + let nav_column = Flex::row() + .with_child(Flex::row().with_children(match_count)) + .with_child(self.render_action_button("all", cx)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() .flex_float(); + Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) - .aligned() .into_any_named("search bar") } } @@ -333,8 +328,9 @@ impl ToolbarItemView for BufferSearchBar { ToolbarItemLocation::Hidden } } + fn row_count(&self, _: &ViewContext) -> usize { - 2 + 1 } } diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 2c180be761..56d6dfa024 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -48,29 +48,6 @@ impl SearchMode { SearchMode::Regex => Box::new(ActivateRegexMode), } } - - pub(crate) fn border_right(&self) -> bool { - match self { - SearchMode::Regex => true, - SearchMode::Text => true, - SearchMode::Semantic => true, - } - } - - pub(crate) fn border_left(&self) -> bool { - match self { - SearchMode::Text => true, - _ => false, - } - } - - pub(crate) fn button_side(&self) -> Option { - match self { - SearchMode::Text => Some(Side::Left), - SearchMode::Semantic => None, - SearchMode::Regex => Some(Side::Right), - } - } } pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8542124a25..c2ecde4ce5 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::SearchMode, + mode::{SearchMode, Side}, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -1424,8 +1424,13 @@ impl View for ProjectSearchBar { }, cx, ); + let search = _search.read(cx); + let is_semantic_available = SemanticIndex::enabled(cx); let is_semantic_disabled = search.semantic_state.is_none(); + let icon_style = theme.search.editor_icon.clone(); + let is_active = search.active_match_index.is_some(); + let render_option_button_icon = |path, option, cx: &mut ViewContext| { crate::search_bar::render_option_button_icon( self.is_option_enabled(option, cx), @@ -1451,28 +1456,23 @@ impl View for ProjectSearchBar { render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) }); - let search = _search.read(cx); - let icon_style = theme.search.editor_icon.clone(); - - // Editor Functionality - let query = Flex::row() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container), + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + side, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, ) - .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) - .with_child( - Flex::row() - .with_child(filter_button) - .with_children(case_sensitive) - .with_children(whole_word) - .flex(1., false) - .constrained() - .contained(), - ) - .align_children_center() - .flex(1., true); + }; let search = _search.read(cx); @@ -1490,50 +1490,6 @@ impl View for ProjectSearchBar { theme.search.include_exclude_editor.input.container }; - let included_files_view = ChildView::new(&search.included_files_editor, cx) - .contained() - .flex(1., true); - let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) - .contained() - .flex(1., true); - let filters = search.filters_enabled.then(|| { - Flex::row() - .with_child( - included_files_view - .contained() - .with_style(include_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .with_child( - excluded_files_view - .contained() - .with_style(exclude_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .contained() - .with_padding_top(theme.workspace.toolbar.container.padding.bottom) - }); - - let editor_column = Flex::column() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) - .with_children(filters) - .flex(1., false); - let matches = search.active_match_index.map(|match_ix| { Label::new( format!( @@ -1548,25 +1504,81 @@ impl View for ProjectSearchBar { .aligned() }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == mode - } else { - false - }; - render_search_mode_button( - mode, - is_active, - move |_, this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, + let query_column = Flex::column() + .with_spacing(theme.search.search_row_spacing) + .with_child( + Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) + .with_child( + Flex::row() + .with_child(filter_button) + .with_children(case_sensitive) + .with_children(whole_word) + .flex(1., false) + .constrained() + .contained(), + ) + .align_children_center() + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false), ) - }; - let is_active = search.active_match_index.is_some(); - let semantic_index = SemanticIndex::enabled(cx) - .then(|| search_button_for_mode(SearchMode::Semantic, cx)); + .with_children(search.filters_enabled.then(|| { + Flex::row() + .with_child( + ChildView::new(&search.included_files_editor, cx) + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .with_child( + ChildView::new(&search.excluded_files_editor, cx) + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false) + })) + .flex(1., false); + + let mode_column = + Flex::row() + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + if is_semantic_available { + None + } else { + Some(Side::Right) + }, + cx, + )) + .with_children(is_semantic_available.then(|| { + search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + })) + .contained() + .with_style(theme.search.modes_container); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { render_nav_button( label, @@ -1582,32 +1594,17 @@ impl View for ProjectSearchBar { }; let nav_column = Flex::row() + .with_child(Flex::row().with_children(matches)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(matches)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() - .top() .flex_float(); Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) .into_any_named("project search") @@ -1636,7 +1633,7 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); ToolbarItemLocation::PrimaryLeft { - flex: Some((1., false)), + flex: Some((1., true)), } } else { ToolbarItemLocation::Hidden @@ -1644,13 +1641,12 @@ impl ToolbarItemView for ProjectSearchBar { } fn row_count(&self, cx: &ViewContext) -> usize { - self.active_project_search - .as_ref() - .map(|search| { - let offset = search.read(cx).filters_enabled as usize; - 2 + offset - }) - .unwrap_or_else(|| 2) + if let Some(search) = self.active_project_search.as_ref() { + if search.read(cx).filters_enabled { + return 2; + } + } + 1 } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 4676b8f027..d1a5a0380a 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -83,6 +83,7 @@ pub(super) fn render_nav_button( pub(crate) fn render_search_mode_button( mode: SearchMode, + side: Option, is_active: bool, on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, cx: &mut ViewContext, @@ -91,41 +92,41 @@ pub(crate) fn render_search_mode_button( enum SearchModeButton {} MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { let theme = theme::current(cx); - let mut style = theme + let style = theme .search .mode_button .in_state(is_active) .style_for(state) .clone(); - style.container.border.left = mode.border_left(); - style.container.border.right = mode.border_right(); - let label = Label::new(mode.label(), style.text.clone()) - .aligned() - .contained(); - let mut container_style = style.container.clone(); - if let Some(button_side) = mode.button_side() { + let mut container_style = style.container; + if let Some(button_side) = side { if button_side == Side::Left { + container_style.border.left = true; container_style.corner_radii = CornerRadii { bottom_right: 0., top_right: 0., ..container_style.corner_radii }; - label.with_style(container_style) } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii { bottom_left: 0., top_left: 0., ..container_style.corner_radii }; - label.with_style(container_style) } } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii::default(); - label.with_style(container_style) } - .constrained() - .with_height(theme.search.search_bar_row_height) + + Label::new(mode.label(), style.text) + .aligned() + .contained() + .with_style(container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9005fc9757..a5faba8eaf 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -437,11 +437,11 @@ pub struct Search { pub match_index: ContainedText, pub major_results_status: TextStyle, pub minor_results_status: TextStyle, - pub dismiss_button: Interactive, pub editor_icon: IconStyle, pub mode_button: Toggleable>, pub nav_button: Toggleable>, pub search_bar_row_height: f32, + pub search_row_spacing: f32, pub option_button_height: f32, pub modes_container: ContainerStyle, } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 72c879d6d4..c3f4bb9723 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -81,10 +81,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryLeft { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let left_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_right(spacing); + let left_item = ChildView::new(item.as_any(), cx).aligned(); if let Some((flex, expanded)) = flex { primary_left_items.push(left_item.flex(flex, expanded).into_any()); } else { @@ -94,11 +91,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryRight { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let right_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_left(spacing) - .flex_float(); + let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); if let Some((flex, expanded)) = flex { primary_right_items.push(right_item.flex(flex, expanded).into_any()); } else { @@ -120,7 +113,7 @@ impl View for Toolbar { let container_style = theme.container; let height = theme.height * primary_items_row_count as f32; - let mut primary_items = Flex::row(); + let mut primary_items = Flex::row().with_spacing(spacing); primary_items.extend(primary_left_items); primary_items.extend(primary_right_items); diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 4493634a8e..27e8c43a4d 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -34,7 +34,7 @@ export default function search(): any { } return { - padding: { top: 16, bottom: 16, left: 16, right: 16 }, + padding: { top: 4, bottom: 4 }, // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive match_background: with_opacity( foreground(theme.highest, "accent"), @@ -210,6 +210,7 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), padding: { left: 9, + right: 9, }, }, option_button_group: { @@ -232,34 +233,6 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), size: 13, }, - dismiss_button: interactive({ - base: { - color: foreground(theme.highest, "variant"), - icon_width: 14, - button_width: 32, - corner_radius: 6, - padding: { - // // top: 10, - // bottom: 10, - left: 10, - right: 10, - }, - - background: background(theme.highest, "variant"), - - border: border(theme.highest, "on"), - }, - state: { - hovered: { - color: foreground(theme.highest, "hovered"), - background: background(theme.highest, "variant", "hovered") - }, - clicked: { - color: foreground(theme.highest, "pressed"), - background: background(theme.highest, "variant", "pressed") - }, - }, - }), editor_icon: { icon: { color: foreground(theme.highest, "variant"), @@ -375,13 +348,9 @@ export default function search(): any { }) } }), - search_bar_row_height: 32, + search_bar_row_height: 34, + search_row_spacing: 8, option_button_height: 22, - modes_container: { - margin: { - right: 9 - } - } - + modes_container: {} } } From 70bea758979f40c6cff2ed01f75e29a1bade579e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 15:15:54 -0700 Subject: [PATCH 071/156] Change cycle mode action to reflect new mode button order --- crates/search/src/mode.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 56d6dfa024..8afc2bd3f4 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -51,15 +51,15 @@ impl SearchMode { } pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { - let next_text_state = if semantic_enabled { - SearchMode::Semantic - } else { - SearchMode::Regex - }; - match mode { - SearchMode::Text => next_text_state, - SearchMode::Semantic => SearchMode::Regex, - SearchMode::Regex => SearchMode::Text, + SearchMode::Text => SearchMode::Regex, + SearchMode::Regex => { + if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Text + } + } + SearchMode::Semantic => SearchMode::Text, } } From c2b60df5afaeb624b21e89ec3a61b9b794331840 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Aug 2023 16:36:07 +0200 Subject: [PATCH 072/156] Allow including conversation when triggering inline assist --- crates/ai/src/assistant.rs | 156 ++++++++++++++++++++++++----- crates/theme/src/theme.rs | 1 + styles/src/style_tree/assistant.ts | 58 ++++++++++- 3 files changed, 189 insertions(+), 26 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 80c3771085..ae223fdb57 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,12 +19,16 @@ use fs::Fs; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{ actions, - elements::*, + elements::{ + ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable, + Stack, Svg, Text, UniformList, UniformListState, + }, fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton}, - Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, + ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; use language::{ language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _, @@ -33,7 +37,7 @@ use language::{ use search::BufferSearchBar; use settings::SettingsStore; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, cmp, env, fmt::Write, iter, @@ -43,7 +47,10 @@ use std::{ sync::Arc, time::Duration, }; -use theme::AssistantStyle; +use theme::{ + components::{action_button::Button, ComponentExt}, + AssistantStyle, +}; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -61,7 +68,8 @@ actions!( QuoteSelection, ToggleFocus, ResetKey, - InlineAssist + InlineAssist, + ToggleIncludeConversation, ] ); @@ -97,6 +105,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(AssistantPanel::cancel_last_inline_assist); cx.add_action(InlineAssistant::confirm); cx.add_action(InlineAssistant::cancel); + cx.add_action(InlineAssistant::toggle_include_conversation); } #[derive(Debug)] @@ -129,6 +138,7 @@ pub struct AssistantPanel { next_inline_assist_id: usize, pending_inline_assists: HashMap, pending_inline_assist_ids_by_editor: HashMap, Vec>, + include_conversation_in_next_inline_assist: bool, _watch_saved_conversations: Task>, } @@ -195,6 +205,7 @@ impl AssistantPanel { next_inline_assist_id: 0, pending_inline_assists: Default::default(), pending_inline_assist_ids_by_editor: Default::default(), + include_conversation_in_next_inline_assist: false, _watch_saved_conversations, }; @@ -270,12 +281,15 @@ impl AssistantPanel { editor.set_placeholder_text(placeholder, cx); editor }); + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant { id: inline_assist_id, prompt_editor, confirmed: false, has_focus: false, + include_conversation: self.include_conversation_in_next_inline_assist, + measurements: measurements.clone(), }; cx.focus_self(); assistant @@ -292,13 +306,11 @@ impl AssistantPanel { render: Arc::new({ let inline_assistant = inline_assistant.clone(); move |cx: &mut BlockContext| { - let theme = theme::current(cx); - ChildView::new(&inline_assistant, cx) - .contained() - .with_padding_left(cx.anchor_x) - .contained() - .with_style(theme.assistant.inline.container) - .into_any() + measurements.set(BlockMeasurements { + anchor_x: cx.anchor_x, + gutter_width: cx.gutter_width, + }); + ChildView::new(&inline_assistant, cx).into_any() } }), disposition: if selection.reversed { @@ -375,8 +387,11 @@ impl AssistantPanel { ) { let assist_id = inline_assistant.read(cx).id; match event { - InlineAssistantEvent::Confirmed { prompt } => { - self.confirm_inline_assist(assist_id, prompt, cx); + InlineAssistantEvent::Confirmed { + prompt, + include_conversation, + } => { + self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx); } InlineAssistantEvent::Canceled => { self.close_inline_assist(assist_id, true, cx); @@ -470,14 +485,24 @@ impl AssistantPanel { &mut self, inline_assist_id: usize, user_prompt: &str, + include_conversation: bool, cx: &mut ViewContext, ) { + self.include_conversation_in_next_inline_assist = include_conversation; + let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { return; }; + let conversation = if include_conversation { + self.active_editor() + .map(|editor| editor.read(cx).conversation.clone()) + } else { + None + }; + let pending_assist = if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { pending_assist @@ -626,14 +651,25 @@ impl AssistantPanel { ) .unwrap(); - let request = OpenAIRequest { + let mut request = OpenAIRequest { model: model.full_name().into(), - messages: vec![RequestMessage { - role: Role::User, - content: prompt, - }], + messages: Vec::new(), stream: true, }; + if let Some(conversation) = conversation { + let conversation = conversation.read(cx); + let buffer = conversation.buffer.read(cx); + request.messages.extend( + conversation + .messages(cx) + .map(|message| message.to_open_ai_message(buffer)), + ); + } + + request.messages.push(RequestMessage { + role: Role::User, + content: prompt, + }); let response = stream_completion(api_key, cx.background().clone(), request); let editor = editor.downgrade(); @@ -2799,7 +2835,10 @@ impl Message { } enum InlineAssistantEvent { - Confirmed { prompt: String }, + Confirmed { + prompt: String, + include_conversation: bool, + }, Canceled, Dismissed, } @@ -2815,6 +2854,8 @@ struct InlineAssistant { prompt_editor: ViewHandle, confirmed: bool, has_focus: bool, + include_conversation: bool, + measurements: Rc>, } impl Entity for InlineAssistant { @@ -2827,9 +2868,55 @@ impl View for InlineAssistant { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() + let theme = theme::current(cx); + + Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned() + .constrained() + .dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f(measurements.gutter_width, constraint.min.y()), + max: vec2f(measurements.gutter_width, constraint.max.y()), + } + } + }), + ) + .with_child(Empty::new().constrained().dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.min.y(), + ), + max: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.max.y(), + ), + } + } + })) + .with_child( + ChildView::new(&self.prompt_editor, cx) + .aligned() + .left() + .flex(1., true), + ) + .contained() + .with_style(theme.assistant.inline.container) + .into_any() .into_any() } @@ -2862,10 +2949,29 @@ impl InlineAssistant { cx, ); }); - cx.emit(InlineAssistantEvent::Confirmed { prompt }); + cx.emit(InlineAssistantEvent::Confirmed { + prompt, + include_conversation: self.include_conversation, + }); self.confirmed = true; } } + + fn toggle_include_conversation( + &mut self, + _: &ToggleIncludeConversation, + cx: &mut ViewContext, + ) { + self.include_conversation = !self.include_conversation; + cx.notify(); + } +} + +// This wouldn't need to exist if we could pass parameters when rendering child views. +#[derive(Copy, Clone, Default)] +struct BlockMeasurements { + anchor_x: f32, + gutter_width: f32, } struct PendingInlineAssist { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 7913685b7a..261933f057 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1160,6 +1160,7 @@ pub struct InlineAssistantStyle { pub editor: FieldEditor, pub disabled_editor: FieldEditor, pub pending_edit_background: Color, + pub include_conversation: ToggleIconButtonStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 8bef2ce16b..e660bf078f 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -1,5 +1,5 @@ import { text, border, background, foreground, TextStyle } from "./components" -import { Interactive, interactive } from "../element" +import { Interactive, interactive, toggleable } from "../element" import { tab_bar_button } from "../component/tab_bar_button" import { StyleSets, useTheme } from "../theme" @@ -80,6 +80,62 @@ export default function assistant(): any { }, }, pending_edit_background: background(theme.highest, "positive"), + include_conversation: toggleable({ + base: interactive({ + base: { + icon_size: 12, + color: foreground(theme.highest, "variant"), + + button_width: 12, + background: background(theme.highest, "on"), + corner_radius: 2, + border: { + width: 1., color: background(theme.highest, "on") + }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: { + width: 1., color: background(theme.highest, "on", "hovered") + }, + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: { + width: 1., color: background(theme.highest, "on", "pressed") + }, + }, + }, + }), + state: { + active: { + default: { + icon_size: 12, + button_width: 12, + color: foreground(theme.highest, "variant"), + background: background(theme.highest, "accent"), + border: border(theme.highest, "accent"), + }, + hovered: { + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "accent", "hovered"), + }, + clicked: { + background: background(theme.highest, "accent", "pressed"), + border: border(theme.highest, "accent", "pressed"), + }, + }, + }, + }), }, message_header: { margin: { bottom: 4, top: 4 }, From 08df24412a4f4c04572696699b0f495caf269c21 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 14:31:58 +0200 Subject: [PATCH 073/156] Delete less aggressively --- crates/ai/src/streaming_diff.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/streaming_diff.rs b/crates/ai/src/streaming_diff.rs index 7ea7f6dacd..7399a7b4fa 100644 --- a/crates/ai/src/streaming_diff.rs +++ b/crates/ai/src/streaming_diff.rs @@ -82,9 +82,9 @@ pub struct StreamingDiff { impl StreamingDiff { const INSERTION_SCORE: f64 = -1.; - const DELETION_SCORE: f64 = -5.; - const EQUALITY_BASE: f64 = 2.; - const MAX_EQUALITY_EXPONENT: i32 = 20; + const DELETION_SCORE: f64 = -20.; + const EQUALITY_BASE: f64 = 1.8; + const MAX_EQUALITY_EXPONENT: i32 = 16; pub fn new(old: String) -> Self { let old = old.chars().collect::>(); From 2332f824421875ce6db4c24a27f5df4755cce3a5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 14:41:02 +0200 Subject: [PATCH 074/156] More polish --- crates/ai/src/assistant.rs | 61 ++++++++++++++++++++---------- styles/src/style_tree/assistant.ts | 1 + 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ae223fdb57..62ff7212bf 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -567,24 +567,37 @@ impl AssistantPanel { } } - let language_name = snapshot - .language_at(range.start) - .map(|language| language.name()); - let language_name = language_name.as_deref().unwrap_or(""); + let language = snapshot.language_at(range.start); + let language_name = if let Some(language) = language.as_ref() { + if Arc::ptr_eq(language, &language::PLAIN_TEXT) { + None + } else { + Some(language.name()) + } + } else { + None + }; + let language_name = language_name.as_deref(); let model = settings::get::(cx) .default_open_ai_model .clone(); let mut prompt = String::new(); - writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); + } match pending_assist.kind { InlineAssistKind::Transform => { writeln!( prompt, - "You're currently working inside an editor on this code:" + "You're currently working inside an editor on this file:" ) .unwrap(); - writeln!(prompt, "```{language_name}").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { write!(prompt, "{chunk}").unwrap(); } @@ -592,31 +605,39 @@ impl AssistantPanel { writeln!( prompt, - "In particular, the user has selected the following code:" + "In particular, the user has selected the following text:" ) .unwrap(); - writeln!(prompt, "```{language_name}").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } writeln!(prompt, "{normalized_selected_text}").unwrap(); writeln!(prompt, "```").unwrap(); writeln!(prompt).unwrap(); writeln!( prompt, - "Modify the selected code given the user prompt: {user_prompt}" + "Modify the selected text given the user prompt: {user_prompt}" ) .unwrap(); writeln!( prompt, - "You MUST reply only with the edited selected code, not the entire file." + "You MUST reply only with the edited selected text, not the entire file." ) .unwrap(); } InlineAssistKind::Generate => { writeln!( prompt, - "You're currently working inside an editor on this code:" + "You're currently working inside an editor on this file:" ) .unwrap(); - writeln!(prompt, "```{language_name}").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } for chunk in snapshot.text_for_range(Anchor::min()..range.start) { write!(prompt, "{chunk}").unwrap(); } @@ -633,23 +654,21 @@ impl AssistantPanel { .unwrap(); writeln!( prompt, - "Code can't be replaced, so assume your answer will be inserted at the cursor." + "Text can't be replaced, so assume your answer will be inserted at the cursor." ) .unwrap(); writeln!( prompt, - "Complete the code given the user prompt: {user_prompt}" + "Complete the text given the user prompt: {user_prompt}" ) .unwrap(); } } - writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); + } writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!( - prompt, - "Never make remarks about the output, always output just code." - ) - .unwrap(); + writeln!(prompt, "Never make remarks about the output.").unwrap(); let mut request = OpenAIRequest { model: model.full_name().into(), diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index e660bf078f..4a33ef9b19 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -60,6 +60,7 @@ export default function assistant(): any { padding: { left: 12 }, }, inline: { + background: background(theme.highest), margin: { top: 3, bottom: 3 }, border: border(theme.lowest, "on", { top: true, From 72413dbaf235ddd9f332e4e7cbd9569936f32932 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 14:51:00 +0200 Subject: [PATCH 075/156] Remove the ability to reply to specific message in assistant --- crates/ai/src/assistant.rs | 255 +++++++++++++++++-------------------- 1 file changed, 114 insertions(+), 141 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 62ff7212bf..ab60d108f0 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1767,15 +1767,20 @@ impl Conversation { cx: &mut ModelContext, ) -> Vec { let mut user_messages = Vec::new(); - let mut tasks = Vec::new(); - let last_message_id = self.message_anchors.iter().rev().find_map(|message| { - message - .start - .is_valid(self.buffer.read(cx)) - .then_some(message.id) - }); + let last_message_id = if let Some(last_message_id) = + self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + }) { + last_message_id + } else { + return Default::default(); + }; + let mut should_assist = false; for selected_message_id in selected_messages { let selected_message_role = if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { @@ -1792,144 +1797,111 @@ impl Conversation { cx, ) { user_messages.push(user_message); - } else { - continue; } } else { - let request = OpenAIRequest { - model: self.model.full_name().to_string(), - messages: self - .messages(cx) - .filter(|message| matches!(message.status, MessageStatus::Done)) - .flat_map(|message| { - let mut system_message = None; - if message.id == selected_message_id { - system_message = Some(RequestMessage { - role: Role::System, - content: concat!( - "Treat the following messages as additional knowledge you have learned about, ", - "but act as if they were not part of this conversation. That is, treat them ", - "as if the user didn't see them and couldn't possibly inquire about them." - ).into() - }); - } - - Some(message.to_open_ai_message(self.buffer.read(cx))).into_iter().chain(system_message) - }) - .chain(Some(RequestMessage { - role: Role::System, - content: format!( - "Direct your reply to message with id {}. Do not include a [Message X] header.", - selected_message_id.0 - ), - })) - .collect(), - stream: true, - }; - - let Some(api_key) = self.api_key.borrow().clone() else { - continue; - }; - let stream = stream_completion(api_key, cx.background().clone(), request); - let assistant_message = self - .insert_message_after( - selected_message_id, - Role::Assistant, - MessageStatus::Pending, - cx, - ) - .unwrap(); - - // Queue up the user's next reply - if Some(selected_message_id) == last_message_id { - let user_message = self - .insert_message_after( - assistant_message.id, - Role::User, - MessageStatus::Done, - cx, - ) - .unwrap(); - user_messages.push(user_message); - } - - tasks.push(cx.spawn_weak({ - |this, mut cx| async move { - let assistant_message_id = assistant_message.id; - let stream_completion = async { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - let text: Arc = choice.delta.content?.into(); - let message_ix = this.message_anchors.iter().position( - |message| message.id == assistant_message_id, - )?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message - .start - .to_offset(buffer) - .saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) - }); - } - smol::future::yield_now().await; - } - - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions.retain(|completion| { - completion.id != this.completion_count - }); - this.summarize(cx); - }); - - anyhow::Ok(()) - }; - - let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = MessageStatus::Error( - error.to_string().trim().into(), - ); - } - } - cx.notify(); - } - }); - } - } - })); + should_assist = true; } } - if !tasks.is_empty() { + if should_assist { + let Some(api_key) = self.api_key.borrow().clone() else { + return Default::default(); + }; + + let request = OpenAIRequest { + model: self.model.full_name().to_string(), + messages: self + .messages(cx) + .filter(|message| matches!(message.status, MessageStatus::Done)) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .collect(), + stream: true, + }; + + let stream = stream_completion(api_key, cx.background().clone(), request); + let assistant_message = self + .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) + .unwrap(); + + // Queue up the user's next reply. + let user_message = self + .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) + .unwrap(); + user_messages.push(user_message); + + let task = cx.spawn_weak({ + |this, mut cx| async move { + let assistant_message_id = assistant_message.id; + let stream_completion = async { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + let text: Arc = choice.delta.content?.into(); + let message_ix = + this.message_anchors.iter().position(|message| { + message.id == assistant_message_id + })?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] + .iter() + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message + .start + .to_offset(buffer) + .saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); + }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + }); + } + smol::future::yield_now().await; + } + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + }); + + anyhow::Ok(()) + }; + + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = + MessageStatus::Error(error.to_string().trim().into()); + } + } + cx.notify(); + } + }); + } + } + }); + self.pending_completions.push(PendingCompletion { id: post_inc(&mut self.completion_count), - _tasks: tasks, + _task: task, }); } @@ -2296,7 +2268,7 @@ impl Conversation { struct PendingCompletion { id: usize, - _tasks: Vec>, + _task: Task<()>, } enum ConversationEditorEvent { @@ -2844,8 +2816,9 @@ pub struct Message { impl Message { fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { - let mut content = format!("[Message {}]\n", self.id.0).to_string(); - content.extend(buffer.text_for_range(self.offset_range.clone())); + let content = buffer + .text_for_range(self.offset_range.clone()) + .collect::(); RequestMessage { role: self.role, content: content.trim_end().into(), From 15628af04b727d6729f20eceac7a8ce2ca0685e1 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 29 Aug 2023 11:21:02 -0400 Subject: [PATCH 076/156] Style language server name in completion menu Omit in buffers with one or zero running language servers with the capability to provide completions Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 118 ++++++++++++++++++++++++-------- crates/theme/src/theme.rs | 3 + styles/src/style_tree/editor.ts | 3 + 3 files changed, 96 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a188a47e35..ab2be13a25 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -44,7 +44,7 @@ use gpui::{ elements::*, executor, fonts::{self, HighlightStyle, TextStyle}, - geometry::vector::Vector2F, + geometry::vector::{vec2f, Vector2F}, impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, @@ -858,21 +858,43 @@ impl CompletionsMenu { project .read(cx) .language_servers_for_buffer(self.buffer.read(cx), cx) - .map(|(adapter, server)| (server.server_id(), format!("{}: ", adapter.short_name))) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .map(|(adapter, server)| (server.server_id(), adapter.short_name)) .collect::>() }); - let get_server_name = move |lookup_server_id: lsp::LanguageServerId| -> Option { - language_servers - .iter() - .flatten() - .find_map(|(server_id, server_name)| { - if *server_id == lookup_server_id { - Some(server_name.clone()) - } else { - None - } - }) - }; + let needs_server_name = language_servers + .as_ref() + .map_or(false, |servers| servers.len() > 1); + + let get_server_name = + move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> { + language_servers + .iter() + .flatten() + .find_map(|(server_id, server_name)| { + if *server_id == lookup_server_id { + Some(*server_name) + } else { + None + } + }) + }; + + let widest_completion_ix = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completion = &self.completions[mat.candidate_id]; + let mut len = completion.label.text.chars().count(); + + if let Some(server_name) = get_server_name(completion.server_id) { + len += server_name.chars().count(); + } + + len + }) + .map(|(ix, _)| ix); let completions = self.completions.clone(); let matches = self.matches.clone(); @@ -917,14 +939,66 @@ impl CompletionsMenu { if let Some(server_name) = get_server_name(completion.server_id) { Flex::row() - .with_child(Text::new(server_name, style.text.clone())) .with_child(completion_label) + .with_children((|| { + if !needs_server_name { + return None; + } + + let text_style = TextStyle { + color: style.autocomplete.server_name_color, + font_size: style.text.font_size + * style.autocomplete.server_name_size_percent, + ..style.text.clone() + }; + + let label = Text::new(server_name, text_style) + .aligned() + .constrained() + .dynamically(move |constraint, _, _| { + gpui::SizeConstraint { + min: constraint.min, + max: vec2f( + constraint.max.x(), + constraint.min.y(), + ), + } + }); + + if Some(item_ix) == widest_completion_ix { + Some( + label + .contained() + .with_style( + style + .autocomplete + .server_name_container, + ) + .into_any(), + ) + } else { + Some(label.flex_float().into_any()) + } + })()) .into_any() } else { completion_label.into_any() } .contained() .with_style(item_style) + .constrained() + .dynamically( + move |constraint, _, _| { + if Some(item_ix) == widest_completion_ix { + constraint + } else { + gpui::SizeConstraint { + min: constraint.min, + max: constraint.min, + } + } + }, + ) }, ) .with_cursor_style(CursorStyle::PointingHand) @@ -941,19 +1015,7 @@ impl CompletionsMenu { } }, ) - .with_width_from_item( - self.matches - .iter() - .enumerate() - .max_by_key(|(_, mat)| { - self.completions[mat.candidate_id] - .label - .text - .chars() - .count() - }) - .map(|(ix, _)| ix), - ) + .with_width_from_item(widest_completion_ix) .contained() .with_style(container_style) .into_any() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4766f636f3..d692660738 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -775,6 +775,9 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, + pub server_name_container: ContainerStyle, + pub server_name_color: Color, + pub server_name_size_percent: f32, } #[derive(Clone, Copy, Default, Deserialize, JsonSchema)] diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9ad008f38d..0b99b6fba6 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -205,6 +205,9 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, + server_name_container: { padding: { left: 40 } }, + server_name_color: text(theme.middle, "sans", "disabled", {}).color, + server_name_size_percent: 0.75, }, diagnostic_header: { background: background(theme.middle), From df377d5195f980e87cd8dada82ab1db4adb76532 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 17:32:23 +0200 Subject: [PATCH 077/156] Use Inline Assist across the board --- crates/quick_action_bar/src/quick_action_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index da5a8e6d72..de4e8828b3 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -96,7 +96,7 @@ impl View for QuickActionBar { 2, "icons/radix/magic-wand.svg", false, - ("Generate code".into(), Some(Box::new(InlineAssist))), + ("Inline Assist".into(), Some(Box::new(InlineAssist))), cx, move |this, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { From 2d411303bb3b034e06bed2d4bba4b1ce275736da Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 29 Aug 2023 10:07:22 -0600 Subject: [PATCH 078/156] Use preview server when not on stable --- crates/client/src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index a32c415f7e..d28c1ab1a9 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1011,9 +1011,9 @@ impl Client { credentials: &Credentials, cx: &AsyncAppContext, ) -> Task> { - let is_preview = cx.read(|cx| { + let use_preview_server = cx.read(|cx| { if cx.has_global::() { - *cx.global::() == ReleaseChannel::Preview + *cx.global::() != ReleaseChannel::Stable } else { false } @@ -1028,7 +1028,7 @@ impl Client { let http = self.http.clone(); cx.background().spawn(async move { - let mut rpc_url = Self::get_rpc_url(http, is_preview).await?; + let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?; let rpc_host = rpc_url .host_str() .zip(rpc_url.port_or_known_default()) From 16422a06ad96909160b5eae36c1c771fce4c45d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 18:21:23 +0200 Subject: [PATCH 079/156] Remember whether include conversation was toggled --- crates/ai/src/assistant.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ab60d108f0..4aca6ae626 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -399,6 +399,11 @@ impl AssistantPanel { InlineAssistantEvent::Dismissed => { self.hide_inline_assist(assist_id, cx); } + InlineAssistantEvent::IncludeConversationToggled { + include_conversation, + } => { + self.include_conversation_in_next_inline_assist = *include_conversation; + } } } @@ -488,8 +493,6 @@ impl AssistantPanel { include_conversation: bool, cx: &mut ViewContext, ) { - self.include_conversation_in_next_inline_assist = include_conversation; - let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { @@ -2833,6 +2836,9 @@ enum InlineAssistantEvent { }, Canceled, Dismissed, + IncludeConversationToggled { + include_conversation: bool, + }, } #[derive(Copy, Clone)] @@ -2955,6 +2961,9 @@ impl InlineAssistant { cx: &mut ViewContext, ) { self.include_conversation = !self.include_conversation; + cx.emit(InlineAssistantEvent::IncludeConversationToggled { + include_conversation: self.include_conversation, + }); cx.notify(); } } From 87e25c8c238b8d4f63431452c057a0a387f6d8ee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Aug 2023 18:21:35 +0200 Subject: [PATCH 080/156] Use model from conversation when available --- crates/ai/src/assistant.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4aca6ae626..46756ad569 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -581,9 +581,6 @@ impl AssistantPanel { None }; let language_name = language_name.as_deref(); - let model = settings::get::(cx) - .default_open_ai_model - .clone(); let mut prompt = String::new(); if let Some(language_name) = language_name { @@ -673,25 +670,30 @@ impl AssistantPanel { writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); writeln!(prompt, "Never make remarks about the output.").unwrap(); - let mut request = OpenAIRequest { - model: model.full_name().into(), - messages: Vec::new(), - stream: true, - }; + let mut messages = Vec::new(); + let mut model = settings::get::(cx) + .default_open_ai_model + .clone(); if let Some(conversation) = conversation { let conversation = conversation.read(cx); let buffer = conversation.buffer.read(cx); - request.messages.extend( + messages.extend( conversation .messages(cx) .map(|message| message.to_open_ai_message(buffer)), ); + model = conversation.model.clone(); } - request.messages.push(RequestMessage { + messages.push(RequestMessage { role: Role::User, content: prompt, }); + let request = OpenAIRequest { + model: model.full_name().into(), + messages, + stream: true, + }; let response = stream_completion(api_key, cx.background().clone(), request); let editor = editor.downgrade(); From 53558bc603f0c24883e8e3d95ed4a48edad6e249 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:05:59 -0400 Subject: [PATCH 081/156] Remove baseurl to prevent theme import issue --- styles/tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/styles/tsconfig.json b/styles/tsconfig.json index 281bd74b21..c7eaa50eed 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -21,8 +21,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "skipLibCheck": true, - "useUnknownInCatchVariables": false, - "baseUrl": "." + "useUnknownInCatchVariables": false }, "exclude": [ "node_modules" From a5b12d535fec69cb3282c7be8385b608c53ba7e3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:06:13 -0400 Subject: [PATCH 082/156] Add margin and padding functions --- styles/src/component/margin.ts | 34 +++++++++++++++++++++++++++++++++ styles/src/component/padding.ts | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 styles/src/component/margin.ts create mode 100644 styles/src/component/padding.ts diff --git a/styles/src/component/margin.ts b/styles/src/component/margin.ts new file mode 100644 index 0000000000..f6262405f0 --- /dev/null +++ b/styles/src/component/margin.ts @@ -0,0 +1,34 @@ +type MarginOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type MarginStyle = { + top: number + bottom: number + left: number + right: number +} + +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 (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 + } +} diff --git a/styles/src/component/padding.ts b/styles/src/component/padding.ts new file mode 100644 index 0000000000..96792bf766 --- /dev/null +++ b/styles/src/component/padding.ts @@ -0,0 +1,34 @@ +type PaddingOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type PaddingStyle = { + top: number + bottom: number + left: number + right: number +} + +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 (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 + } +} From 05da4b740a05762fa8181905ac544088caa304d1 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:28:06 -0400 Subject: [PATCH 083/156] Update spacing, button heights --- styles/src/style_tree/search.ts | 78 +++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 27e8c43a4d..c3dbff4341 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -3,8 +3,21 @@ import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +const search_results = () => { + const theme = useTheme() + + return { + // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive + match_background: with_opacity( + foreground(theme.highest, "accent"), + 0.4 + ), + } +} + export default function search(): any { const theme = useTheme() + const SEARCH_ROW_SPACING = 12 // Search input const editor = { @@ -35,11 +48,7 @@ export default function search(): any { return { padding: { top: 4, bottom: 4 }, - // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive - match_background: with_opacity( - foreground(theme.highest, "accent"), - 0.4 - ), + option_button: toggleable({ base: interactive({ base: { @@ -167,7 +176,9 @@ export default function search(): any { // top: 2, }, margin: { - right: 9, + top: 1, + bottom: 1, + right: SEARCH_ROW_SPACING } }, state: { @@ -210,13 +221,13 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), padding: { left: 9, - right: 9, + right: SEARCH_ROW_SPACING, }, }, option_button_group: { padding: { - left: 12, - right: 12, + left: SEARCH_ROW_SPACING, + right: SEARCH_ROW_SPACING, }, }, include_exclude_inputs: { @@ -233,24 +244,26 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), size: 13, }, + // Input Icon editor_icon: { icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/magnifying_glass_12.svg", + color: foreground(theme.highest, "disabled"), + asset: "icons/magnifying_glass.svg", dimensions: { - width: 12, - height: 12, + width: 14, + height: 14, } }, container: { - margin: { right: 6 }, - padding: { left: 2, right: 2 }, + margin: { right: 4 }, + padding: { left: 1, right: 1 }, } }, + // Toggle group buttons - Text | Regex | Semantic mode_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", "variant", { size: "sm" }), background: background(theme.highest, "variant"), border: { @@ -258,21 +271,24 @@ export default function search(): any { left: false, right: false }, - + margin: { + top: 1, + bottom: 1, + }, padding: { - left: 10, - right: 10, + left: 12, + right: 12, }, corner_radius: 6, }, state: { hovered: { - ...text(theme.highest, "mono", "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"), + ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }), background: background(theme.highest, "variant", "pressed"), border: border(theme.highest, "on", "pressed"), }, @@ -281,20 +297,21 @@ export default function search(): any { state: { active: { default: { - ...text(theme.highest, "mono", "on"), + ...text(theme.highest, "mono", "on", { size: "sm" }), background: background(theme.highest, "on") }, hovered: { - ...text(theme.highest, "mono", "on", "hovered"), + ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }), background: background(theme.highest, "on", "hovered") }, clicked: { - ...text(theme.highest, "mono", "on", "pressed"), + ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }), background: background(theme.highest, "on", "pressed") }, }, }, }), + // Next/Previous Match buttons nav_button: toggleable({ state: { inactive: interactive({ @@ -307,7 +324,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -327,7 +347,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -351,6 +374,7 @@ export default function search(): any { search_bar_row_height: 34, search_row_spacing: 8, option_button_height: 22, - modes_container: {} + modes_container: {}, + ...search_results() } } From f6faeea7207fe39a7b84c08460ef29a88cb7bef8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:40:46 -0400 Subject: [PATCH 084/156] Add disabled as an option on text_button --- styles/src/component/text_button.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index b911cd5b77..ead017a803 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -17,6 +17,7 @@ interface TextButtonOptions { variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial + disabled?: boolean text_properties?: TextProperties } @@ -29,6 +30,7 @@ export function text_button({ color, layer, margin, + disabled, text_properties, }: TextButtonOptions = {}) { const theme = useTheme() @@ -65,13 +67,17 @@ export function text_button({ state: { default: { background: background_color, - color: foreground(layer ?? theme.lowest, color), + color: + disabled + ? foreground(layer ?? theme.lowest, "disabled") + : foreground(layer ?? theme.lowest, color), }, - hovered: { - background: background(layer ?? theme.lowest, color, "hovered"), - color: foreground(layer ?? theme.lowest, color, "hovered"), - }, - clicked: { + 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"), }, From f626c61b1e4466543324710d84995d48233aef0f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:40:58 -0400 Subject: [PATCH 085/156] Update action_button style --- styles/src/style_tree/search.ts | 49 ++++++--------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c3dbff4341..73f35b9dd4 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,6 +2,7 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +import { text_button } from "../component/text_button" const search_results = () => { const theme = useTheme() @@ -162,49 +163,13 @@ export default function search(): any { }, }, }), + // Search tool buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ - base: interactive({ - base: { - ...text(theme.highest, "mono", "disabled"), - background: background(theme.highest, "disabled"), - corner_radius: 6, - border: border(theme.highest, "disabled"), - padding: { - // bottom: 2, - left: 10, - right: 10, - // top: 2, - }, - margin: { - top: 1, - bottom: 1, - right: SEARCH_ROW_SPACING - } - }, - state: { - hovered: {} - }, - }), state: { - active: interactive({ - base: { - ...text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), - border: border(theme.highest, "on"), - }, - state: { - hovered: { - ...text(theme.highest, "mono", "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"), - border: border(theme.highest, "on", "pressed"), - }, - }, - }) + inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING } }), + active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING } }) } }), editor, @@ -312,6 +277,8 @@ export default function search(): any { }, }), // Next/Previous Match buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled nav_button: toggleable({ state: { inactive: interactive({ From f0ab27a83da2459af999bb0e0f565ec702a92089 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 14:04:17 -0400 Subject: [PATCH 086/156] Reorder "Select All" button --- crates/search/src/buffer_search.rs | 2 +- styles/src/style_tree/search.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 26ae86f375..78729df936 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -261,8 +261,8 @@ impl View for BufferSearchBar { .with_height(theme.search.search_bar_row_height); let nav_column = Flex::row() - .with_child(Flex::row().with_children(match_count)) .with_child(self.render_action_button("all", cx)) + .with_child(Flex::row().with_children(match_count)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) .constrained() diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 73f35b9dd4..c37a4e4b9a 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -168,8 +168,8 @@ 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 } }), - active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING } }) + 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, @@ -183,9 +183,8 @@ export default function search(): any { border: border(theme.highest, "negative"), }, match_index: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", { size: "sm" }), padding: { - left: 9, right: SEARCH_ROW_SPACING, }, }, From e89ccf2e2692ab6b0e33070e9caa180af5208177 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 14:09:31 -0400 Subject: [PATCH 087/156] Remove unused `label_button` --- styles/src/component/label_button.ts | 78 ---------------------------- 1 file changed, 78 deletions(-) delete mode 100644 styles/src/component/label_button.ts diff --git a/styles/src/component/label_button.ts b/styles/src/component/label_button.ts deleted file mode 100644 index 3f1c54a7f6..0000000000 --- a/styles/src/component/label_button.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Interactive, interactive, toggleable, Toggleable } from "../element" -import { TextStyle, background, text } from "../style_tree/components" -import { useTheme } from "../theme" -import { Button } from "./button" - -type LabelButtonStyle = { - corder_radius: number - background: string | null - padding: { - top: number - bottom: number - left: number - right: number - }, - margin: Button.Options['margin'] - button_height: number -} & TextStyle - -/** Styles an Interactive<ContainedText> */ -export function label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Interactive { - const theme = useTheme() - - const base = Button.button_base(options) - const layer = options.layer ?? theme.middle - const color = options.color ?? "base" - - const default_state = { - ...base, - ...text(layer ?? theme.lowest, "sans", color), - font_size: Button.FONT_SIZE, - } - - return interactive({ - base: default_state, - state: { - hovered: { - background: background(layer, options.background ?? color, "hovered") - }, - clicked: { - background: background(layer, options.background ?? color, "pressed") - } - } - }) -} - -/** Styles an Toggleable<Interactive<ContainedText>> */ -export function toggle_label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Toggleable> { - const activeOptions = { - ...options, - color: options.active_color || options.color, - background: options.active_background || options.background - } - - return toggleable({ - state: { - inactive: label_button_style(options), - active: label_button_style(activeOptions), - }, - }) -} From 100870aa9c1636f977f1eb5e10b94593d3275c49 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Aug 2023 20:32:24 +0300 Subject: [PATCH 088/156] Do not blink the cursor if Zed window is focused away co-authored-by: Max --- crates/editor/src/blink_manager.rs | 13 ++++++++----- crates/editor/src/editor.rs | 10 ++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/blink_manager.rs b/crates/editor/src/blink_manager.rs index 24ea4774aa..fa5a3af0c6 100644 --- a/crates/editor/src/blink_manager.rs +++ b/crates/editor/src/blink_manager.rs @@ -37,10 +37,7 @@ impl BlinkManager { } pub fn pause_blinking(&mut self, cx: &mut ModelContext) { - if !self.visible { - self.visible = true; - cx.notify(); - } + self.show_cursor(cx); let epoch = self.next_blink_epoch(); let interval = self.blink_interval; @@ -82,7 +79,13 @@ impl BlinkManager { }) .detach(); } - } else if !self.visible { + } else { + self.show_cursor(cx); + } + } + + pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) { + if !self.visible { self.visible = true; cx.notify(); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fe8e4e338c..8a432a6e4f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1454,6 +1454,16 @@ impl Editor { cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe_global::(Self::settings_changed), + cx.observe_window_activation(|editor, active, cx| { + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.show_cursor(cx); + blink_manager.disable(cx); + } + }); + }), ], }; From 0e6c91818f33aa69da759e4195ef899616a7c720 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 29 Aug 2023 15:37:51 -0400 Subject: [PATCH 089/156] Woooooops, don't notify the language server until initialized --- crates/project/src/project.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 63b6786d8c..800e0ae01c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2901,6 +2901,8 @@ impl Project { }) .detach(); + let language_server = language_server.initialize(initialization_options).await?; + language_server .notify::( lsp::DidChangeConfigurationParams { @@ -2909,9 +2911,7 @@ impl Project { ) .ok(); - Ok(Some( - language_server.initialize(initialization_options).await?, - )) + Ok(Some(language_server)) } fn insert_newly_running_language_server( From 0a14e33dba0bc26da975c6f0de4d1094c4508fac Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 15:59:35 -0400 Subject: [PATCH 090/156] Pull toolbar into it's own styletree --- styles/src/style_tree/toolbar.ts | 20 ++++++++++++++++++++ styles/src/style_tree/workspace.ts | 14 ++------------ 2 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 styles/src/style_tree/toolbar.ts diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts new file mode 100644 index 0000000000..32fd4ab1b5 --- /dev/null +++ b/styles/src/style_tree/toolbar.ts @@ -0,0 +1,20 @@ +import { useTheme } from "../common" +import { toggleable_icon_button } from "../component/icon_button" +import { background, border } from "./components" + +export const toolbar = () => { + const theme = useTheme() + + return { + height: 42, + background: background(theme.highest), + border: border(theme.highest, { bottom: true }), + item_spacing: 8, + toggleable_tool: toggleable_icon_button(theme, { + margin: { left: 8 }, + variant: "ghost", + active_color: "accent", + }), + padding: { left: 8, right: 8 }, + } +} diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 43a6cec585..7feccfc8bc 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -13,6 +13,7 @@ import { interactive } from "../element" import { titlebar } from "./titlebar" import { useTheme } from "../theme" import { toggleable_icon_button } from "../component/icon_button" +import { toolbar } from "./toolbar" export default function workspace(): any { const theme = useTheme() @@ -128,18 +129,7 @@ export default function workspace(): any { }, status_bar: statusBar(), titlebar: titlebar(), - toolbar: { - height: 42, - background: background(theme.highest), - border: border(theme.highest, { bottom: true }), - item_spacing: 8, - toggleable_tool: toggleable_icon_button(theme, { - margin: { left: 8 }, - variant: "ghost", - active_color: "accent", - }), - padding: { left: 8, right: 8 }, - }, + toolbar: toolbar(), breadcrumb_height: 24, breadcrumbs: interactive({ base: { From 33c9f1485215e3c33a9998b3babc023332fa43fa Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:01:08 -0400 Subject: [PATCH 091/156] Don't require passing `theme` to `toggleable_icon_button` --- styles/src/component/icon_button.ts | 5 +---- styles/src/style_tree/collab_panel.ts | 2 +- styles/src/style_tree/titlebar.ts | 8 ++++---- styles/src/style_tree/toolbar.ts | 2 +- styles/src/style_tree/workspace.ts | 1 - 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 935909afdb..dda6be83b2 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -77,10 +77,7 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO }) } -export function toggleable_icon_button( - theme: Theme, - { color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions -) { +export function toggleable_icon_button({ color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions) { if (!color) color = "base" return toggleable({ diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 07f367c8af..c4ffee889c 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -61,7 +61,7 @@ export default function contacts_panel(): any { width: 14, } - const header_icon_button = toggleable_icon_button(theme, { + const header_icon_button = toggleable_icon_button({ variant: "ghost", size: "sm", active_layer: theme.lowest, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 0a0b69e596..0565982eea 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -34,7 +34,7 @@ function call_controls() { } return { - toggle_microphone_button: toggleable_icon_button(theme, { + toggle_microphone_button: toggleable_icon_button({ margin: { ...margin_y, left: space.group, @@ -43,7 +43,7 @@ function call_controls() { active_color: "negative", }), - toggle_speakers_button: toggleable_icon_button(theme, { + toggle_speakers_button: toggleable_icon_button({ margin: { ...margin_y, left: space.half_item, @@ -51,7 +51,7 @@ function call_controls() { }, }), - screen_share_button: toggleable_icon_button(theme, { + screen_share_button: toggleable_icon_button({ margin: { ...margin_y, left: space.half_item, @@ -263,7 +263,7 @@ export function titlebar(): any { ...call_controls(), - toggle_contacts_button: toggleable_icon_button(theme, { + toggle_contacts_button: toggleable_icon_button({ margin: { left: ITEM_SPACING, }, diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index 32fd4ab1b5..b3f323eb9e 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -10,7 +10,7 @@ export const toolbar = () => { background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, - toggleable_tool: toggleable_icon_button(theme, { + toggleable_tool: toggleable_icon_button({ margin: { left: 8 }, variant: "ghost", active_color: "accent", diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 7feccfc8bc..c24bc3f770 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -12,7 +12,6 @@ import tabBar from "./tab_bar" import { interactive } from "../element" import { titlebar } from "./titlebar" import { useTheme } from "../theme" -import { toggleable_icon_button } from "../component/icon_button" import { toolbar } from "./toolbar" export default function workspace(): any { From d91a9615b5d80c3255f450cd5803025cc5364511 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:02:04 -0400 Subject: [PATCH 092/156] Format --- styles/src/build_themes.ts | 9 +- styles/src/build_tokens.ts | 4 +- styles/src/component/button.ts | 55 ++++++---- styles/src/component/icon_button.ts | 31 ++++-- styles/src/component/indicator.ts | 8 +- styles/src/component/input.ts | 2 +- styles/src/component/margin.ts | 23 ++-- styles/src/component/padding.ts | 23 ++-- styles/src/component/tab.ts | 20 ++-- styles/src/component/tab_bar_button.ts | 67 ++++++------ styles/src/component/text_button.ts | 54 ++++++---- styles/src/style_tree/assistant.ts | 69 ++++++------ styles/src/style_tree/collab_modals.ts | 23 ++-- styles/src/style_tree/collab_panel.ts | 12 +-- styles/src/style_tree/component_test.ts | 9 +- styles/src/style_tree/contacts_popover.ts | 1 - styles/src/style_tree/editor.ts | 2 +- styles/src/style_tree/feedback.ts | 2 +- styles/src/style_tree/picker.ts | 2 +- styles/src/style_tree/project_panel.ts | 16 +-- styles/src/style_tree/search.ts | 121 ++++++++++++++++------ styles/src/style_tree/status_bar.ts | 8 +- styles/src/style_tree/tab_bar.ts | 6 +- styles/src/style_tree/titlebar.ts | 4 +- styles/src/theme/create_theme.ts | 17 +-- styles/src/theme/tokens/theme.ts | 6 +- styles/tsconfig.json | 4 +- 27 files changed, 354 insertions(+), 244 deletions(-) diff --git a/styles/src/build_themes.ts b/styles/src/build_themes.ts index 17575663a1..4d262f8146 100644 --- a/styles/src/build_themes.ts +++ b/styles/src/build_themes.ts @@ -21,9 +21,7 @@ function clear_themes(theme_directory: string) { } } -const all_themes: Theme[] = themes.map((theme) => - create_theme(theme) -) +const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) function write_themes(themes: Theme[], output_directory: string) { clear_themes(output_directory) @@ -34,10 +32,7 @@ function write_themes(themes: Theme[], output_directory: string) { const style_tree = app() const style_tree_json = JSON.stringify(style_tree, null, 2) const temp_path = path.join(temp_directory, `${theme.name}.json`) - const out_path = path.join( - output_directory, - `${theme.name}.json` - ) + const out_path = path.join(output_directory, `${theme.name}.json`) fs.writeFileSync(temp_path, style_tree_json) fs.renameSync(temp_path, out_path) console.log(`- ${out_path} created`) diff --git a/styles/src/build_tokens.ts b/styles/src/build_tokens.ts index fd6aa18ced..3c52b6d989 100644 --- a/styles/src/build_tokens.ts +++ b/styles/src/build_tokens.ts @@ -83,8 +83,6 @@ function write_tokens(themes: Theme[], tokens_directory: string) { console.log(`- ${METADATA_FILE} created`) } -const all_themes: Theme[] = themes.map((theme) => - create_theme(theme) -) +const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) write_tokens(all_themes, TOKENS_DIRECTORY) diff --git a/styles/src/component/button.ts b/styles/src/component/button.ts index 3b554ae37a..e0e831b082 100644 --- a/styles/src/component/button.ts +++ b/styles/src/component/button.ts @@ -5,7 +5,7 @@ import { TextStyle, background } from "../style_tree/components" // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Button { export type Options = { - layer: Layer, + layer: Layer background: keyof Theme["lowest"] color: keyof Theme["lowest"] variant: Button.Variant @@ -16,13 +16,13 @@ export namespace Button { bottom?: number left?: number right?: number - }, + } states: { - enabled?: boolean, - hovered?: boolean, - pressed?: boolean, - focused?: boolean, - disabled?: boolean, + enabled?: boolean + hovered?: boolean + pressed?: boolean + focused?: boolean + disabled?: boolean } } @@ -38,26 +38,26 @@ export namespace Button { export const CORNER_RADIUS = 6 export const variant = { - Default: 'filled', - Outline: 'outline', - Ghost: 'ghost' + Default: "filled", + Outline: "outline", + Ghost: "ghost", } as const - export type Variant = typeof variant[keyof typeof variant] + export type Variant = (typeof variant)[keyof typeof variant] export const shape = { - Rectangle: 'rectangle', - Square: 'square' + Rectangle: "rectangle", + Square: "square", } as const - export type Shape = typeof shape[keyof typeof shape] + export type Shape = (typeof shape)[keyof typeof shape] export const size = { Small: "sm", - Medium: "md" + Medium: "md", } as const - export type Size = typeof size[keyof typeof size] + export type Size = (typeof size)[keyof typeof size] export type BaseStyle = { corder_radius: number @@ -67,8 +67,8 @@ export namespace Button { bottom: number left: number right: number - }, - margin: Button.Options['margin'] + } + margin: Button.Options["margin"] button_height: number } @@ -81,15 +81,18 @@ export namespace Button { shape: Button.shape.Rectangle, states: { hovered: true, - pressed: true - } + pressed: true, + }, } ): BaseStyle => { const theme = useTheme() const layer = options.layer ?? theme.middle const color = options.color ?? "base" - const background_color = options.variant === Button.variant.Ghost ? null : background(layer, options.background ?? color) + const background_color = + options.variant === Button.variant.Ghost + ? null + : background(layer, options.background ?? color) const m = { top: options.margin?.top ?? 0, @@ -106,8 +109,14 @@ export namespace Button { padding: { top: padding, bottom: padding, - left: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding, - right: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding + left: + options.shape === Button.shape.Rectangle + ? padding + Button.RECTANGLE_PADDING + : padding, + right: + options.shape === Button.shape.Rectangle + ? padding + Button.RECTANGLE_PADDING + : padding, }, margin: m, button_height: 16, diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index dda6be83b2..38729b044c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -11,10 +11,7 @@ export type Margin = { } interface IconButtonOptions { - layer?: - | Theme["lowest"] - | Theme["middle"] - | Theme["highest"] + layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] color?: keyof Theme["lowest"] margin?: Partial variant?: Button.Variant @@ -26,15 +23,20 @@ type ToggleableIconButtonOptions = IconButtonOptions & { active_layer?: Layer } -export function icon_button({ color, margin, layer, variant, size }: IconButtonOptions = { - variant: Button.variant.Default, - size: Button.size.Medium, -}) { +export function icon_button( + { color, margin, layer, variant, size }: IconButtonOptions = { + variant: Button.variant.Default, + size: Button.size.Medium, + } +) { const theme = useTheme() if (!color) color = "base" - const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) + const background_color = + variant === Button.variant.Ghost + ? null + : background(layer ?? theme.lowest, color) const m = { top: margin?.top ?? 0, @@ -77,7 +79,14 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO }) } -export function toggleable_icon_button({ color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions) { +export function toggleable_icon_button({ + color, + active_color, + margin, + variant, + size, + active_layer, +}: ToggleableIconButtonOptions) { if (!color) color = "base" return toggleable({ @@ -87,7 +96,7 @@ export function toggleable_icon_button({ color, active_color, margin, variant, s color: active_color ? active_color : color, margin, layer: active_layer, - size + size, }), }, }) diff --git a/styles/src/component/indicator.ts b/styles/src/component/indicator.ts index 81a3b40da7..b3d2105f6a 100644 --- a/styles/src/component/indicator.ts +++ b/styles/src/component/indicator.ts @@ -1,7 +1,13 @@ import { foreground } from "../style_tree/components" import { Layer, StyleSets } from "../theme" -export const indicator = ({ layer, color }: { layer: Layer, color: StyleSets }) => ({ +export const indicator = ({ + layer, + color, +}: { + layer: Layer + color: StyleSets +}) => ({ corner_radius: 4, padding: 4, margin: { top: 12, left: 12 }, diff --git a/styles/src/component/input.ts b/styles/src/component/input.ts index cadfcc8d4a..5921210f88 100644 --- a/styles/src/component/input.ts +++ b/styles/src/component/input.ts @@ -18,6 +18,6 @@ export const input = () => { bottom: 3, left: 12, right: 8, - } + }, } } diff --git a/styles/src/component/margin.ts b/styles/src/component/margin.ts index f6262405f0..5bbdd646a8 100644 --- a/styles/src/component/margin.ts +++ b/styles/src/component/margin.ts @@ -16,19 +16,26 @@ export type MarginStyle = { export const margin_style = (options: MarginOptions): MarginStyle => { const { all, top, bottom, left, right } = options - if (all !== undefined) return { - top: all, - bottom: all, - left: all, - right: all - } + if (all !== undefined) + return { + top: all, + bottom: all, + left: all, + right: all, + } - if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value") + if ( + top === undefined && + bottom === undefined && + left === undefined && + right === undefined + ) + throw new Error("Margin must have at least one value") return { top: top || 0, bottom: bottom || 0, left: left || 0, - right: right || 0 + right: right || 0, } } diff --git a/styles/src/component/padding.ts b/styles/src/component/padding.ts index 96792bf766..b94e263922 100644 --- a/styles/src/component/padding.ts +++ b/styles/src/component/padding.ts @@ -16,19 +16,26 @@ export type PaddingStyle = { export const padding_style = (options: PaddingOptions): PaddingStyle => { const { all, top, bottom, left, right } = options - if (all !== undefined) return { - top: all, - bottom: all, - left: all, - right: all - } + if (all !== undefined) + return { + top: all, + bottom: all, + left: all, + right: all, + } - if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value") + if ( + top === undefined && + bottom === undefined && + left === undefined && + right === undefined + ) + throw new Error("Padding must have at least one value") return { top: top || 0, bottom: bottom || 0, left: left || 0, - right: right || 0 + right: right || 0, } } diff --git a/styles/src/component/tab.ts b/styles/src/component/tab.ts index 9938fb9311..6f73b6f3fb 100644 --- a/styles/src/component/tab.ts +++ b/styles/src/component/tab.ts @@ -9,7 +9,7 @@ type TabProps = { export const tab = ({ layer }: TabProps) => { const active_color = text(layer, "sans", "base").color const inactive_border: Border = { - color: '#FFFFFF00', + color: "#FFFFFF00", width: 1, bottom: true, left: false, @@ -27,7 +27,7 @@ export const tab = ({ layer }: TabProps) => { top: 8, left: 8, right: 8, - bottom: 6 + bottom: 6, }, border: inactive_border, } @@ -35,17 +35,17 @@ export const tab = ({ layer }: TabProps) => { const i = interactive({ state: { default: { - ...base + ...base, }, hovered: { ...base, - ...text(layer, "sans", "base", "hovered") + ...text(layer, "sans", "base", "hovered"), }, clicked: { ...base, - ...text(layer, "sans", "base", "pressed") + ...text(layer, "sans", "base", "pressed"), }, - } + }, }) return toggleable({ @@ -60,14 +60,14 @@ export const tab = ({ layer }: TabProps) => { hovered: { ...i, ...text(layer, "sans", "base", "hovered"), - border: active_border + border: active_border, }, clicked: { ...i, ...text(layer, "sans", "base", "pressed"), - border: active_border + border: active_border, }, - } - } + }, + }, }) } diff --git a/styles/src/component/tab_bar_button.ts b/styles/src/component/tab_bar_button.ts index 0c43e7010e..9e7f9acfc3 100644 --- a/styles/src/component/tab_bar_button.ts +++ b/styles/src/component/tab_bar_button.ts @@ -12,44 +12,47 @@ type TabBarButtonProps = TabBarButtonOptions & { state?: Partial>> } -export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) { +export function tab_bar_button( + theme: Theme, + { icon, color = "base" }: TabBarButtonProps +) { const button_spacing = 8 - return ( - interactive({ - base: { - icon: { - color: foreground(theme.middle, color), - asset: icon, - dimensions: { - width: 15, - height: 15, - }, + return interactive({ + base: { + icon: { + color: foreground(theme.middle, color), + asset: icon, + dimensions: { + width: 15, + height: 15, }, + }, + container: { + corner_radius: 4, + padding: { + top: 4, + bottom: 4, + left: 4, + right: 4, + }, + margin: { + left: button_spacing / 2, + right: button_spacing / 2, + }, + }, + }, + state: { + hovered: { container: { - corner_radius: 4, - padding: { - top: 4, bottom: 4, left: 4, right: 4 - }, - margin: { - left: button_spacing / 2, - right: button_spacing / 2, - }, + background: background(theme.middle, color, "hovered"), }, }, - state: { - hovered: { - container: { - background: background(theme.middle, color, "hovered"), - - } - }, - clicked: { - container: { - background: background(theme.middle, color, "pressed"), - } + clicked: { + container: { + background: background(theme.middle, color, "pressed"), }, }, - }) - ) + }, + }) } diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index ead017a803..b73d20ff1a 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -10,10 +10,7 @@ import { Button } from "./button" import { Margin } from "./icon_button" interface TextButtonOptions { - layer?: - | Theme["lowest"] - | Theme["middle"] - | Theme["highest"] + layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial @@ -36,7 +33,10 @@ export function text_button({ const theme = useTheme() if (!color) color = "base" - const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) + const background_color = + variant === Button.variant.Ghost + ? null + : background(layer ?? theme.lowest, color) const text_options: TextProperties = { size: "xs", @@ -67,20 +67,38 @@ export function text_button({ state: { default: { background: background_color, - color: - disabled - ? foreground(layer ?? theme.lowest, "disabled") - : foreground(layer ?? theme.lowest, color), - }, - hovered: - disabled ? {} : { - background: background(layer ?? theme.lowest, color, "hovered"), - color: foreground(layer ?? theme.lowest, color, "hovered"), - }, - clicked: disabled ? {} : { - background: background(layer ?? theme.lowest, color, "pressed"), - color: foreground(layer ?? theme.lowest, color, "pressed"), + color: disabled + ? foreground(layer ?? theme.lowest, "disabled") + : foreground(layer ?? theme.lowest, color), }, + hovered: disabled + ? {} + : { + background: background( + layer ?? theme.lowest, + color, + "hovered" + ), + color: foreground( + layer ?? theme.lowest, + color, + "hovered" + ), + }, + clicked: disabled + ? {} + : { + background: background( + layer ?? theme.lowest, + color, + "pressed" + ), + color: foreground( + layer ?? theme.lowest, + color, + "pressed" + ), + }, }, }) } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index cfc1f8d813..7df5434f91 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -8,50 +8,48 @@ type RoleCycleButton = TextStyle & { } // TODO: Replace these with zed types type RemainingTokens = TextStyle & { - background: string, - margin: { top: number, right: number }, + background: string + margin: { top: number; right: number } padding: { - right: number, - left: number, - top: number, - bottom: number, - }, - corner_radius: number, + right: number + left: number + top: number + bottom: number + } + corner_radius: number } export default function assistant(): any { const theme = useTheme() - const interactive_role = (color: StyleSets): Interactive => { - return ( - interactive({ - base: { + const interactive_role = ( + color: StyleSets + ): Interactive => { + return interactive({ + base: { + ...text(theme.highest, "sans", color, { size: "sm" }), + }, + state: { + hovered: { ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "hovered"), }, - state: { - hovered: { - ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "hovered"), - }, - clicked: { - ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "pressed"), - } + clicked: { + ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "pressed"), }, - }) - ) + }, + }) } const tokens_remaining = (color: StyleSets): RemainingTokens => { - return ( - { - ...text(theme.highest, "mono", color, { size: "xs" }), - background: background(theme.highest, "on", "default"), - margin: { top: 12, right: 20 }, - padding: { right: 4, left: 4, top: 1, bottom: 1 }, - corner_radius: 6, - } - ) + return { + ...text(theme.highest, "mono", color, { size: "xs" }), + background: background(theme.highest, "on", "default"), + margin: { top: 12, right: 20 }, + padding: { right: 4, left: 4, top: 1, bottom: 1 }, + corner_radius: 6, + } } return { @@ -93,7 +91,10 @@ export default function assistant(): any { base: { background: background(theme.middle), padding: { top: 4, bottom: 4 }, - border: border(theme.middle, "default", { top: true, overlay: true }), + border: border(theme.middle, "default", { + top: true, + overlay: true, + }), }, state: { hovered: { @@ -101,7 +102,7 @@ export default function assistant(): any { }, clicked: { background: background(theme.middle, "pressed"), - } + }, }, }), saved_at: { diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index 0f50e01a39..f9b22b6867 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -39,7 +39,12 @@ export default function channel_modal(): any { row_height: ITEM_HEIGHT, header: { background: background(theme.lowest), - border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }), + border: border(theme.middle, { + bottom: true, + top: false, + left: false, + right: false, + }), padding: { top: SPACING, left: SPACING - BUTTON_OFFSET, @@ -48,7 +53,7 @@ export default function channel_modal(): any { corner_radii: { top_right: 12, top_left: 12, - } + }, }, body: { background: background(theme.middle), @@ -57,12 +62,11 @@ export default function channel_modal(): any { left: SPACING, right: SPACING, bottom: SPACING, - }, corner_radii: { bottom_right: 12, bottom_left: 12, - } + }, }, modal: { background: background(theme.middle), @@ -74,7 +78,6 @@ export default function channel_modal(): any { right: 0, top: 0, }, - }, // FIXME: due to a bug in the picker's size calculation, this must be 600 max_height: 600, @@ -83,7 +86,7 @@ export default function channel_modal(): any { ...text(theme.middle, "sans", "on", { size: "lg" }), padding: { left: BUTTON_OFFSET, - } + }, }, picker: { empty_container: {}, @@ -108,8 +111,8 @@ export default function channel_modal(): any { background: background(theme.middle), padding: { left: 7, - right: 7 - } + right: 7, + }, }, cancel_invite_button: { ...text(theme.middle, "sans", { size: "xs" }), @@ -125,7 +128,7 @@ export default function channel_modal(): any { padding: { left: 4, right: 4, - } + }, }, contact_avatar: { corner_radius: 10, @@ -147,6 +150,6 @@ export default function channel_modal(): any { background: background(theme.middle, "disabled"), color: foreground(theme.middle, "disabled"), }, - } + }, } } diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index c4ffee889c..4d605d118c 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -27,7 +27,7 @@ export default function contacts_panel(): any { color: foreground(layer, "on"), icon_width: 14, button_width: 16, - corner_radius: 8 + corner_radius: 8, } const project_row = { @@ -275,7 +275,7 @@ export default function contacts_panel(): any { list_empty_label_container: { margin: { left: NAME_MARGIN, - } + }, }, list_empty_icon: { color: foreground(layer, "variant"), @@ -289,7 +289,7 @@ export default function contacts_panel(): any { top: SPACING / 2, bottom: SPACING / 2, left: SPACING, - right: SPACING + right: SPACING, }, }, state: { @@ -330,7 +330,7 @@ export default function contacts_panel(): any { right: 4, }, background: background(layer, "hovered"), - ...text(layer, "sans", "hovered", { size: "xs" }) + ...text(layer, "sans", "hovered", { size: "xs" }), }, contact_status_free: indicator({ layer, color: "positive" }), contact_status_busy: indicator({ layer, color: "negative" }), @@ -404,7 +404,7 @@ export default function contacts_panel(): any { channel_editor: { padding: { left: NAME_MARGIN, - } - } + }, + }, } } diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index e2bb0915c1..71057c67ea 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -1,4 +1,3 @@ - import { useTheme } from "../common" import { text_button } from "../component/text_button" import { icon_button } from "../component/icon_button" @@ -14,14 +13,14 @@ export default function contacts_panel(): any { base: text_button({}), state: { active: { - ...text_button({ color: "accent" }) - } - } + ...text_button({ color: "accent" }), + }, + }, }), disclosure: { ...text(theme.lowest, "sans", "base"), button: icon_button({ variant: "ghost" }), spacing: 4, - } + }, } } diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts index 0e76bbb38a..dcd84c3252 100644 --- a/styles/src/style_tree/contacts_popover.ts +++ b/styles/src/style_tree/contacts_popover.ts @@ -3,5 +3,4 @@ import { background, border } from "./components" export default function contacts_popover(): any { const theme = useTheme() - } diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9277a2e7a1..5a662098e8 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -307,7 +307,7 @@ export default function editor(): any { ? with_opacity(theme.ramps.green(0.5).hex(), 0.8) : with_opacity(theme.ramps.green(0.4).hex(), 0.8), }, - selections: foreground(layer, "accent") + selections: foreground(layer, "accent"), }, composition_mark: { underline: { diff --git a/styles/src/style_tree/feedback.ts b/styles/src/style_tree/feedback.ts index b1bd96e165..0349359533 100644 --- a/styles/src/style_tree/feedback.ts +++ b/styles/src/style_tree/feedback.ts @@ -37,7 +37,7 @@ export default function feedback(): any { ...text(theme.highest, "mono", "on", "disabled"), background: background(theme.highest, "on", "disabled"), border: border(theme.highest, "on", "disabled"), - } + }, }, }), button_margin: 8, diff --git a/styles/src/style_tree/picker.ts b/styles/src/style_tree/picker.ts index 28ae854787..317f600b1e 100644 --- a/styles/src/style_tree/picker.ts +++ b/styles/src/style_tree/picker.ts @@ -152,7 +152,7 @@ export default function picker(): any { 0.5 ), }, - } + }, }), } } diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index e239f9a840..51958af145 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -64,17 +64,17 @@ export default function project_panel(): any { const unselected_default_style = merge( base_properties, unselected?.default ?? {}, - {}, + {} ) const unselected_hovered_style = merge( base_properties, { background: background(theme.middle, "hovered") }, - unselected?.hovered ?? {}, + unselected?.hovered ?? {} ) const unselected_clicked_style = merge( base_properties, { background: background(theme.middle, "pressed") }, - unselected?.clicked ?? {}, + unselected?.clicked ?? {} ) const selected_default_style = merge( base_properties, @@ -82,7 +82,7 @@ export default function project_panel(): any { background: background(theme.lowest), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.default ?? {}, + selected_style?.default ?? {} ) const selected_hovered_style = merge( base_properties, @@ -90,7 +90,7 @@ export default function project_panel(): any { background: background(theme.lowest, "hovered"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.hovered ?? {}, + selected_style?.hovered ?? {} ) const selected_clicked_style = merge( base_properties, @@ -98,7 +98,7 @@ export default function project_panel(): any { background: background(theme.lowest, "pressed"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.clicked ?? {}, + selected_style?.clicked ?? {} ) return toggleable({ @@ -175,7 +175,7 @@ export default function project_panel(): any { default: { icon_color: foreground(theme.middle, "variant"), }, - }, + } ), cut_entry: entry( { @@ -190,7 +190,7 @@ export default function project_panel(): any { size: "sm", }), }, - }, + } ), filename_editor: { background: background(theme.middle, "on"), diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c37a4e4b9a..3afa1932a4 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -60,7 +60,8 @@ export default function search(): any { corner_radius: 2, margin: { right: 2 }, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, padding: { left: 4, @@ -74,14 +75,16 @@ export default function search(): any { ...text(theme.highest, "mono", "variant", "hovered"), background: background(theme.highest, "on", "hovered"), border: { - width: 1., color: background(theme.highest, "on", "hovered") + width: 1, + color: background(theme.highest, "on", "hovered"), }, }, clicked: { ...text(theme.highest, "mono", "variant", "pressed"), background: background(theme.highest, "on", "pressed"), border: { - width: 1., color: background(theme.highest, "on", "pressed") + width: 1, + color: background(theme.highest, "on", "pressed"), }, }, }, @@ -96,11 +99,19 @@ export default function search(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered" + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed" + ), border: border(theme.highest, "accent", "pressed"), }, }, @@ -117,7 +128,8 @@ export default function search(): any { corner_radius: 2, margin: { right: 2 }, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, padding: { left: 4, @@ -131,14 +143,16 @@ export default function search(): any { ...text(theme.highest, "mono", "variant", "hovered"), background: background(theme.highest, "on", "hovered"), border: { - width: 1., color: background(theme.highest, "on", "hovered") + width: 1, + color: background(theme.highest, "on", "hovered"), }, }, clicked: { ...text(theme.highest, "mono", "variant", "pressed"), background: background(theme.highest, "on", "pressed"), border: { - width: 1., color: background(theme.highest, "on", "pressed") + width: 1, + color: background(theme.highest, "on", "pressed"), }, }, }, @@ -153,11 +167,19 @@ export default function search(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered" + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed" + ), border: border(theme.highest, "accent", "pressed"), }, }, @@ -168,9 +190,20 @@ export default function search(): any { // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ state: { - inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }), - active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }) - } + inactive: text_button({ + variant: "ghost", + layer: theme.highest, + disabled: true, + margin: { right: SEARCH_ROW_SPACING }, + text_properties: { size: "sm" }, + }), + active: text_button({ + variant: "ghost", + layer: theme.highest, + margin: { right: SEARCH_ROW_SPACING }, + text_properties: { size: "sm" }, + }), + }, }), editor, invalid_editor: { @@ -216,12 +249,12 @@ export default function search(): any { dimensions: { width: 14, height: 14, - } + }, }, container: { margin: { right: 4 }, padding: { left: 1, right: 1 }, - } + }, }, // Toggle group buttons - Text | Regex | Semantic mode_button: toggleable({ @@ -233,7 +266,7 @@ export default function search(): any { border: { ...border(theme.highest, "on"), left: false, - right: false + right: false, }, margin: { top: 1, @@ -247,13 +280,25 @@ export default function search(): any { }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered", { size: "sm" }), - background: background(theme.highest, "variant", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered", { + size: "sm", + }), + background: background( + theme.highest, + "variant", + "hovered" + ), border: border(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }), - background: background(theme.highest, "variant", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed", { + size: "sm", + }), + background: background( + theme.highest, + "variant", + "pressed" + ), border: border(theme.highest, "on", "pressed"), }, }, @@ -262,15 +307,19 @@ export default function search(): any { active: { default: { ...text(theme.highest, "mono", "on", { size: "sm" }), - background: background(theme.highest, "on") + background: background(theme.highest, "on"), }, hovered: { - ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }), - background: background(theme.highest, "on", "hovered") + ...text(theme.highest, "mono", "on", "hovered", { + size: "sm", + }), + background: background(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }), - background: background(theme.highest, "on", "pressed") + ...text(theme.highest, "mono", "on", "pressed", { + size: "sm", + }), + background: background(theme.highest, "on", "pressed"), }, }, }, @@ -300,8 +349,8 @@ export default function search(): any { }, }, state: { - hovered: {} - } + hovered: {}, + }, }), active: interactive({ base: { @@ -325,22 +374,30 @@ export default function search(): any { state: { hovered: { ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), + background: background( + theme.highest, + "on", + "hovered" + ), border: border(theme.highest, "on", "hovered"), }, clicked: { ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), + background: background( + theme.highest, + "on", + "pressed" + ), border: border(theme.highest, "on", "pressed"), }, }, - }) - } + }), + }, }), search_bar_row_height: 34, search_row_spacing: 8, option_button_height: 22, modes_container: {}, - ...search_results() + ...search_results(), } } diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 2d3b81f7c2..3b5ebf2c88 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -34,9 +34,11 @@ export default function status_bar(): any { ...text(layer, "mono", "base", { size: "xs" }), }, active_language: text_button({ - color: "base" + color: "base", + }), + auto_update_progress_message: text(layer, "sans", "base", { + size: "xs", }), - auto_update_progress_message: text(layer, "sans", "base", { size: "xs" }), auto_update_done_message: text(layer, "sans", "base", { size: "xs" }), lsp_status: interactive({ base: { @@ -125,7 +127,7 @@ export default function status_bar(): any { }, clicked: { background: background(layer, "pressed"), - } + }, }, }), state: { diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 129bd17869..23ff03a6a3 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -93,7 +93,7 @@ export default function tab_bar(): any { border: border(theme.lowest, "on", { bottom: true, overlay: true, - }) + }), }, state: { hovered: { @@ -101,7 +101,7 @@ export default function tab_bar(): any { background: background(theme.highest, "on", "hovered"), }, disabled: { - color: foreground(theme.highest, "on", "disabled") + color: foreground(theme.highest, "on", "disabled"), }, }, }) @@ -162,6 +162,6 @@ export default function tab_bar(): any { right: false, }, }, - nav_button: nav_button + nav_button: nav_button, } } diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 0565982eea..e4e274684c 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -187,10 +187,10 @@ export function titlebar(): any { project_name_divider: text(theme.lowest, "sans", "variant"), project_menu_button: toggleable_text_button(theme, { - color: 'base', + color: "base", }), git_menu_button: toggleable_text_button(theme, { - color: 'variant', + color: "variant", }), // Collaborators diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index ab3c96f280..61471616fb 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -13,16 +13,16 @@ export interface Theme { is_light: boolean /** - * App background, other elements that should sit directly on top of the background. - */ + * App background, other elements that should sit directly on top of the background. + */ lowest: Layer /** - * Panels, tabs, other UI surfaces that sit on top of the background. - */ + * Panels, tabs, other UI surfaces that sit on top of the background. + */ middle: Layer /** - * Editors like code buffers, conversation editors, etc. - */ + * Editors like code buffers, conversation editors, etc. + */ highest: Layer ramps: RampSet @@ -206,7 +206,10 @@ function build_color_family(ramps: RampSet): ColorFamily { for (const ramp in ramps) { const ramp_value = ramps[ramp as keyof RampSet] - const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100] + const lightnessValues = [ + ramp_value(0).get("hsl.l") * 100, + ramp_value(1).get("hsl.l") * 100, + ] const low = Math.min(...lightnessValues) const high = Math.max(...lightnessValues) const range = high - low diff --git a/styles/src/theme/tokens/theme.ts b/styles/src/theme/tokens/theme.ts index f759bc8139..e2c3bb33d3 100644 --- a/styles/src/theme/tokens/theme.ts +++ b/styles/src/theme/tokens/theme.ts @@ -4,11 +4,7 @@ import { SingleOtherToken, TokenTypes, } from "@tokens-studio/types" -import { - Shadow, - SyntaxHighlightStyle, - ThemeSyntax, -} from "../create_theme" +import { Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../create_theme" import { LayerToken, layer_token } from "./layer" import { PlayersToken, players_token } from "./players" import { color_token } from "./token" diff --git a/styles/tsconfig.json b/styles/tsconfig.json index c7eaa50eed..940442e1b7 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -23,7 +23,5 @@ "skipLibCheck": true, "useUnknownInCatchVariables": false }, - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From 93cf52a719eb3778623e9db3d4130803bf55ae1c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:10:40 -0400 Subject: [PATCH 093/156] Update toolbar active state style --- styles/src/component/icon_button.ts | 22 +++++++++++++++------- styles/src/style_tree/titlebar.ts | 2 ++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 38729b044c..b20c81df68 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -13,6 +13,7 @@ export type Margin = { interface IconButtonOptions { layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] color?: keyof Theme["lowest"] + background_color?: keyof Theme["lowest"] margin?: Partial variant?: Button.Variant size?: Button.Size @@ -20,11 +21,13 @@ interface IconButtonOptions { type ToggleableIconButtonOptions = IconButtonOptions & { active_color?: keyof Theme["lowest"] + active_background_color?: keyof Theme["lowest"] active_layer?: Layer + active_variant?: Button.Variant } export function icon_button( - { color, margin, layer, variant, size }: IconButtonOptions = { + { color, background_color, margin, layer, variant, size }: IconButtonOptions = { variant: Button.variant.Default, size: Button.size.Medium, } @@ -33,10 +36,10 @@ export function icon_button( if (!color) color = "base" - const background_color = + const default_background = variant === Button.variant.Ghost ? null - : background(layer ?? theme.lowest, color) + : background(layer ?? theme.lowest, background_color ?? color) const m = { top: margin?.top ?? 0, @@ -64,15 +67,15 @@ export function icon_button( }, state: { default: { - background: background_color, + background: default_background, color: foreground(layer ?? theme.lowest, color), }, hovered: { - background: background(layer ?? theme.lowest, color, "hovered"), + background: background(layer ?? theme.lowest, background_color ?? color, "hovered"), color: foreground(layer ?? theme.lowest, color, "hovered"), }, clicked: { - background: background(layer ?? theme.lowest, color, "pressed"), + background: background(layer ?? theme.lowest, background_color ?? color, "pressed"), color: foreground(layer ?? theme.lowest, color, "pressed"), }, }, @@ -81,7 +84,10 @@ export function icon_button( export function toggleable_icon_button({ color, + background_color, active_color, + active_background_color, + active_variant, margin, variant, size, @@ -91,11 +97,13 @@ export function toggleable_icon_button({ return toggleable({ state: { - inactive: icon_button({ color, margin, variant, size }), + inactive: icon_button({ color, background_color, margin, variant, size }), active: icon_button({ color: active_color ? active_color : color, + background_color: active_background_color ? active_background_color : background_color, margin, layer: active_layer, + variant: active_variant || variant, size, }), }, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index e4e274684c..9fb439f618 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -41,6 +41,7 @@ function call_controls() { right: space.half_item, }, active_color: "negative", + active_background_color: "negative", }), toggle_speakers_button: toggleable_icon_button({ @@ -58,6 +59,7 @@ function call_controls() { right: space.group, }, active_color: "accent", + active_background_color: "accent", }), muted: foreground(theme.lowest, "negative"), From c9b12370790d679da816ea8365d9da550d22f67d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:20:19 -0400 Subject: [PATCH 094/156] Update titlebar size --- styles/src/component/icon_button.ts | 2 +- styles/src/style_tree/search.ts | 10 +++++----- styles/src/style_tree/toolbar.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index b20c81df68..5b7c61b17c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -60,7 +60,7 @@ export function icon_button( corner_radius: 6, padding: padding, margin: m, - icon_width: 12, + icon_width: 14, icon_height: 14, button_width: size === Button.size.Small ? 16 : 20, button_height: 14, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 3afa1932a4..a93aab4ea8 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -31,7 +31,7 @@ export default function search(): any { text: text(theme.highest, "mono", "default"), border: border(theme.highest), margin: { - right: 9, + right: SEARCH_ROW_SPACING, }, padding: { top: 4, @@ -48,7 +48,7 @@ export default function search(): any { } return { - padding: { top: 4, bottom: 4 }, + padding: { top: 0, bottom: 0 }, option_button: toggleable({ base: interactive({ @@ -273,8 +273,8 @@ export default function search(): any { bottom: 1, }, padding: { - left: 12, - right: 12, + left: 10, + right: 10, }, corner_radius: 6, }, @@ -394,7 +394,7 @@ export default function search(): any { }), }, }), - search_bar_row_height: 34, + search_bar_row_height: 32, search_row_spacing: 8, option_button_height: 22, modes_container: {}, diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index b3f323eb9e..39f11f58bf 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -6,15 +6,15 @@ export const toolbar = () => { const theme = useTheme() return { - height: 42, + height: 32, + padding: { left: 4, right: 4, top: 4, bottom: 4 }, background: background(theme.highest), border: border(theme.highest, { bottom: true }), - item_spacing: 8, + item_spacing: 4, toggleable_tool: toggleable_icon_button({ - margin: { left: 8 }, + margin: { left: 4 }, variant: "ghost", active_color: "accent", }), - padding: { left: 8, right: 8 }, } } From 2af5fc503061935d5ed8a316c26eea55bf15705e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 16:37:48 -0400 Subject: [PATCH 095/156] Move breadcrumb style to toolbar --- crates/breadcrumbs/src/breadcrumbs.rs | 8 ++++---- crates/theme/src/theme.rs | 4 ++-- styles/src/style_tree/toolbar.ts | 20 +++++++++++++++++++- styles/src/style_tree/workspace.ts | 17 ----------------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 615e238648..41985edb75 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -50,7 +50,7 @@ impl View for Breadcrumbs { let not_editor = active_item.downcast::().is_none(); let theme = theme::current(cx).clone(); - let style = &theme.workspace.breadcrumbs; + let style = &theme.workspace.toolbar.breadcrumbs; let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { Some(breadcrumbs) => breadcrumbs, @@ -60,7 +60,7 @@ impl View for Breadcrumbs { .map(|breadcrumb| { Text::new( breadcrumb.text, - theme.workspace.breadcrumbs.default.text.clone(), + theme.workspace.toolbar.breadcrumbs.default.text.clone(), ) .with_highlights(breadcrumb.highlights.unwrap_or_default()) .into_any() @@ -68,10 +68,10 @@ impl View for Breadcrumbs { let crumbs = Flex::row() .with_children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" 〉 ", style.default.text.clone()).into_any() + Label::new(" › ", style.default.text.clone()).into_any() })) .constrained() - .with_height(theme.workspace.breadcrumb_height) + .with_height(theme.workspace.toolbar.breadcrumb_height) .contained(); if not_editor || !self.pane_focused { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a5faba8eaf..423bb879d2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -88,8 +88,6 @@ pub struct Workspace { pub dock: Dock, pub status_bar: StatusBar, pub toolbar: Toolbar, - pub breadcrumb_height: f32, - pub breadcrumbs: Interactive, pub disconnected_overlay: ContainedText, pub modal: ContainerStyle, pub zoomed_panel_foreground: ContainerStyle, @@ -411,6 +409,8 @@ pub struct Toolbar { pub height: f32, pub item_spacing: f32, pub toggleable_tool: Toggleable>, + pub breadcrumb_height: f32, + pub breadcrumbs: Interactive, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index 39f11f58bf..7292a220a8 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -1,6 +1,7 @@ import { useTheme } from "../common" import { toggleable_icon_button } from "../component/icon_button" -import { background, border } from "./components" +import { interactive } from "../element" +import { background, border, foreground, text } from "./components" export const toolbar = () => { const theme = useTheme() @@ -16,5 +17,22 @@ export const toolbar = () => { variant: "ghost", active_color: "accent", }), + breadcrumb_height: 24, + breadcrumbs: interactive({ + base: { + ...text(theme.highest, "sans", "variant"), + corner_radius: 6, + padding: { + left: 6, + right: 6, + }, + }, + state: { + hovered: { + color: foreground(theme.highest, "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + }, + }, + }), } } diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index c24bc3f770..ba89c7b05f 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -129,23 +129,6 @@ export default function workspace(): any { status_bar: statusBar(), titlebar: titlebar(), toolbar: toolbar(), - breadcrumb_height: 24, - breadcrumbs: interactive({ - base: { - ...text(theme.highest, "sans", "variant"), - corner_radius: 6, - padding: { - left: 6, - right: 6, - }, - }, - state: { - hovered: { - color: foreground(theme.highest, "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - }, - }, - }), disconnected_overlay: { ...text(theme.lowest, "sans"), background: with_opacity(background(theme.lowest), 0.8), From 97d187bba7a33ccecf708287987e8b69bcca9f9d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 19:50:27 -0400 Subject: [PATCH 096/156] Remove project divider in titlebar --- crates/collab_ui/src/collab_titlebar_item.rs | 8 ---- styles/src/component/index.ts | 6 +++ styles/src/component/text_button.ts | 44 ++++++++++---------- styles/src/element/index.ts | 2 + styles/src/{component => element}/margin.ts | 0 styles/src/{component => element}/padding.ts | 0 styles/src/style_tree/titlebar.ts | 12 ++---- styles/src/theme/index.ts | 1 + 8 files changed, 35 insertions(+), 38 deletions(-) create mode 100644 styles/src/component/index.ts rename styles/src/{component => element}/margin.ts (100%) rename styles/src/{component => element}/padding.ts (100%) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 684ddca08d..bd94c85f95 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -213,7 +213,6 @@ impl CollabTitlebarItem { .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH)); let project_style = theme.titlebar.project_menu_button.clone(); let git_style = theme.titlebar.git_menu_button.clone(); - let divider_style = theme.titlebar.project_name_divider.clone(); let item_spacing = theme.titlebar.item_spacing; let mut ret = Flex::row().with_child( @@ -249,13 +248,6 @@ impl CollabTitlebarItem { if let Some(git_branch) = branch_prepended { ret = ret.with_child( Flex::row() - .with_child( - Label::new("/", divider_style.text) - .contained() - .with_style(divider_style.container) - .aligned() - .left(), - ) .with_child( Stack::new() .with_child( diff --git a/styles/src/component/index.ts b/styles/src/component/index.ts new file mode 100644 index 0000000000..f2cbc7b26a --- /dev/null +++ b/styles/src/component/index.ts @@ -0,0 +1,6 @@ +export * from "./icon_button" +export * from "./indicator" +export * from "./input" +export * from "./tab" +export * from "./tab_bar_button" +export * from "./text_button" diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index b73d20ff1a..8333d9e81a 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -74,31 +74,31 @@ export function text_button({ hovered: disabled ? {} : { - background: background( - layer ?? theme.lowest, - color, - "hovered" - ), - color: foreground( - layer ?? theme.lowest, - color, - "hovered" - ), - }, + background: background( + layer ?? theme.lowest, + color, + "hovered" + ), + color: foreground( + layer ?? theme.lowest, + color, + "hovered" + ), + }, clicked: disabled ? {} : { - background: background( - layer ?? theme.lowest, - color, - "pressed" - ), - color: foreground( - layer ?? theme.lowest, - color, - "pressed" - ), - }, + background: background( + layer ?? theme.lowest, + color, + "pressed" + ), + color: foreground( + layer ?? theme.lowest, + color, + "pressed" + ), + }, }, }) } diff --git a/styles/src/element/index.ts b/styles/src/element/index.ts index d41b4e2cc3..0586399fb1 100644 --- a/styles/src/element/index.ts +++ b/styles/src/element/index.ts @@ -1,4 +1,6 @@ import { interactive, Interactive } from "./interactive" import { toggleable, Toggleable } from "./toggle" +export * from "./padding" +export * from "./margin" export { interactive, Interactive, toggleable, Toggleable } diff --git a/styles/src/component/margin.ts b/styles/src/element/margin.ts similarity index 100% rename from styles/src/component/margin.ts rename to styles/src/element/margin.ts diff --git a/styles/src/component/padding.ts b/styles/src/element/padding.ts similarity index 100% rename from styles/src/component/padding.ts rename to styles/src/element/padding.ts diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 9fb439f618..672907b22c 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -1,8 +1,6 @@ -import { icon_button, toggleable_icon_button } from "../component/icon_button" -import { toggleable_text_button } from "../component/text_button" +import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component" import { interactive, toggleable } from "../element" -import { useTheme } from "../theme" -import { with_opacity } from "../theme/color" +import { useTheme, with_opacity } from "../theme" import { background, border, foreground, text } from "./components" const ITEM_SPACING = 8 @@ -185,12 +183,10 @@ export function titlebar(): any { height: 400, }, - // Project - project_name_divider: text(theme.lowest, "sans", "variant"), - project_menu_button: toggleable_text_button(theme, { - color: "base", + color: "base" }), + git_menu_button: toggleable_text_button(theme, { color: "variant", }), diff --git a/styles/src/theme/index.ts b/styles/src/theme/index.ts index ca8aaa461f..47110940f5 100644 --- a/styles/src/theme/index.ts +++ b/styles/src/theme/index.ts @@ -23,3 +23,4 @@ export * from "./create_theme" export * from "./ramps" export * from "./syntax" export * from "./theme_config" +export * from "./color" From 6e964acd92f055ad67504517e3b0b96ad34d5c2a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 20:04:15 -0400 Subject: [PATCH 097/156] Fix extra theme entry --- crates/theme/src/theme.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 423bb879d2..8574357777 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -118,7 +118,6 @@ pub struct Titlebar { pub height: f32, pub menu: TitlebarMenu, pub project_menu_button: Toggleable>, - pub project_name_divider: ContainedText, pub git_menu_button: Toggleable>, pub item_spacing: f32, pub face_pile_spacing: f32, From e3a0252b04e6db8ae327dc55df4e1a667e07c549 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 29 Aug 2023 20:42:13 -0400 Subject: [PATCH 098/156] Make multi-server completion requests not serial --- crates/project/src/project.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 800e0ae01c..c672a37cad 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4438,21 +4438,27 @@ impl Project { let position = position.to_point_utf16(buffer.read(cx)); let server_ids: Vec<_> = self .language_servers_for_buffer(buffer.read(cx), cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) .map(|(_, server)| server.server_id()) .collect(); let buffer = buffer.clone(); cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(server_ids.len()); + this.update(&mut cx, |this, cx| { + for server_id in server_ids { + tasks.push(this.request_lsp( + buffer.clone(), + server_id, + GetCompletions { position }, + cx, + )); + } + }); + let mut completions = Vec::new(); - - for server_id in server_ids { - let new_completions = this - .update(&mut cx, |this, cx| { - this.request_lsp(buffer.clone(), server_id, GetCompletions { position }, cx) - }) - .await; - - if let Ok(new_completions) = new_completions { + for task in tasks { + if let Ok(new_completions) = task.await { completions.extend_from_slice(&new_completions); } } From d3650594c386e2b96958a0fb552e5ad322a6df30 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 28 Aug 2023 11:47:37 -0600 Subject: [PATCH 099/156] Fix find_{,preceding}boundary to work on buffer text Before this change the bounday could mistakenly have happened on a soft line wrap. Also fixes interaction with inlays better. --- crates/editor/src/movement.rs | 174 ++++++------------ crates/vim/src/motion.rs | 26 ++- crates/vim/src/normal.rs | 2 +- crates/vim/src/normal/change.rs | 20 +- crates/vim/src/object.rs | 30 ++- crates/vim/src/test.rs | 18 ++ .../src/test/neovim_backed_test_context.rs | 7 +- crates/vim/test_data/test_end_of_word.json | 32 ++++ .../test_data/test_visual_word_object.json | 6 +- crates/vim/test_data/test_wrapped_lines.json | 5 + 10 files changed, 174 insertions(+), 146 deletions(-) create mode 100644 crates/vim/test_data/test_end_of_word.json diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index def6340e38..915da7b23f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,8 +1,14 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{char_kind, CharKind, ToPoint}; +use crate::{char_kind, CharKind, ToOffset, ToPoint}; use language::Point; use std::ops::Range; +#[derive(Debug, PartialEq)] +pub enum FindRange { + SingleLine, + MultiLine, +} + pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; @@ -179,7 +185,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa let raw_point = point.to_point(map); let language = map.buffer_snapshot.language_at(raw_point); - find_preceding_boundary(map, point, |left, right| { + find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) || left == '\n' }) @@ -188,7 +194,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa 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| { + find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { let is_word_start = char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); let is_subword_start = @@ -200,7 +206,7 @@ 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| { + find_boundary(map, point, FindRange::MultiLine, |left, right| { (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) || right == '\n' }) @@ -209,7 +215,7 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint 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| { + find_boundary(map, point, FindRange::MultiLine, |left, right| { let is_word_end = (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); let is_subword_end = @@ -272,79 +278,34 @@ pub fn end_of_paragraph( map.max_point() } -/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the -/// given predicate returning true. The predicate is called with the character to the left and right -/// of the candidate boundary location, and will be called with `\n` characters indicating the start -/// or end of a line. +/// Scans for a boundary preceding the given start point `from` until a boundary is found, +/// indicated by the given predicate returning true. +/// The predicate is called with the character to the left and right of the candidate boundary location. +/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned. pub fn find_preceding_boundary( map: &DisplaySnapshot, from: DisplayPoint, + find_range: FindRange, mut is_boundary: impl FnMut(char, char) -> bool, ) -> DisplayPoint { - let mut start_column = 0; - let mut soft_wrap_row = from.row() + 1; + let mut prev_ch = None; + let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot); - let mut prev = None; - for (ch, point) in map.reverse_chars_at(from) { - // Recompute soft_wrap_indent if the row has changed - if point.row() != soft_wrap_row { - soft_wrap_row = point.row(); - - if point.row() == 0 { - start_column = 0; - } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { - start_column = indent; - } - } - - // If the current point is in the soft_wrap, skip comparing it - if point.column() < start_column { - continue; - } - - if let Some((prev_ch, prev_point)) = prev { - if is_boundary(ch, prev_ch) { - return map.clip_point(prev_point, Bias::Left); - } - } - - prev = Some((ch, point)); - } - map.clip_point(DisplayPoint::zero(), Bias::Left) -} - -/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the -/// given predicate returning true. The predicate is called with the character to the left and right -/// of the candidate boundary location, and will be called with `\n` characters indicating the start -/// or end of a line. If no boundary is found, the start of the line is returned. -pub fn find_preceding_boundary_in_line( - map: &DisplaySnapshot, - from: DisplayPoint, - mut is_boundary: impl FnMut(char, char) -> bool, -) -> DisplayPoint { - let mut start_column = 0; - if from.row() > 0 { - if let Some(indent) = map.soft_wrap_indent(from.row() - 1) { - start_column = indent; - } - } - - let mut prev = None; - for (ch, point) in map.reverse_chars_at(from) { - if let Some((prev_ch, prev_point)) = prev { - if is_boundary(ch, prev_ch) { - return map.clip_point(prev_point, Bias::Left); - } - } - - if ch == '\n' || point.column() < start_column { + for ch in map.buffer_snapshot.reversed_chars_at(offset) { + if find_range == FindRange::SingleLine && ch == '\n' { break; } + if let Some(prev_ch) = prev_ch { + if is_boundary(ch, prev_ch) { + break; + } + } - prev = Some((ch, point)); + offset -= ch.len_utf8(); + prev_ch = Some(ch); } - map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left) + map.clip_point(offset.to_display_point(map), Bias::Left) } /// Scans for a boundary following the given start point until a boundary is found, indicated by the @@ -354,47 +315,26 @@ pub fn find_preceding_boundary_in_line( pub fn find_boundary( map: &DisplaySnapshot, from: DisplayPoint, + find_range: FindRange, mut is_boundary: impl FnMut(char, char) -> bool, ) -> DisplayPoint { + let mut offset = from.to_offset(&map, Bias::Right); let mut prev_ch = None; - for (ch, point) in map.chars_at(from) { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - return map.clip_point(point, Bias::Right); - } - } - prev_ch = Some(ch); - } - map.clip_point(map.max_point(), Bias::Right) -} - -/// Scans for a boundary following the given start point until a boundary is found, indicated by the -/// given predicate returning true. The predicate is called with the character to the left and right -/// of the candidate boundary location, and will be called with `\n` characters indicating the start -/// or end of a line. If no boundary is found, the end of the line is returned -pub fn find_boundary_in_line( - map: &DisplaySnapshot, - from: DisplayPoint, - mut is_boundary: impl FnMut(char, char) -> bool, -) -> DisplayPoint { - let mut prev = None; - for (ch, point) in map.chars_at(from) { - if let Some((prev_ch, _)) = prev { - if is_boundary(prev_ch, ch) { - return map.clip_point(point, Bias::Right); - } - } - - prev = Some((ch, point)); - - if ch == '\n' { + for ch in map.buffer_snapshot.chars_at(offset) { + if find_range == FindRange::SingleLine && ch == '\n' { break; } - } + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + break; + } + } - // Return the last position checked so that we give a point right before the newline or eof. - map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right) + offset += ch.len_utf8(); + prev_ch = Some(ch); + } + map.clip_point(offset.to_display_point(map), Bias::Right) } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { @@ -533,7 +473,12 @@ mod tests { ) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( - find_preceding_boundary(&snapshot, display_points[1], is_boundary), + find_preceding_boundary( + &snapshot, + display_points[1], + FindRange::MultiLine, + is_boundary + ), display_points[0] ); } @@ -612,21 +557,15 @@ mod tests { find_preceding_boundary( &snapshot, buffer_snapshot.len().to_display_point(&snapshot), - |left, _| left == 'a', + FindRange::MultiLine, + |left, _| left == 'e', ), - 0.to_display_point(&snapshot), + snapshot + .buffer_snapshot + .offset_to_point(5) + .to_display_point(&snapshot), "Should not stop at inlays when looking for boundaries" ); - - assert_eq!( - find_preceding_boundary_in_line( - &snapshot, - buffer_snapshot.len().to_display_point(&snapshot), - |left, _| left == 'a', - ), - 0.to_display_point(&snapshot), - "Should not stop at inlays when looking for boundaries in line" - ); } #[gpui::test] @@ -699,7 +638,12 @@ mod tests { ) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( - find_boundary(&snapshot, display_points[0], is_boundary), + find_boundary( + &snapshot, + display_points[0], + FindRange::MultiLine, + is_boundary + ), display_points[1] ); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 0d3fb700ef..6f28430796 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3,7 +3,8 @@ use std::{cmp, sync::Arc}; use editor::{ char_kind, display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, - movement, Bias, CharKind, DisplayPoint, ToOffset, + movement::{self, FindRange}, + Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; use language::{Point, Selection, SelectionGoal}; @@ -592,7 +593,7 @@ pub(crate) fn next_word_start( 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| { + point = movement::find_boundary(map, point, FindRange::MultiLine, |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 at_newline = right == '\n'; @@ -616,8 +617,14 @@ fn next_word_end( ) -> 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| { + if point.column() < map.line_len(point.row()) { + *point.column_mut() += 1; + } else if point.row() < map.max_buffer_row() { + *point.row_mut() += 1; + *point.column_mut() = 0; + } + // *point.column_mut() += 1; + point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); @@ -649,12 +656,13 @@ fn previous_word_start( 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); + point = + movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { + 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' - }); + (left_kind != right_kind && !right.is_whitespace()) || left == '\n' + }); } point } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index a73c518809..c8e623e4c1 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -445,7 +445,7 @@ mod test { } #[gpui::test] - async fn test_e(cx: &mut gpui::TestAppContext) { + async fn test_end_of_word(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]); cx.assert_all(indoc! {" Thˇe quicˇkˇ-browˇn diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 5591de89c6..6e64b050d1 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,7 +1,10 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim}; use editor::{ - char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind, - DisplayPoint, + char_kind, + display_map::DisplaySnapshot, + movement::{self, FindRange}, + scroll::autoscroll::Autoscroll, + CharKind, DisplayPoint, }; use gpui::WindowContext; use language::Selection; @@ -96,12 +99,15 @@ fn expand_changed_word_selection( .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); + selection.end = + movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| { + 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 - }); + left_kind != right_kind && left_kind != CharKind::Whitespace + }); true } else { Motion::NextWordStart { ignore_punctuation } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index dd922e7af6..94906a1e80 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1,6 +1,11 @@ use std::ops::Range; -use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint}; +use editor::{ + char_kind, + display_map::DisplaySnapshot, + movement::{self, FindRange}, + Bias, CharKind, DisplayPoint, +}; use gpui::{actions, impl_actions, AppContext, WindowContext}; use language::Selection; use serde::Deserialize; @@ -178,15 +183,16 @@ fn in_word( ) -> 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( + let start = movement::find_preceding_boundary( map, right(map, relative_to, 1), + movement::FindRange::SingleLine, |left, right| { 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| { + let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { char_kind(language, left).coerce_punctuation(ignore_punctuation) != char_kind(language, right).coerce_punctuation(ignore_punctuation) }); @@ -241,9 +247,10 @@ fn around_next_word( ) -> 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( + let start = movement::find_preceding_boundary( map, right(map, relative_to, 1), + FindRange::SingleLine, |left, right| { char_kind(language, left).coerce_punctuation(ignore_punctuation) != char_kind(language, right).coerce_punctuation(ignore_punctuation) @@ -251,7 +258,7 @@ fn around_next_word( ); let mut word_found = false; - let end = movement::find_boundary(map, relative_to, |left, right| { + let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| { let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); @@ -566,11 +573,18 @@ mod test { async fn test_visual_word_object(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("The quick ˇbrown\nfox").await; + /* + cx.set_shared_state("The quick ˇbrown\nfox").await; + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state("The quick «bˇ»rown\nfox").await; + cx.simulate_shared_keystrokes(["i", "w"]).await; + cx.assert_shared_state("The quick «brownˇ»\nfox").await; + */ + cx.set_shared_state("The quick brown\nˇ\nfox").await; cx.simulate_shared_keystrokes(["v"]).await; - cx.assert_shared_state("The quick «bˇ»rown\nfox").await; + cx.assert_shared_state("The quick brown\n«\nˇ»fox").await; cx.simulate_shared_keystrokes(["i", "w"]).await; - cx.assert_shared_state("The quick «brownˇ»\nfox").await; + cx.assert_shared_state("The quick brown\n«\nˇ»fox").await; cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS) .await; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 88fa375851..c6a212d77f 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -431,6 +431,24 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { twelve char "}) .await; + + // line wraps as: + // fourteen ch + // ar + // fourteen ch + // ar + cx.set_shared_state(indoc! { " + fourteen chaˇr + fourteen char + "}) + .await; + + cx.simulate_shared_keystrokes(["d", "i", "w"]).await; + cx.assert_shared_state(indoc! {" + fourteenˇ• + fourteen char + "}) + .await; } #[gpui::test] diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index d04b1b7768..b433a6bfc0 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -153,6 +153,7 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn assert_shared_state(&mut self, marked_text: &str) { + let marked_text = marked_text.replace("•", " "); let neovim = self.neovim_state().await; let editor = self.editor_state(); if neovim == marked_text && neovim == editor { @@ -184,9 +185,9 @@ impl<'a> NeovimBackedTestContext<'a> { message, initial_state, self.recent_keystrokes.join(" "), - marked_text, - neovim, - editor + marked_text.replace(" \n", "•\n"), + neovim.replace(" \n", "•\n"), + editor.replace(" \n", "•\n") ) } diff --git a/crates/vim/test_data/test_end_of_word.json b/crates/vim/test_data/test_end_of_word.json new file mode 100644 index 0000000000..06f80dc245 --- /dev/null +++ b/crates/vim/test_data/test_end_of_word.json @@ -0,0 +1,32 @@ +{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"e"} +{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} +{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json index 0041baf969..5e1a9839e9 100644 --- a/crates/vim/test_data/test_visual_word_object.json +++ b/crates/vim/test_data/test_visual_word_object.json @@ -1,9 +1,9 @@ -{"Put":{"state":"The quick ˇbrown\nfox"}} +{"Put":{"state":"The quick brown\nˇ\nfox"}} {"Key":"v"} -{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}} +{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}} +{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json index 1ebbd4f205..1fbfc935d9 100644 --- a/crates/vim/test_data/test_wrapped_lines.json +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -48,3 +48,8 @@ {"Key":"o"} {"Key":"escape"} {"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"fourteen chaˇr\nfourteen char\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}} From 5c498c86103c390b6c5930641699fc4923ebc3a8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 11:04:48 +0200 Subject: [PATCH 100/156] Show inline assistant errors --- crates/ai/src/assistant.rs | 160 +++++++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 35 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 46756ad569..d19730172a 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -40,7 +40,7 @@ use std::{ cell::{Cell, RefCell}, cmp, env, fmt::Write, - iter, + future, iter, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -55,7 +55,7 @@ use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, searchable::Direction, - Save, ToggleZoom, Toolbar, Workspace, + Save, Toast, ToggleZoom, Toolbar, Workspace, }; actions!( @@ -290,6 +290,7 @@ impl AssistantPanel { has_focus: false, include_conversation: self.include_conversation_in_next_inline_assist, measurements: measurements.clone(), + error: None, }; cx.focus_self(); assistant @@ -331,7 +332,7 @@ impl AssistantPanel { editor: editor.downgrade(), range, highlighted_ranges: Default::default(), - inline_assistant_block_id: Some(block_id), + inline_assistant: Some((block_id, inline_assistant.clone())), code_generation: Task::ready(None), transaction_id: None, _subscriptions: vec![ @@ -477,7 +478,7 @@ impl AssistantPanel { fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { if let Some(editor) = pending_assist.editor.upgrade(cx) { - if let Some(block_id) = pending_assist.inline_assistant_block_id.take() { + if let Some((block_id, _)) = pending_assist.inline_assistant.take() { editor.update(cx, |editor, cx| { editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); }); @@ -699,22 +700,17 @@ impl AssistantPanel { pending_assist.code_generation = cx.spawn(|this, mut cx| { async move { - let _cleanup = util::defer({ - let mut cx = cx.clone(); - let this = this.clone(); - move || { - let _ = this.update(&mut cx, |this, cx| { - this.close_inline_assist(inline_assist_id, false, cx) - }); - } - }); - let mut edit_start = range.start.to_offset(&snapshot); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let diff = cx.background().spawn(async move { let chunks = strip_markdown_codeblock(response.await?.filter_map( - |message| async move { message.ok()?.choices.pop()?.delta.content }, + |message| async move { + match message { + Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)), + Err(error) => Some(Err(error)), + } + }, )); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -737,6 +733,7 @@ impl AssistantPanel { let mut new_text = String::new(); while let Some(chunk) = chunks.next().await { + let chunk = chunk?; if first_chunk && (chunk.starts_with(' ') || chunk.starts_with('\t')) { autoindent = false; } @@ -771,9 +768,17 @@ impl AssistantPanel { }); while let Some(hunks) = hunks_rx.next().await { - let editor = editor - .upgrade(&cx) - .ok_or_else(|| anyhow!("editor was dropped"))?; + let editor = if let Some(editor) = editor.upgrade(&cx) { + editor + } else { + break; + }; + + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; this.update(&mut cx, |this, cx| { let pending_assist = if let Some(pending_assist) = @@ -840,9 +845,42 @@ impl AssistantPanel { }); this.update_highlights_for_editor(&editor, cx); - })?; + }); + } + + if let Err(error) = diff.await { + this.update(&mut cx, |this, cx| { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + if let Some((_, inline_assistant)) = + pending_assist.inline_assistant.as_ref() + { + inline_assistant.update(cx, |inline_assistant, cx| { + inline_assistant.set_error(error, cx); + }); + } else if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + inline_assist_id, + format!("Inline assistant error: {}", error), + ), + cx, + ); + }) + } + })?; + } else { + let _ = this.update(&mut cx, |this, cx| { + this.close_inline_assist(inline_assist_id, false, cx) + }); } - diff.await?; anyhow::Ok(()) } @@ -2856,6 +2894,7 @@ struct InlineAssistant { has_focus: bool, include_conversation: bool, measurements: Rc>, + error: Option, } impl Entity for InlineAssistant { @@ -2868,17 +2907,42 @@ impl View for InlineAssistant { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum ErrorIcon {} let theme = theme::current(cx); Flex::row() .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) - .element() + Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned(), + ) + .with_children(if let Some(error) = self.error.as_ref() { + Some( + Svg::new("icons/circle_x_mark_12.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }) .aligned() .constrained() .dynamically({ @@ -2954,6 +3018,8 @@ impl InlineAssistant { include_conversation: self.include_conversation, }); self.confirmed = true; + self.error = None; + cx.notify(); } } @@ -2968,6 +3034,19 @@ impl InlineAssistant { }); cx.notify(); } + + fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext) { + self.error = Some(error); + self.confirmed = false; + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.set_field_editor_style( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + }); + cx.notify(); + } } // This wouldn't need to exist if we could pass parameters when rendering child views. @@ -2982,7 +3061,7 @@ struct PendingInlineAssist { editor: WeakViewHandle, range: Range, highlighted_ranges: Vec>, - inline_assistant_block_id: Option, + inline_assistant: Option<(BlockId, ViewHandle)>, code_generation: Task>, transaction_id: Option, _subscriptions: Vec, @@ -3010,23 +3089,29 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } -fn strip_markdown_codeblock(stream: impl Stream) -> impl Stream { +fn strip_markdown_codeblock( + stream: impl Stream>, +) -> impl Stream> { let mut first_line = true; let mut buffer = String::new(); let mut starts_with_fenced_code_block = false; stream.filter_map(move |chunk| { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => return future::ready(Some(Err(err))), + }; buffer.push_str(&chunk); if first_line { if buffer == "" || buffer == "`" || buffer == "``" { - return futures::future::ready(None); + return future::ready(None); } else if buffer.starts_with("```") { starts_with_fenced_code_block = true; if let Some(newline_ix) = buffer.find('\n') { buffer.replace_range(..newline_ix + 1, ""); first_line = false; } else { - return futures::future::ready(None); + return future::ready(None); } } } @@ -3050,10 +3135,10 @@ fn strip_markdown_codeblock(stream: impl Stream) -> impl Stream() .await, "Lorem ipsum dolor" ); assert_eq!( strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "Lorem ipsum dolor" ); assert_eq!( strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "```js\nLorem ipsum dolor\n```" ); assert_eq!( strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) .collect::() .await, "``\nLorem ipsum dolor\n```" ); - fn chunks(text: &str, size: usize) -> impl Stream { + fn chunks(text: &str, size: usize) -> impl Stream> { stream::iter( text.chars() .collect::>() .chunks(size) - .map(|chunk| chunk.iter().collect::()) + .map(|chunk| Ok(chunk.iter().collect::())) .collect::>(), ) } From c6f439051131ddeca55f9c9c30a7cf3554cfbe00 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 11:30:51 +0200 Subject: [PATCH 101/156] Retain search history for inline assistants This only works in-memory for now. --- crates/ai/src/assistant.rs | 132 ++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index d19730172a..fd6e6c63eb 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -7,13 +7,13 @@ use crate::{ }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use collections::{hash_map, HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ display_map::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; @@ -106,6 +106,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(InlineAssistant::confirm); cx.add_action(InlineAssistant::cancel); cx.add_action(InlineAssistant::toggle_include_conversation); + cx.add_action(InlineAssistant::move_up); + cx.add_action(InlineAssistant::move_down); } #[derive(Debug)] @@ -139,10 +141,13 @@ pub struct AssistantPanel { pending_inline_assists: HashMap, pending_inline_assist_ids_by_editor: HashMap, Vec>, include_conversation_in_next_inline_assist: bool, + inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, } impl AssistantPanel { + const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; + pub fn load( workspace: WeakViewHandle, cx: AsyncAppContext, @@ -206,6 +211,7 @@ impl AssistantPanel { pending_inline_assists: Default::default(), pending_inline_assist_ids_by_editor: Default::default(), include_conversation_in_next_inline_assist: false, + inline_prompt_history: Default::default(), _watch_saved_conversations, }; @@ -269,29 +275,16 @@ impl AssistantPanel { } else { InlineAssistKind::Transform }; - let prompt_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); - let placeholder = match assist_kind { - InlineAssistKind::Transform => "Enter transformation prompt…", - InlineAssistKind::Generate => "Enter generation prompt…", - }; - editor.set_placeholder_text(placeholder, cx); - editor - }); let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { - let assistant = InlineAssistant { - id: inline_assist_id, - prompt_editor, - confirmed: false, - has_focus: false, - include_conversation: self.include_conversation_in_next_inline_assist, - measurements: measurements.clone(), - error: None, - }; + let assistant = InlineAssistant::new( + inline_assist_id, + assist_kind, + measurements.clone(), + self.include_conversation_in_next_inline_assist, + self.inline_prompt_history.clone(), + cx, + ); cx.focus_self(); assistant }); @@ -520,6 +513,10 @@ impl AssistantPanel { return; }; + self.inline_prompt_history.push_back(user_prompt.into()); + if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { + self.inline_prompt_history.pop_front(); + } let range = pending_assist.range.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selected_text = snapshot @@ -2895,6 +2892,10 @@ struct InlineAssistant { include_conversation: bool, measurements: Rc>, error: Option, + prompt_history: VecDeque, + prompt_history_ix: Option, + pending_prompt: String, + _subscription: Subscription, } impl Entity for InlineAssistant { @@ -2995,6 +2996,54 @@ impl View for InlineAssistant { } impl InlineAssistant { + fn new( + id: usize, + kind: InlineAssistKind, + measurements: Rc>, + include_conversation: bool, + prompt_history: VecDeque, + cx: &mut ViewContext, + ) -> Self { + let prompt_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + let placeholder = match kind { + InlineAssistKind::Transform => "Enter transformation prompt…", + InlineAssistKind::Generate => "Enter generation prompt…", + }; + editor.set_placeholder_text(placeholder, cx); + editor + }); + let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events); + Self { + id, + prompt_editor, + confirmed: false, + has_focus: false, + include_conversation, + measurements, + error: None, + prompt_history, + prompt_history_ix: None, + pending_prompt: String::new(), + _subscription: subscription, + } + } + + fn handle_prompt_editor_events( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + if let editor::Event::Edited = event { + self.pending_prompt = self.prompt_editor.read(cx).text(cx); + cx.notify(); + } + } + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { cx.emit(InlineAssistantEvent::Canceled); } @@ -3047,6 +3096,43 @@ impl InlineAssistant { }); cx.notify(); } + + fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix > 0 { + self.prompt_history_ix = Some(ix - 1); + let prompt = self.prompt_history[ix - 1].clone(); + self.set_prompt(&prompt, cx); + } + } else if !self.prompt_history.is_empty() { + self.prompt_history_ix = Some(self.prompt_history.len() - 1); + let prompt = self.prompt_history[self.prompt_history.len() - 1].clone(); + self.set_prompt(&prompt, cx); + } + } + + fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix < self.prompt_history.len() - 1 { + self.prompt_history_ix = Some(ix + 1); + let prompt = self.prompt_history[ix + 1].clone(); + self.set_prompt(&prompt, cx); + } else { + self.prompt_history_ix = None; + let pending_prompt = self.pending_prompt.clone(); + self.set_prompt(&pending_prompt, cx); + } + } + } + + fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext) { + self.prompt_editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + let len = buffer.len(cx); + buffer.edit([(0..len, prompt)], None, cx); + }); + }); + } } // This wouldn't need to exist if we could pass parameters when rendering child views. From 5f6562c21448e79971250352a395aee730b420d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 12:07:58 +0200 Subject: [PATCH 102/156] Detect indentation from GPT output --- crates/ai/src/assistant.rs | 44 ++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index fd6e6c63eb..7b360534ec 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -712,7 +712,7 @@ impl AssistantPanel { futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); - let indent_len; + let mut indent_len; let indent_text; if let Some(base_indent) = base_indent { indent_len = base_indent.len; @@ -725,30 +725,43 @@ impl AssistantPanel { indent_text = ""; }; - let mut autoindent = true; - let mut first_chunk = true; + let mut first_line_len = 0; + let mut first_line_non_whitespace_char_ix = None; + let mut first_line = true; let mut new_text = String::new(); while let Some(chunk) = chunks.next().await { let chunk = chunk?; - if first_chunk && (chunk.starts_with(' ') || chunk.starts_with('\t')) { - autoindent = false; - } - - if first_chunk && autoindent { - let first_line_indent = - indent_len.saturating_sub(selection_start.column) as usize; - new_text = indent_text.repeat(first_line_indent); - } let mut lines = chunk.split('\n'); - if let Some(first_line) = lines.next() { - new_text.push_str(first_line); + if let Some(mut line) = lines.next() { + if first_line { + if first_line_non_whitespace_char_ix.is_none() { + if let Some(mut char_ix) = + line.find(|ch: char| !ch.is_whitespace()) + { + line = &line[char_ix..]; + char_ix += first_line_len; + first_line_non_whitespace_char_ix = Some(char_ix); + let first_line_indent = char_ix + .saturating_sub(selection_start.column as usize) + as usize; + new_text.push_str(&indent_text.repeat(first_line_indent)); + indent_len = indent_len.saturating_sub(char_ix as u32); + } + } + first_line_len += line.len(); + } + + if first_line_non_whitespace_char_ix.is_some() { + new_text.push_str(line); + } } for line in lines { + first_line = false; new_text.push('\n'); - if !line.is_empty() && autoindent { + if !line.is_empty() { new_text.push_str(&indent_text.repeat(indent_len as usize)); } new_text.push_str(line); @@ -757,7 +770,6 @@ impl AssistantPanel { let hunks = diff.push_new(&new_text); hunks_tx.send(hunks).await?; new_text.clear(); - first_chunk = false; } hunks_tx.send(diff.finish()).await?; From bf67d3710a3d2a5406aa490ed97b89d0902a4f90 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Aug 2023 12:08:14 +0200 Subject: [PATCH 103/156] Remove trailing backticks when assistant ends with a trailing newline --- crates/ai/src/assistant.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 7b360534ec..2aaf75ae39 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -3216,7 +3216,8 @@ fn strip_markdown_codeblock( let text = if starts_with_fenced_code_block { buffer - .strip_suffix("\n```") + .strip_suffix("\n```\n") + .or_else(|| buffer.strip_suffix("\n```")) .or_else(|| buffer.strip_suffix("\n``")) .or_else(|| buffer.strip_suffix("\n`")) .or_else(|| buffer.strip_suffix('\n')) @@ -3636,6 +3637,13 @@ mod tests { .await, "Lorem ipsum dolor" ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); assert_eq!( strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) .map(|chunk| chunk.unwrap()) From 256949bee04b2cc18f6c0c1b4c5683be25b2cd91 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 30 Aug 2023 10:56:08 -0400 Subject: [PATCH 104/156] fmt --- crates/collab_ui/src/collab_titlebar_item.rs | 67 +++++++++----------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index bd94c85f95..95b9868937 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -247,42 +247,37 @@ impl CollabTitlebarItem { ); if let Some(git_branch) = branch_prepended { ret = ret.with_child( - Flex::row() - .with_child( - Stack::new() - .with_child( - MouseEventHandler::new::( - 0, - cx, - |mouse_state, cx| { - enum BranchPopoverTooltip {} - let style = git_style - .in_state(self.branch_popover.is_some()) - .style_for(mouse_state); - Label::new(git_branch, style.text.clone()) - .contained() - .with_style(style.container.clone()) - .with_margin_right(item_spacing) - .aligned() - .left() - .with_tooltip::( - 0, - "Recent branches", - Some(Box::new(ToggleVcsMenu)), - theme.tooltip.clone(), - cx, - ) - .into_any_named("title-project-branch") - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, move |_, this, cx| { - this.toggle_vcs_menu(&Default::default(), cx) - }) - .on_click(MouseButton::Left, move |_, _, _| {}), - ) - .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), - ), + Flex::row().with_child( + Stack::new() + .with_child( + MouseEventHandler::new::(0, cx, |mouse_state, cx| { + enum BranchPopoverTooltip {} + let style = git_style + .in_state(self.branch_popover.is_some()) + .style_for(mouse_state); + Label::new(git_branch, style.text.clone()) + .contained() + .with_style(style.container.clone()) + .with_margin_right(item_spacing) + .aligned() + .left() + .with_tooltip::( + 0, + "Recent branches", + Some(Box::new(ToggleVcsMenu)), + theme.tooltip.clone(), + cx, + ) + .into_any_named("title-project-branch") + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.toggle_vcs_menu(&Default::default(), cx) + }) + .on_click(MouseButton::Left, move |_, _, _| {}), + ) + .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), + ), ) } ret.into_any() From 1da3be06463dbd6a8aec7a527aa47cb406bf777f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 30 Aug 2023 11:14:50 -0400 Subject: [PATCH 105/156] Align diagnostic icons with other statusbar icons --- assets/icons/check_circle.svg | 4 ++-- assets/icons/error.svg | 4 ++-- assets/icons/warning.svg | 7 +++--- styles/src/style_tree/status_bar.ts | 34 +++++++++++++++-------------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg index 85ba2e1f37..b48fe34631 100644 --- a/assets/icons/check_circle.svg +++ b/assets/icons/check_circle.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/error.svg b/assets/icons/error.svg index 82b9401d08..593629beee 100644 --- a/assets/icons/error.svg +++ b/assets/icons/error.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 6b3d0fd41e..e581def0d0 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1,5 +1,6 @@ - - - + + + + diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 3b5ebf2c88..b279bbac14 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -75,34 +75,36 @@ export default function status_bar(): any { icon_color_error: foreground(layer, "negative"), container_ok: { corner_radius: 6, - padding: { top: 3, bottom: 3, left: 7, right: 7 }, - }, - container_warning: { - ...diagnostic_status_container, - background: background(layer, "warning"), - border: border(layer, "warning"), - }, - container_error: { - ...diagnostic_status_container, - background: background(layer, "negative"), - border: border(layer, "negative"), + padding: { top: 2, bottom: 2, left: 6, right: 6 }, }, + container_warning: diagnostic_status_container, + container_error: diagnostic_status_container }, state: { hovered: { icon_color_ok: foreground(layer, "on"), container_ok: { - background: background(layer, "on", "hovered"), + background: background(layer, "hovered") }, container_warning: { - background: background(layer, "warning", "hovered"), - border: border(layer, "warning", "hovered"), + background: background(layer, "hovered") }, container_error: { - background: background(layer, "negative", "hovered"), - border: border(layer, "negative", "hovered"), + background: background(layer, "hovered") }, }, + clicked: { + icon_color_ok: foreground(layer, "on"), + container_ok: { + background: background(layer, "pressed") + }, + container_warning: { + background: background(layer, "pressed") + }, + container_error: { + background: background(layer, "pressed") + } + } }, }), panel_buttons: { From 7e5735c8f1ecef5336b8f68ce6ad08b9a6880b8d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 30 Aug 2023 18:41:41 +0300 Subject: [PATCH 106/156] Reap overly long LSP requests with a 2m timeout Co-authored-by: Julia Risley --- crates/lsp/src/lsp.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index f39d97aeb5..6a9e48b481 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -4,7 +4,7 @@ pub use lsp_types::*; use anyhow::{anyhow, Context, Result}; use collections::HashMap; -use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; +use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt}; use gpui::{executor, AsyncAppContext, Task}; use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; @@ -26,12 +26,14 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, Weak, }, + time::{Duration, Instant}, }; use std::{path::Path, process::Stdio}; use util::{ResultExt, TryFutureExt}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; +const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; @@ -697,7 +699,7 @@ impl LanguageServer { outbound_tx: &channel::Sender, executor: &Arc, params: T::Params, - ) -> impl 'static + Future> + ) -> impl 'static + Future> where T::Result: 'static + Send, { @@ -738,10 +740,25 @@ impl LanguageServer { .try_send(message) .context("failed to write to language server's stdin"); + let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse(); + let started = Instant::now(); async move { handle_response?; send?; - rx.await? + + let method = T::METHOD; + futures::select! { + response = rx.fuse() => { + let elapsed = started.elapsed(); + log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}"); + response? + } + + _ = timeout => { + log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}"); + anyhow::bail!("LSP request timeout"); + } + } } } From 7204c245ea1c301be0be84bfc13bc92e68509ae2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 30 Aug 2023 13:41:09 -0400 Subject: [PATCH 107/156] v0.103.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e87ff3d991..d744f92ce8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9702,7 +9702,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.102.0" +version = "0.103.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2a97764647..66d55b38f0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.102.0" +version = "0.103.0" publish = false [lib] From feb7a8a0f44010dce20bae4472bd9e0befda1c91 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 30 Aug 2023 14:37:55 -0400 Subject: [PATCH 108/156] collab 0.19.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d744f92ce8..ca00e69416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,7 +1453,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8adc38615c..914e3f2dfb 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.18.0" +version = "0.19.0" publish = false [[bin]] From 46429426ef01d76f5a99ad7c13f9a5e1396d84ba Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 13:14:06 -0700 Subject: [PATCH 109/156] Avoid accidental gpui transitive dependency in collab * Make Fs depend on Text, not vise versa Co-authored-by: Joseph Co-authored-by: Mikayla --- Cargo.lock | 2 +- crates/collab/src/tests/integration_tests.rs | 4 +- .../src/tests/randomized_integration_tests.rs | 2 +- crates/copilot/src/copilot.rs | 2 +- crates/fs/Cargo.toml | 1 + crates/fs/src/fs.rs | 67 +------------------ crates/language/src/buffer.rs | 1 - crates/language/src/buffer_tests.rs | 2 +- crates/language/src/language.rs | 1 + crates/language/src/proto.rs | 12 ++-- crates/project/src/lsp_command.rs | 2 +- crates/project/src/project_tests.rs | 4 +- crates/project/src/worktree.rs | 4 +- crates/text/Cargo.toml | 1 - crates/text/src/text.rs | 67 ++++++++++++++++++- 15 files changed, 85 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca00e69416..91a8a12eac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2768,6 +2768,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", + "text", "time 0.3.27", "util", ] @@ -7636,7 +7637,6 @@ dependencies = [ "ctor", "digest 0.9.0", "env_logger 0.9.3", - "fs", "gpui", "lazy_static", "log", diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index f64a82e32e..2613d01131 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -9,7 +9,7 @@ use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, }; -use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; +use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, @@ -19,7 +19,7 @@ use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, OffsetRangeExt, Point, Rope, + LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 814f248b6d..e48753ed41 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -762,7 +762,7 @@ async fn apply_client_operation( client .fs() - .save(&path, &content.as_str().into(), fs::LineEnding::Unix) + .save(&path, &content.as_str().into(), text::LineEnding::Unix) .await .unwrap(); } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 427134894f..499ae2e808 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1188,7 +1188,7 @@ mod tests { _: u64, _: &clock::Global, _: language::RopeFingerprint, - _: ::fs::LineEnding, + _: language::LineEnding, _: std::time::SystemTime, _: &mut AppContext, ) { diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index b3ebd224b0..7584dec21a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -12,6 +12,7 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rope = { path = "../rope" } +text = { path = "../text" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } rpc = { path = "../rpc" } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index ec8a249ff4..ecaee4534e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -4,14 +4,10 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; use git2::Repository as LibGitRepository; -use lazy_static::lazy_static; use parking_lot::Mutex; -use regex::Regex; use repository::GitRepository; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; -use std::borrow::Cow; -use std::cmp; use std::io::Write; use std::sync::Arc; use std::{ @@ -22,6 +18,7 @@ use std::{ time::{Duration, SystemTime}, }; use tempfile::NamedTempFile; +use text::LineEnding; use util::ResultExt; #[cfg(any(test, feature = "test-support"))] @@ -33,66 +30,6 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; -lazy_static! { - static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum LineEnding { - Unix, - Windows, -} - -impl Default for LineEnding { - fn default() -> Self { - #[cfg(unix)] - return Self::Unix; - - #[cfg(not(unix))] - return Self::CRLF; - } -} - -impl LineEnding { - pub fn as_str(&self) -> &'static str { - match self { - LineEnding::Unix => "\n", - LineEnding::Windows => "\r\n", - } - } - - pub fn detect(text: &str) -> Self { - let mut max_ix = cmp::min(text.len(), 1000); - while !text.is_char_boundary(max_ix) { - max_ix -= 1; - } - - if let Some(ix) = text[..max_ix].find(&['\n']) { - if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { - Self::Windows - } else { - Self::Unix - } - } else { - Self::default() - } - } - - pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { - *text = replaced; - } - } - - pub fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } -} - #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; @@ -520,7 +457,7 @@ impl FakeFsState { } #[cfg(any(test, feature = "test-support"))] -lazy_static! { +lazy_static::lazy_static! { pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git"); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4310f84830..8adf6f6421 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -15,7 +15,6 @@ use crate::{ }; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; -use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use lsp::LanguageServerId; diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index db3749aa25..3bedf5b7a8 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -5,7 +5,6 @@ use crate::language_settings::{ use super::*; use clock::ReplicaId; use collections::BTreeMap; -use fs::LineEnding; use gpui::{AppContext, ModelHandle}; use indoc::indoc; use proto::deserialize_operation; @@ -20,6 +19,7 @@ use std::{ time::{Duration, Instant}, }; use text::network::Network; +use text::LineEnding; use unindent::Unindent as _; use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7a9e6b83ce..89d0592627 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -57,6 +57,7 @@ pub use diagnostic_set::DiagnosticEntry; pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo}; +pub use text::LineEnding; pub use tree_sitter::{Parser, Tree}; pub fn init(cx: &mut AppContext) { diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 09c5ec7fc3..cf5465a601 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -20,17 +20,17 @@ pub fn deserialize_fingerprint(fingerprint: &str) -> Result { .map_err(|error| anyhow!("invalid fingerprint: {}", error)) } -pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding { +pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding { match message { - proto::LineEnding::Unix => fs::LineEnding::Unix, - proto::LineEnding::Windows => fs::LineEnding::Windows, + proto::LineEnding::Unix => text::LineEnding::Unix, + proto::LineEnding::Windows => text::LineEnding::Windows, } } -pub fn serialize_line_ending(message: fs::LineEnding) -> proto::LineEnding { +pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding { match message { - fs::LineEnding::Unix => proto::LineEnding::Unix, - fs::LineEnding::Windows => proto::LineEnding::Windows, + text::LineEnding::Unix => proto::LineEnding::Unix, + text::LineEnding::Windows => proto::LineEnding::Windows, } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8239cf8690..6b10ed26c1 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -6,7 +6,6 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; -use fs::LineEnding; use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ @@ -19,6 +18,7 @@ use language::{ }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; +use text::LineEnding; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { lsp::FormattingOptions { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7c5983a0a9..397223c4bb 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,11 +1,11 @@ use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; -use fs::{FakeFs, LineEnding, RealFs}; +use fs::{FakeFs, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; use language::{ language_settings::{AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, - OffsetRangeExt, Point, ToPoint, + LineEnding, OffsetRangeExt, Point, ToPoint, }; use lsp::Url; use parking_lot::Mutex; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e6e0f37cc7..2de3671033 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -8,7 +8,7 @@ use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, - Fs, LineEnding, + Fs, }; use futures::{ channel::{ @@ -27,7 +27,7 @@ use language::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, serialize_version, }, - Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped, + Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped, }; use lsp::LanguageServerId; use parking_lot::Mutex; diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index adec2d4fd0..65e9b6fcec 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -14,7 +14,6 @@ test-support = ["rand"] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -fs = { path = "../fs" } rope = { path = "../rope" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 6a00ea12db..2fabb0f87f 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -14,17 +14,17 @@ pub use anchor::*; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; use collections::{HashMap, HashSet}; -use fs::LineEnding; use locator::Locator; use operation_queue::OperationQueue; pub use patch::Patch; use postage::{oneshot, prelude::*}; +use lazy_static::lazy_static; +use regex::Regex; pub use rope::*; pub use selection::*; -use util::ResultExt; - use std::{ + borrow::Cow, cmp::{self, Ordering, Reverse}, future::Future, iter::Iterator, @@ -37,10 +37,15 @@ pub use subscription::*; pub use sum_tree::Bias; use sum_tree::{FilterCursor, SumTree, TreeMap}; use undo_map::UndoMap; +use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; +lazy_static! { + static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); +} + pub type TransactionId = clock::Local; pub struct Buffer { @@ -2671,3 +2676,59 @@ impl FromAnchor for usize { snapshot.summary_for_anchor(anchor) } } + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineEnding { + Unix, + Windows, +} + +impl Default for LineEnding { + fn default() -> Self { + #[cfg(unix)] + return Self::Unix; + + #[cfg(not(unix))] + return Self::CRLF; + } +} + +impl LineEnding { + pub fn as_str(&self) -> &'static str { + match self { + LineEnding::Unix => "\n", + LineEnding::Windows => "\r\n", + } + } + + pub fn detect(text: &str) -> Self { + let mut max_ix = cmp::min(text.len(), 1000); + while !text.is_char_boundary(max_ix) { + max_ix -= 1; + } + + if let Some(ix) = text[..max_ix].find(&['\n']) { + if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { + Self::Windows + } else { + Self::Unix + } + } else { + Self::default() + } + } + + pub fn normalize(text: &mut String) { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { + *text = replaced; + } + } + + pub fn normalize_arc(text: Arc) -> Arc { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { + replaced.into() + } else { + text + } + } +} From e0bdd857f15a1320669baf7d88c55aef8f43ab76 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 14:14:18 -0700 Subject: [PATCH 110/156] Fix cursor colors of non-followed collaborators Co-authored-by: Mikayla --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 62f4c8c806..90fe6ccc52 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2251,7 +2251,7 @@ impl Element for EditorElement { let replica_id = if let Some(mapping) = &editor.replica_id_mapping { mapping.get(&replica_id).copied() } else { - None + Some(replica_id) }; // The local selections match the leader's selections. From 123bc85a8e8cca52fc8ce1365d8ed6ac6f3f4c7a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 14:23:12 -0700 Subject: [PATCH 111/156] collab panel: Make screen share row line up with shared projects Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 59 ++++++---------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0593bfcb1f..79e33c5048 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1106,23 +1106,17 @@ impl CollabPanel { ) -> AnyElement { enum OpenSharedScreen {} - let font_cache = cx.font_cache(); - let host_avatar_height = theme + let host_avatar_width = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; MouseEventHandler::new::( peer_id.as_u64() as usize, cx, - |mouse_state, _| { + |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -1130,49 +1124,20 @@ impl CollabPanel { .style_for(mouse_state); Flex::row() - .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - })) - .constrained() - .with_width(host_avatar_height), - ) + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) .with_child( Svg::new("icons/disable_screen_sharing_12.svg") - .with_color(row.icon.color) + .with_color(theme.channel_hash.color) .constrained() - .with_width(row.icon.width) + .with_width(theme.channel_hash.width) .aligned() - .left() - .contained() - .with_style(row.icon.container), + .left(), ) .with_child( Label::new("Screen", row.name.text.clone()) From f9dffc1734e28ccb90ecdd6b39b340e39fa86a3a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 14:35:02 -0700 Subject: [PATCH 112/156] Remove unnecessary ConstrainedBoxes in collab panel Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 79e33c5048..daaa483975 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2518,27 +2518,16 @@ impl View for CollabPanel { .with_child( Flex::column() .with_child( - Flex::row() - .with_child( - ChildView::new(&self.filter_editor, cx) - .contained() - .with_style(theme.user_query_editor.container) - .flex(1.0, true), - ) - .constrained() - .with_width(self.size(cx)), - ) - .with_child( - List::new(self.list_state.clone()) - .constrained() - .with_width(self.size(cx)) - .flex(1., true) - .into_any(), + Flex::row().with_child( + ChildView::new(&self.filter_editor, cx) + .contained() + .with_style(theme.user_query_editor.container) + .flex(1.0, true), + ), ) + .with_child(List::new(self.list_state.clone()).flex(1., true).into_any()) .contained() .with_style(theme.container) - .constrained() - .with_width(self.size(cx)) .into_any(), ) .with_children( From ab49f8c592db243e426b6104dcb0f402d1b8473a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 00:37:00 +0300 Subject: [PATCH 113/156] Rewrite inlay hint collab tests to remove races --- crates/collab/src/tests/integration_tests.rs | 179 +++++++------------ 1 file changed, 67 insertions(+), 112 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 2613d01131..8121b0ac91 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -33,7 +33,7 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::{ - atomic::{AtomicBool, AtomicU32, Ordering::SeqCst}, + atomic::{self, AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, }; @@ -7799,7 +7799,7 @@ async fn test_on_input_format_from_guest_to_host( }); } -#[gpui::test] +#[gpui::test(iterations = 10)] async fn test_mutual_editor_inlay_hint_cache_update( deterministic: Arc, cx_a: &mut TestAppContext, @@ -7913,30 +7913,27 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); // Set up the language server to return an additional inlay hint on each request. - let next_call_id = Arc::new(AtomicU32::new(0)); + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); fake_language_server .handle_request::(move |params, _| { - let task_next_call_id = Arc::clone(&next_call_id); + let task_edits_made = Arc::clone(&closure_edits_made); async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let call_count = task_next_call_id.fetch_add(1, SeqCst); - Ok(Some( - (0..=call_count) - .map(|ix| lsp::InlayHint { - position: lsp::Position::new(0, ix), - label: lsp::InlayHintLabel::String(ix.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }) - .collect(), - )) + let edits_made = task_edits_made.load(atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) } }) .next() @@ -7945,17 +7942,17 @@ async fn test_mutual_editor_inlay_hint_cache_update( deterministic.run_until_parked(); - let mut edits_made = 1; + let initial_edit = edits_made.load(atomic::Ordering::Acquire); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec!["0".to_string()], + vec![initial_edit.to_string()], extract_hint_labels(editor), "Host should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 1, "Host editor update the cache version after every cache/view change", ); }); @@ -7972,144 +7969,104 @@ async fn test_mutual_editor_inlay_hint_cache_update( deterministic.run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string()], + vec![initial_edit.to_string()], extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 1, "Guest editor update the cache version after every cache/view change" ); }); + let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; editor_b.update(cx_b, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); editor.handle_input(":", cx); cx.focus(&editor_b); - edits_made += 1; }); deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string() - ], + vec![after_client_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 2); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(), "2".to_string(),], + vec![after_client_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 2); }); + let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; editor_a.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.handle_input("a change to increment both buffers' versions", cx); cx.focus(&editor_a); - edits_made += 1; }); deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string() - ], + vec![after_host_edit.to_string()], extract_hint_labels(editor), - "Host should get hints from 3rd edit, 5th LSP query: \ -4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 3); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - ], + vec![after_host_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 3); }); + let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; fake_language_server .request::(()) .await .expect("inlay refresh request failed"); - edits_made += 1; deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - "6".to_string(), - ], + vec![after_special_edit_for_refresh.to_string()], extract_hint_labels(editor), - "Host should react to /refresh LSP request and get new hints from 7th LSP query" + "Host should react to /refresh LSP request" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 4, "Host should accepted all edits and bump its cache version every time" ); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - "6".to_string(), - "7".to_string(), - ], + vec![after_special_edit_for_refresh.to_string()], extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query" + "Guest should get a /refresh LSP request propagated by host" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 4, "Guest should accepted all edits and bump its cache version every time" ); }); } -#[gpui::test] +#[gpui::test(iterations = 10)] async fn test_inlay_hint_refresh_is_forwarded( deterministic: Arc, cx_a: &mut TestAppContext, @@ -8223,35 +8180,34 @@ async fn test_inlay_hint_refresh_is_forwarded( .downcast::() .unwrap(); + let other_hints = Arc::new(AtomicBool::new(false)); let fake_language_server = fake_language_servers.next().await.unwrap(); - let next_call_id = Arc::new(AtomicU32::new(0)); + let closure_other_hints = Arc::clone(&other_hints); fake_language_server .handle_request::(move |params, _| { - let task_next_call_id = Arc::clone(&next_call_id); + let task_other_hints = Arc::clone(&closure_other_hints); async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); - let mut new_hints = Vec::with_capacity(current_call_id as usize); - loop { - new_hints.push(lsp::InlayHint { - position: lsp::Position::new(0, current_call_id), - label: lsp::InlayHintLabel::String(current_call_id.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }); - if current_call_id == 0 { - break; - } - current_call_id -= 1; - } - Ok(Some(new_hints)) + let other_hints = task_other_hints.load(atomic::Ordering::Acquire); + let character = if other_hints { 0 } else { 2 }; + let label = if other_hints { + "other hint" + } else { + "initial hint" + }; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, character), + label: lsp::InlayHintLabel::String(label.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) } }) .next() @@ -8270,26 +8226,26 @@ async fn test_inlay_hint_refresh_is_forwarded( assert_eq!( inlay_cache.version(), 0, - "Host should not increment its cache version due to no changes", + "Turned off hints should not generate version updates" ); }); - let mut edits_made = 1; cx_b.foreground().run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string()], + vec!["initial hint".to_string()], extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, - "Guest editor update the cache version after every cache/view change" + 1, + "Should update cache verison after first hints" ); }); + other_hints.fetch_or(true, atomic::Ordering::Release); fake_language_server .request::(()) .await @@ -8304,22 +8260,21 @@ async fn test_inlay_hint_refresh_is_forwarded( assert_eq!( inlay_cache.version(), 0, - "Host should not increment its cache version due to no changes", + "Turned off hints should not generate version updates, again" ); }); - edits_made += 1; cx_b.foreground().run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(),], + vec!["other hint".to_string()], extract_hint_labels(editor), "Guest should get a /refresh LSP request propagated by host despite host hints are off" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 2, "Guest should accepted all edits and bump its cache version every time" ); }); From 4b5948e00404259eea7fd8ecc9c37844d84f8616 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Aug 2023 15:05:47 -0700 Subject: [PATCH 114/156] Disable save as prompt for channel notes --- crates/collab_ui/src/channel_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index a34f10b2db..5086cc8b37 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -213,7 +213,7 @@ impl Item for ChannelView { } fn is_singleton(&self, _cx: &AppContext) -> bool { - true + false } fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { From 54e7e2f59db54e053dbde4e8f2d3b6c932d5b676 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 30 Aug 2023 17:38:30 +0300 Subject: [PATCH 115/156] Capture language servers' stderr into server logs --- crates/language_tools/src/lsp_log.rs | 74 ++++++++++++++---------- crates/lsp/src/lsp.rs | 85 ++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 47 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 51bdb4c5ce..60c4e41666 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -12,6 +12,7 @@ use gpui::{ ViewHandle, WeakModelHandle, }; use language::{Buffer, LanguageServerId, LanguageServerName}; +use lsp::IoKind; use project::{Project, Worktree}; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; @@ -26,7 +27,7 @@ const RECEIVE_LINE: &str = "// Receive:\n"; pub struct LogStore { projects: HashMap, ProjectState>, - io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, bool, String)>, + io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, IoKind, String)>, } struct ProjectState { @@ -37,12 +38,12 @@ struct ProjectState { struct LanguageServerState { log_buffer: ModelHandle, rpc_state: Option, + _subscription: Option, } struct LanguageServerRpcState { buffer: ModelHandle, last_message_kind: Option, - _subscription: lsp::Subscription, } pub struct LspLogView { @@ -118,11 +119,11 @@ impl LogStore { io_tx, }; cx.spawn_weak(|this, mut cx| async move { - while let Some((project, server_id, is_output, mut message)) = io_rx.next().await { + while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { message.push('\n'); - this.on_io(project, server_id, is_output, &message, cx); + this.on_io(project, server_id, io_kind, &message, cx); }); } } @@ -168,22 +169,29 @@ impl LogStore { cx: &mut ModelContext, ) -> Option> { let project_state = self.projects.get_mut(&project.downgrade())?; - Some( - project_state - .servers - .entry(id) - .or_insert_with(|| { - cx.notify(); - LanguageServerState { - rpc_state: None, - log_buffer: cx - .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) - .clone(), - } - }) - .log_buffer - .clone(), - ) + let server_state = project_state.servers.entry(id).or_insert_with(|| { + cx.notify(); + LanguageServerState { + rpc_state: None, + log_buffer: cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) + .clone(), + _subscription: None, + } + }); + + let server = project.read(cx).language_server_for_id(id); + let weak_project = project.downgrade(); + let io_tx = self.io_tx.clone(); + server_state._subscription = server.map(|server| { + server.on_io(move |io_kind, message| { + io_tx + .unbounded_send((weak_project, id, io_kind, message.to_string())) + .ok(); + }) + }); + + Some(server_state.log_buffer.clone()) } fn add_language_server_log( @@ -230,7 +238,7 @@ impl LogStore { Some(server_state.log_buffer.clone()) } - pub fn enable_rpc_trace_for_language_server( + fn enable_rpc_trace_for_language_server( &mut self, project: &ModelHandle, server_id: LanguageServerId, @@ -239,9 +247,7 @@ impl LogStore { let weak_project = project.downgrade(); let project_state = self.projects.get_mut(&weak_project)?; let server_state = project_state.servers.get_mut(&server_id)?; - let server = project.read(cx).language_server_for_id(server_id)?; let rpc_state = server_state.rpc_state.get_or_insert_with(|| { - let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); cx.spawn_weak({ @@ -258,11 +264,6 @@ impl LogStore { LanguageServerRpcState { buffer, last_message_kind: None, - _subscription: server.on_io(move |is_received, json| { - io_tx - .unbounded_send((weak_project, server_id, is_received, json.to_string())) - .ok(); - }), } }); Some(rpc_state.buffer.clone()) @@ -285,10 +286,25 @@ impl LogStore { &mut self, project: WeakModelHandle, language_server_id: LanguageServerId, - is_received: bool, + io_kind: IoKind, message: &str, cx: &mut AppContext, ) -> Option<()> { + let is_received = match io_kind { + IoKind::StdOut => true, + IoKind::StdIn => false, + IoKind::StdErr => { + let project = project.upgrade(cx)?; + project.update(cx, |_, cx| { + cx.emit(project::Event::LanguageServerLog( + language_server_id, + format!("stderr: {}\n", message.trim()), + )) + }); + return Some(()); + } + }; + let state = self .projects .get_mut(&project)? diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d49dafff2f..2abe0baefa 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -35,7 +35,14 @@ const CONTENT_LEN_HEADER: &str = "Content-Length: "; type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; -type IoHandler = Box; +type IoHandler = Box; + +#[derive(Debug, Clone, Copy)] +pub enum IoKind { + StdOut, + StdIn, + StdErr, +} #[derive(Debug, Clone, Deserialize)] pub struct LanguageServerBinary { @@ -144,16 +151,18 @@ impl LanguageServer { .args(binary.arguments) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) + .stderr(Stdio::piped()) .kill_on_drop(true) .spawn()?; let stdin = server.stdin.take().unwrap(); - let stout = server.stdout.take().unwrap(); + let stdout = server.stdout.take().unwrap(); + let stderr = server.stderr.take().unwrap(); let mut server = Self::new_internal( server_id.clone(), stdin, - stout, + stdout, + stderr, Some(server), root_path, code_action_kinds, @@ -181,10 +190,11 @@ impl LanguageServer { Ok(server) } - fn new_internal( + fn new_internal( server_id: LanguageServerId, stdin: Stdin, stdout: Stdout, + stderr: Stderr, server: Option, root_path: &Path, code_action_kinds: Option>, @@ -194,7 +204,8 @@ impl LanguageServer { where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, - F: FnMut(AnyNotification) + 'static + Send, + Stderr: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send + Clone, { let (outbound_tx, outbound_rx) = channel::unbounded::(); let (output_done_tx, output_done_rx) = barrier::channel(); @@ -203,17 +214,26 @@ impl LanguageServer { let response_handlers = Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default()))); let io_handlers = Arc::new(Mutex::new(HashMap::default())); - let input_task = cx.spawn(|cx| { - Self::handle_input( - stdout, - on_unhandled_notification, - notification_handlers.clone(), - response_handlers.clone(), - io_handlers.clone(), - cx, - ) + + let stdout_input_task = cx.spawn(|cx| { + { + Self::handle_input( + stdout, + on_unhandled_notification.clone(), + notification_handlers.clone(), + response_handlers.clone(), + io_handlers.clone(), + cx, + ) + } .log_err() }); + let stderr_input_task = + cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err()); + let input_task = cx.spawn(|_| async move { + let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); + stdout.or(stderr) + }); let output_task = cx.background().spawn({ Self::handle_output( stdin, @@ -284,7 +304,7 @@ impl LanguageServer { if let Ok(message) = str::from_utf8(&buffer) { log::trace!("incoming message:{}", message); for handler in io_handlers.lock().values_mut() { - handler(true, message); + handler(IoKind::StdOut, message); } } @@ -327,6 +347,30 @@ impl LanguageServer { } } + async fn handle_stderr( + stderr: Stderr, + io_handlers: Arc>>, + ) -> anyhow::Result<()> + where + Stderr: AsyncRead + Unpin + Send + 'static, + { + let mut stderr = BufReader::new(stderr); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stderr.read_until(b'\n', &mut buffer).await?; + if let Ok(message) = str::from_utf8(&buffer) { + log::trace!("incoming stderr message:{message}"); + for handler in io_handlers.lock().values_mut() { + handler(IoKind::StdErr, message); + } + } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; + } + } + async fn handle_output( stdin: Stdin, outbound_rx: channel::Receiver, @@ -348,7 +392,7 @@ impl LanguageServer { while let Ok(message) = outbound_rx.recv().await { log::trace!("outgoing message:{}", message); for handler in io_handlers.lock().values_mut() { - handler(false, &message); + handler(IoKind::StdIn, &message); } content_len_buffer.clear(); @@ -532,7 +576,7 @@ impl LanguageServer { #[must_use] pub fn on_io(&self, f: F) -> Subscription where - F: 'static + Send + FnMut(bool, &str), + F: 'static + Send + FnMut(IoKind, &str), { let id = self.next_id.fetch_add(1, SeqCst); self.io_handlers.lock().insert(id, Box::new(f)); @@ -845,12 +889,16 @@ impl LanguageServer { ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); + // writers will be dropped after we exit, so readers will also be noop for the fake servers + let (_stderr_writer, stderr_reader) = async_pipe::pipe(); + let (_stderr_writer_2, stderr_reader_2) = async_pipe::pipe(); let (notifications_tx, notifications_rx) = channel::unbounded(); let server = Self::new_internal( LanguageServerId(0), stdin_writer, stdout_reader, + stderr_reader, None, Path::new("/"), None, @@ -862,6 +910,7 @@ impl LanguageServer { LanguageServerId(0), stdout_writer, stdin_reader, + stderr_reader_2, None, Path::new("/"), None, From 0f619e0b6750b1f08dee6cfd2ea6b108a2e59d65 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 01:00:37 +0300 Subject: [PATCH 116/156] Do not write TRACE logs into file for Lua --- crates/zed/src/languages/lua.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 7c5c7179d0..45d520df27 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -6,7 +6,7 @@ use futures::{io::BufReader, StreamExt}; use language::{LanguageServerName, LspAdapterDelegate}; use lsp::LanguageServerBinary; use smol::fs; -use std::{any::Any, env::consts, ffi::OsString, path::PathBuf}; +use std::{any::Any, env::consts, path::PathBuf}; use util::{ async_iife, github::{latest_github_release, GitHubLspBinaryVersion}, @@ -16,13 +16,6 @@ use util::{ #[derive(Copy, Clone)] pub struct LuaLspAdapter; -fn server_binary_arguments() -> Vec { - vec![ - "--logpath=~/lua-language-server.log".into(), - "--loglevel=trace".into(), - ] -} - #[async_trait] impl super::LspAdapter for LuaLspAdapter { async fn name(&self) -> LanguageServerName { @@ -83,7 +76,7 @@ impl super::LspAdapter for LuaLspAdapter { .await?; Ok(LanguageServerBinary { path: binary_path, - arguments: server_binary_arguments(), + arguments: Vec::new(), }) } @@ -127,7 +120,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option Date: Wed, 30 Aug 2023 21:14:39 -0400 Subject: [PATCH 117/156] Scope Tailwind in JS/TS to within string In some situations outside JSX elements Tailwind will never respond to a completion request, holding up the tsserver completions. Only submit the request to Tailwind when we wouldn't get tsserver completions anyway and don't submit to Tailwind when we know we won't get Tailwind completions Co-Authored-By: Kirill Bulatov --- crates/language/src/language.rs | 31 ++++++++++++++++++- crates/project/src/project.rs | 13 +++++++- .../zed/src/languages/javascript/config.toml | 2 ++ crates/zed/src/languages/tsx/config.toml | 2 ++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index a2d02ecd96..bb83beeeea 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -345,6 +345,8 @@ pub struct LanguageConfig { #[serde(default)] pub block_comment: Option<(Arc, Arc)>, #[serde(default)] + pub scope_opt_in_language_servers: Vec, + #[serde(default)] pub overrides: HashMap, #[serde(default)] pub word_characters: HashSet, @@ -377,6 +379,8 @@ pub struct LanguageConfigOverride { pub disabled_bracket_ixs: Vec, #[serde(default)] pub word_characters: Override>, + #[serde(default)] + pub opt_into_language_servers: Vec, } #[derive(Clone, Deserialize, Debug)] @@ -415,6 +419,7 @@ impl Default for LanguageConfig { autoclose_before: Default::default(), line_comment: Default::default(), block_comment: Default::default(), + scope_opt_in_language_servers: Default::default(), overrides: Default::default(), collapsed_placeholder: Default::default(), word_characters: Default::default(), @@ -1352,13 +1357,23 @@ impl Language { Ok(self) } - pub fn with_override_query(mut self, source: &str) -> Result { + pub fn with_override_query(mut self, source: &str) -> anyhow::Result { let query = Query::new(self.grammar_mut().ts_language, source)?; let mut override_configs_by_id = HashMap::default(); for (ix, name) in query.capture_names().iter().enumerate() { if !name.starts_with('_') { let value = self.config.overrides.remove(name).unwrap_or_default(); + for server_name in &value.opt_into_language_servers { + if !self + .config + .scope_opt_in_language_servers + .contains(server_name) + { + util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server"); + } + } + override_configs_by_id.insert(ix as u32, (name.clone(), value)); } } @@ -1597,6 +1612,20 @@ impl LanguageScope { c.is_whitespace() || self.language.config.autoclose_before.contains(c) } + pub fn language_allowed(&self, name: &LanguageServerName) -> bool { + let config = &self.language.config; + let opt_in_servers = &config.scope_opt_in_language_servers; + if opt_in_servers.iter().any(|o| *o == *name.0) { + if let Some(over) = self.config_override() { + over.opt_into_language_servers.iter().any(|o| *o == *name.0) + } else { + false + } + } else { + true + } + } + fn config_override(&self) -> Option<&LanguageConfigOverride> { let id = self.override_id?; let grammar = self.language.grammar.as_ref()?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c672a37cad..597deacd1a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4429,16 +4429,27 @@ impl Project { self.request_primary_lsp(buffer.clone(), GetHover { position }, cx) } - pub fn completions( + pub fn completions( &self, buffer: &ModelHandle, position: T, cx: &mut ModelContext, ) -> Task>> { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); let position = position.to_point_utf16(buffer.read(cx)); + + let scope = snapshot.language_scope_at(offset); + let server_ids: Vec<_> = self .language_servers_for_buffer(buffer.read(cx), cx) .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) .map(|(_, server)| server.server_id()) .collect(); diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index 7f6c6931e4..6f7ce49d3d 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -13,6 +13,7 @@ brackets = [ { start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, ] +scope_opt_in_language_servers = ["tailwindcss-language-server"] [overrides.element] line_comment = { remove = true } @@ -20,3 +21,4 @@ block_comment = ["{/* ", " */}"] [overrides.string] word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index 2f676f6710..3bdc638f73 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -12,6 +12,7 @@ brackets = [ { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, ] +scope_opt_in_language_servers = ["tailwindcss-language-server"] [overrides.element] line_comment = { remove = true } @@ -19,3 +20,4 @@ block_comment = ["{/* ", " */}"] [overrides.string] word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] From af665cc3d275e45d3f980547f824ef09b32cc8ad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 10:43:29 +0300 Subject: [PATCH 118/156] Use `ctrl-:` instead of `ctrl-shift-:` for inlay hints toggling The latter is not posible to press in Zed, since `:` is typed as `shift-;` with typical US keyboard layouts. In the end, it's the same buttons you have to press to toggle the inlay hints, but working this time. --- assets/keymaps/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7c8d7f01da..1b2d8ce419 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -522,7 +522,7 @@ // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", "cmd-alt-i": "zed::DebugElements", - "ctrl-shift-:": "editor::ToggleInlayHints", + "ctrl-:": "editor::ToggleInlayHints", } }, { From 18efc0d5e5346eed70de44516f32489cab8e75fd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 11:07:37 +0300 Subject: [PATCH 119/156] Fix the tests, by not requiring stderr for fake servers --- crates/lsp/src/lsp.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 2abe0baefa..51f48a66a0 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -162,7 +162,7 @@ impl LanguageServer { server_id.clone(), stdin, stdout, - stderr, + Some(stderr), Some(server), root_path, code_action_kinds, @@ -194,7 +194,7 @@ impl LanguageServer { server_id: LanguageServerId, stdin: Stdin, stdout: Stdout, - stderr: Stderr, + stderr: Option, server: Option, root_path: &Path, code_action_kinds: Option>, @@ -228,8 +228,9 @@ impl LanguageServer { } .log_err() }); - let stderr_input_task = - cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err()); + let stderr_input_task = stderr + .map(|stderr| cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err())) + .unwrap_or_else(|| Task::Ready(Some(None))); let input_task = cx.spawn(|_| async move { let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); stdout.or(stderr) @@ -889,16 +890,13 @@ impl LanguageServer { ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); - // writers will be dropped after we exit, so readers will also be noop for the fake servers - let (_stderr_writer, stderr_reader) = async_pipe::pipe(); - let (_stderr_writer_2, stderr_reader_2) = async_pipe::pipe(); let (notifications_tx, notifications_rx) = channel::unbounded(); let server = Self::new_internal( LanguageServerId(0), stdin_writer, stdout_reader, - stderr_reader, + None::, None, Path::new("/"), None, @@ -910,7 +908,7 @@ impl LanguageServer { LanguageServerId(0), stdout_writer, stdin_reader, - stderr_reader_2, + None::, None, Path::new("/"), None, From 9e12df43d0c2b1d4fd19bc251bfbb72e6ce99fb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 11:43:18 +0300 Subject: [PATCH 120/156] Post-rebase fixes --- Cargo.lock | 482 ++++++++++++++------------- crates/zed/src/languages/tailwind.rs | 1 - 2 files changed, 243 insertions(+), 240 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4bf4506e4..a185542c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,11 +36,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli 0.27.3", + "gimli 0.28.0", ] [[package]] @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -146,7 +146,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -157,7 +157,7 @@ dependencies = [ "alacritty_config", "alacritty_config_derive", "base64 0.13.1", - "bitflags 2.3.3", + "bitflags 2.4.0", "home", "libc", "log", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -274,9 +274,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -343,7 +343,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -357,7 +357,7 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -417,15 +417,15 @@ dependencies = [ "polling", "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -488,7 +488,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -511,7 +511,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "wasm-bindgen-futures", @@ -525,7 +525,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -536,7 +536,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -573,13 +573,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -592,7 +592,7 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tungstenite 0.16.0", ] @@ -687,12 +687,12 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.9", + "itoa", "matchit", "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -733,7 +733,7 @@ dependencies = [ "futures-util", "http", "mime", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "tokio", @@ -745,16 +745,16 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ - "addr2line 0.20.0", + "addr2line 0.21.0", "cc", "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", - "object 0.31.1", + "object 0.32.0", "rustc-demangle", ] @@ -837,7 +837,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.28", + "syn 2.0.29", "which", ] @@ -864,9 +864,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -1002,7 +1002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "serde", ] @@ -1163,11 +1163,12 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -1225,7 +1226,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", @@ -1293,9 +1294,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive 4.3.12", @@ -1304,9 +1305,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", @@ -1336,7 +1337,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1398,7 +1399,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", @@ -1505,7 +1506,7 @@ dependencies = [ "sqlx", "text", "theme", - "time 0.3.24", + "time 0.3.27", "tokio", "tokio-tungstenite", "toml 0.5.11", @@ -2047,7 +2048,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi 0.3.9", ] @@ -2068,9 +2069,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" dependencies = [ "cfg-if 1.0.0", "hashbrown 0.14.0", @@ -2128,9 +2129,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ "serde", ] @@ -2318,9 +2319,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "editor" @@ -2383,9 +2384,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if 1.0.0", ] @@ -2433,9 +2434,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" dependencies = [ "serde", ] @@ -2606,13 +2607,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "windows-sys", ] @@ -2624,9 +2625,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide 0.7.1", @@ -2768,7 +2769,7 @@ dependencies = [ "sum_tree", "tempfile", "text", - "time 0.3.24", + "time 0.3.27", "util", ] @@ -2906,7 +2907,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "waker-fn", ] @@ -2918,7 +2919,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2947,7 +2948,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "tokio-io", @@ -3025,9 +3026,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "git" @@ -3070,11 +3071,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "bstr", "fnv", "log", @@ -3161,7 +3162,7 @@ dependencies = [ "sqlez", "sum_tree", "taffy", - "time 0.3.24", + "time 0.3.27", "tiny-skia", "usvg", "util", @@ -3187,9 +3188,9 @@ checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes 1.4.0", "fnv", @@ -3383,7 +3384,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.9", + "itoa", ] [[package]] @@ -3394,7 +3395,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.4.0", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -3411,9 +3412,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human_bytes" @@ -3442,9 +3443,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.9", - "pin-project-lite 0.2.10", - "socket2", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -3458,7 +3459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tokio-io-timeout", ] @@ -3676,7 +3677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -3716,12 +3717,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.9" @@ -4148,9 +4143,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "serde", "value-bag", @@ -4280,9 +4275,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" [[package]] name = "memfd" @@ -4638,9 +4633,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -4796,9 +4791,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -4840,9 +4835,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -4861,7 +4856,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4872,9 +4867,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -5009,7 +5004,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -5101,12 +5096,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -5134,22 +5129,22 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5160,9 +5155,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -5214,7 +5209,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time 0.3.24", + "time 0.3.27", ] [[package]] @@ -5279,7 +5274,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "windows-sys", ] @@ -5329,7 +5324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5670,9 +5665,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -5905,13 +5900,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -5926,11 +5921,11 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", "regex-syntax 0.7.4", ] @@ -5979,9 +5974,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "20b9b67e2ca7dd9e9f9285b759de30ff538aab981abaaf7bc9bd90b84a0126c3" dependencies = [ "base64 0.21.2", "bytes 1.4.0", @@ -6000,7 +5995,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -6220,7 +6215,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.28", + "syn 2.0.29", "walkdir", ] @@ -6237,13 +6232,12 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.31.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ "arrayvec 0.7.4", "borsh", - "byteorder", "bytes 1.4.0", "num-traits", "rand 0.8.5", @@ -6291,7 +6285,7 @@ dependencies = [ "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes 0.5.3", - "itoa 1.0.9", + "itoa", "libc", "linux-raw-sys 0.0.42", "once_cell", @@ -6314,11 +6308,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.2", "libc", "linux-raw-sys 0.4.5", @@ -6520,7 +6514,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.24", + "time 0.3.27", "tracing", "url", "uuid 1.4.1", @@ -6548,7 +6542,7 @@ dependencies = [ "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6563,7 +6557,7 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6695,7 +6689,7 @@ dependencies = [ "smol", "tempdir", "theme", - "tiktoken-rs 0.5.0", + "tiktoken-rs 0.5.1", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6743,22 +6737,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6783,24 +6777,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] [[package]] name = "serde_json_lenient" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" +checksum = "29591aaa3a13f5ad0f2dd1a8a21bcddab11eaae7c3522b20ade2e85e9df52206" dependencies = [ - "indexmap 1.9.3", - "itoa 0.4.8", + "indexmap 2.0.0", + "itoa", "ryu", "serde", ] @@ -6813,7 +6807,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6832,7 +6826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] @@ -7044,9 +7038,9 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -7128,6 +7122,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -7227,7 +7231,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "indexmap 1.9.3", - "itoa 1.0.9", + "itoa", "libc", "libsqlite3-sys", "log", @@ -7250,7 +7254,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.24", + "time 0.3.27", "tokio-stream", "url", "uuid 1.4.1", @@ -7365,7 +7369,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7376,7 +7380,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7441,9 +7445,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -7538,14 +7542,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -7691,22 +7695,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7753,9 +7757,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a99d843674a3468b4a9200a565bbe909a0152f95e82a52feae71e6bf2d4b49d" +checksum = "2bf14cb08d8fda6e484c75ec2bfb6bcef48347d47abcd011fa9d56ee995a3da0" dependencies = [ "anyhow", "base64 0.21.2", @@ -7779,12 +7783,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.24" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" +checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" dependencies = [ "deranged", - "itoa 1.0.9", + "itoa", "serde", "time-core", "time-macros", @@ -7798,9 +7802,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" dependencies = [ "time-core", ] @@ -7849,20 +7853,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes 1.4.0", "libc", "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -7884,7 +7887,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7896,7 +7899,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7927,7 +7930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7953,7 +7956,7 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7967,7 +7970,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tracing", ] @@ -8056,7 +8059,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "rand 0.8.5", "slab", "tokio", @@ -8079,7 +8082,7 @@ dependencies = [ "http", "http-body", "http-range-header", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tower", "tower-layer", "tower-service", @@ -8105,7 +8108,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tracing-attributes", "tracing-core", ] @@ -8118,7 +8121,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -8203,9 +8206,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1bb73a4101c88775e4fefcd0543ee25e192034484a5bd45cb99eefb997dca9" +checksum = "30b03bdf218020057abee831581a74bff8c298323d6c6cd1a70556430ded9f4b" dependencies = [ "cc", "tree-sitter", @@ -8352,9 +8355,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47ebd9cac632764b2f4389b08517bf2ef895431dd163eb562e3d2062cc23a14" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" dependencies = [ "cc", "tree-sitter", @@ -8381,9 +8384,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" +checksum = "b0832309b0b2b6d33760ce5c0e818cb47e1d72b468516bfe4134408926fa7594" dependencies = [ "cc", "tree-sitter", @@ -8504,9 +8507,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -8912,7 +8915,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -8946,7 +8949,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9183,9 +9186,9 @@ dependencies = [ [[package]] name = "wast" -version = "62.0.1" +version = "63.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f" +checksum = "2560471f60a48b77fccefaf40796fda61c97ce1e790b59dfcec9dc3995c9f63a" dependencies = [ "leb128", "memchr", @@ -9195,11 +9198,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637" +checksum = "3bdc306c2c4c2f2bf2ba69e083731d0d2a77437fc6a350a19db139636e7e416c" dependencies = [ - "wast 62.0.1", + "wast 63.0.0", ] [[package]] @@ -9401,7 +9404,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9410,7 +9413,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9430,17 +9433,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -9451,9 +9454,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -9463,9 +9466,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -9475,9 +9478,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -9487,9 +9490,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -9499,9 +9502,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -9511,9 +9514,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -9523,26 +9526,27 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.2" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi 0.3.9", + "cfg-if 1.0.0", + "windows-sys", ] [[package]] @@ -9662,7 +9666,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.19", + "clap 4.3.24", "schemars", "serde_json", "theme", @@ -9845,7 +9849,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 1b7c271d10..12a0a4e3b8 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -66,7 +66,6 @@ impl LspAdapter for TailwindLspAdapter { let server_path = container_dir.join(SERVER_PATH); if fs::metadata(&server_path).await.is_err() { - dbg!(&container_dir, version.as_str()); self.node .npm_install_packages( &container_dir, From fff385a585f4e83db0bc96f403141f09ebdaf5f4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 12:16:54 +0300 Subject: [PATCH 121/156] Fix project tests --- crates/editor/src/editor_tests.rs | 8 +++++++- crates/project/src/project_tests.rs | 26 ++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a3688215db..d44b8728fd 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7712,7 +7712,13 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: ) .with_override_query("(jsx_self_closing_element) @element") .unwrap(), - Default::default(), + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, cx, ) .await; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 397223c4bb..b6adb371e1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2272,7 +2272,18 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_typescript::language_typescript()), ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -2358,7 +2369,18 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_typescript::language_typescript()), ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; let fs = FakeFs::new(cx.background()); fs.insert_tree( From 292af55ebc7ac98d26d1d3f2e8d365151b473db2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 13:41:20 +0300 Subject: [PATCH 122/156] Ensure all client LSP queries are forwarded via collab --- crates/project/src/project.rs | 106 +++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index edada5c630..091e1986f6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -156,6 +156,11 @@ struct DelayedDebounced { cancel_channel: Option>, } +enum LanguageServerToQuery { + Primary, + Other(LanguageServerId), +} + impl DelayedDebounced { fn new() -> DelayedDebounced { DelayedDebounced { @@ -4199,7 +4204,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetDefinition { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDefinition { position }, + cx, + ) } pub fn type_definition( @@ -4209,7 +4219,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetTypeDefinition { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetTypeDefinition { position }, + cx, + ) } pub fn references( @@ -4219,7 +4234,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetReferences { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetReferences { position }, + cx, + ) } pub fn document_highlights( @@ -4229,7 +4249,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetDocumentHighlights { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDocumentHighlights { position }, + cx, + ) } pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { @@ -4457,7 +4482,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer.clone(), GetHover { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetHover { position }, + cx, + ) } pub fn completions( @@ -4491,7 +4521,7 @@ impl Project { for server_id in server_ids { tasks.push(this.request_lsp( buffer.clone(), - server_id, + LanguageServerToQuery::Other(server_id), GetCompletions { position }, cx, )); @@ -4628,7 +4658,12 @@ impl Project { ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); - self.request_primary_lsp(buffer_handle.clone(), GetCodeActions { range }, cx) + self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + GetCodeActions { range }, + cx, + ) } pub fn apply_code_action( @@ -4984,7 +5019,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp(buffer, PrepareRename { position }, cx) + self.request_lsp( + buffer, + LanguageServerToQuery::Primary, + PrepareRename { position }, + cx, + ) } pub fn perform_rename( @@ -4996,8 +5036,9 @@ impl Project { cx: &mut ModelContext, ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_primary_lsp( + self.request_lsp( buffer, + LanguageServerToQuery::Primary, PerformRename { position, new_name, @@ -5023,8 +5064,9 @@ impl Project { .tab_size, ) }); - self.request_primary_lsp( + self.request_lsp( buffer.clone(), + LanguageServerToQuery::Primary, OnTypeFormatting { position, trigger, @@ -5050,7 +5092,12 @@ impl Project { let lsp_request = InlayHints { range }; if self.is_local() { - let lsp_request_task = self.request_primary_lsp(buffer_handle.clone(), lsp_request, cx); + let lsp_request_task = self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + lsp_request, + cx, + ); cx.spawn(|_, mut cx| async move { buffer_handle .update(&mut cx, |buffer, _| { @@ -5483,28 +5530,10 @@ impl Project { .await; } - fn request_primary_lsp( - &self, - buffer_handle: ModelHandle, - request: R, - cx: &mut ModelContext, - ) -> Task> - where - ::Result: Send, - { - let buffer = buffer_handle.read(cx); - let server_id = match self.primary_language_server_for_buffer(buffer, cx) { - Some((_, server)) => server.server_id(), - None => return Task::ready(Ok(Default::default())), - }; - - self.request_lsp(buffer_handle, server_id, request, cx) - } - fn request_lsp( &self, buffer_handle: ModelHandle, - server_id: LanguageServerId, + server: LanguageServerToQuery, request: R, cx: &mut ModelContext, ) -> Task> @@ -5513,11 +5542,18 @@ impl Project { { let buffer = buffer_handle.read(cx); if self.is_local() { + let language_server = match server { + LanguageServerToQuery::Primary => { + match self.primary_language_server_for_buffer(buffer, cx) { + Some((_, server)) => Some(Arc::clone(server)), + None => return Task::ready(Ok(Default::default())), + } + } + LanguageServerToQuery::Other(id) => self + .language_server_for_buffer(buffer, id, cx) + .map(|(_, server)| Arc::clone(server)), + }; let file = File::from_dyn(buffer.file()).and_then(File::as_local); - let language_server = self - .language_server_for_buffer(buffer, server_id, cx) - .map(|(_, server)| server.clone()); - if let (Some(file), Some(language_server)) = (file, language_server) { let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx); return cx.spawn(|this, cx| async move { @@ -7212,7 +7248,7 @@ impl Project { let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version()); let response = this .update(&mut cx, |this, cx| { - this.request_primary_lsp(buffer_handle, request, cx) + this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx) }) .await?; this.update(&mut cx, |this, cx| { From 5bc5831032a84b3897330264c3e5fddf1e770358 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 14:31:43 +0300 Subject: [PATCH 123/156] Fix wrong assertion in the test --- crates/editor/src/editor_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d44b8728fd..ad97639d0b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7780,7 +7780,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-blue", "bg-yellow"] + &["bg-yellow"] ); } else { panic!("expected completion menu to be open"); From e682db7101edd69e64ae4cc133e74f7de935098d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 15:22:13 +0300 Subject: [PATCH 124/156] Route completion requests through remote protocol, if needed --- crates/project/src/project.rs | 129 +++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 58 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 091e1986f6..5cd13b8be8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4496,47 +4496,52 @@ impl Project { position: T, cx: &mut ModelContext, ) -> Task>> { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); let position = position.to_point_utf16(buffer.read(cx)); + if self.is_local() { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(offset); - let scope = snapshot.language_scope_at(offset); + let server_ids: Vec<_> = self + .language_servers_for_buffer(buffer.read(cx), cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect(); - let server_ids: Vec<_> = self - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) + let buffer = buffer.clone(); + cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(server_ids.len()); + this.update(&mut cx, |this, cx| { + for server_id in server_ids { + tasks.push(this.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + GetCompletions { position }, + cx, + )); + } + }); + + let mut completions = Vec::new(); + for task in tasks { + if let Ok(new_completions) = task.await { + completions.extend_from_slice(&new_completions); + } + } + + Ok(completions) }) - .map(|(_, server)| server.server_id()) - .collect(); - - let buffer = buffer.clone(); - cx.spawn(|this, mut cx| async move { - let mut tasks = Vec::with_capacity(server_ids.len()); - this.update(&mut cx, |this, cx| { - for server_id in server_ids { - tasks.push(this.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetCompletions { position }, - cx, - )); - } - }); - - let mut completions = Vec::new(); - for task in tasks { - if let Ok(new_completions) = task.await { - completions.extend_from_slice(&new_completions); - } - } - - Ok(completions) - }) + } else if let Some(project_id) = self.remote_id() { + self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx) + } else { + Task::ready(Ok(Default::default())) + } } pub fn apply_additional_edits_for_completion( @@ -5587,32 +5592,40 @@ impl Project { }); } } else if let Some(project_id) = self.remote_id() { - let rpc = self.client.clone(); - let message = request.to_proto(project_id, buffer); - return cx.spawn_weak(|this, cx| async move { - // Ensure the project is still alive by the time the task - // is scheduled. - this.upgrade(&cx) - .ok_or_else(|| anyhow!("project dropped"))?; - - let response = rpc.request(message).await?; - - let this = this - .upgrade(&cx) - .ok_or_else(|| anyhow!("project dropped"))?; - if this.read_with(&cx, |this, _| this.is_read_only()) { - Err(anyhow!("disconnected before completing request")) - } else { - request - .response_from_proto(response, this, buffer_handle, cx) - .await - } - }); + return self.send_lsp_proto_request(buffer_handle, project_id, request, cx); } Task::ready(Ok(Default::default())) } + fn send_lsp_proto_request( + &self, + buffer: ModelHandle, + project_id: u64, + request: R, + cx: &mut ModelContext<'_, Project>, + ) -> Task::Response>> { + let rpc = self.client.clone(); + let message = request.to_proto(project_id, buffer.read(cx)); + cx.spawn_weak(|this, cx| async move { + // Ensure the project is still alive by the time the task + // is scheduled. + this.upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + let response = rpc.request(message).await?; + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + if this.read_with(&cx, |this, _| this.is_read_only()) { + Err(anyhow!("disconnected before completing request")) + } else { + request + .response_from_proto(response, this, buffer, cx) + .await + } + }) + } + fn sort_candidates_and_open_buffers( mut matching_paths_rx: Receiver, cx: &mut ModelContext, From 5731ef51cd8f11aaa17606a0b36de5c4edb8e432 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Aug 2023 15:32:24 +0300 Subject: [PATCH 125/156] Fix plugin LSP adapter intefrace --- crates/zed/src/languages/language_plugin.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index b071936392..b2405d8bb8 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -70,6 +70,10 @@ impl LspAdapter for PluginLspAdapter { LanguageServerName(name.into()) } + fn short_name(&self) -> &'static str { + "PluginLspAdapter" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, From 7b5974e8e9f0ef912cafce81ce87a59aa7351137 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 30 Aug 2023 13:04:25 +0300 Subject: [PATCH 126/156] Add LSP logs clear button --- crates/language_tools/src/lsp_log.rs | 52 ++++++++++++++++++++++++---- crates/theme/src/theme.rs | 1 + styles/src/style_tree/toolbar.ts | 9 ++++- styles/src/style_tree/workspace.ts | 2 ++ 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 60c4e41666..3275b3ee01 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -570,10 +570,12 @@ impl View for LspLogToolbarItemView { let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any(); }; - let log_view = log_view.read(cx); - let menu_rows = log_view.menu_items(cx).unwrap_or_default(); + let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| { + let menu_rows = log_view.menu_items(cx).unwrap_or_default(); + let current_server_id = log_view.current_server_id; + (menu_rows, current_server_id) + }); - let current_server_id = log_view.current_server_id; let current_server = current_server_id.and_then(|current_server_id| { if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) { Some(menu_rows[ix].clone()) @@ -583,8 +585,7 @@ impl View for LspLogToolbarItemView { }); enum Menu {} - - Stack::new() + let lsp_menu = Stack::new() .with_child(Self::render_language_server_menu_header( current_server, &theme, @@ -631,8 +632,45 @@ impl View for LspLogToolbarItemView { }) .aligned() .left() - .clipped() - .into_any() + .clipped(); + + enum LspCleanupButton {} + let log_cleanup_button = + MouseEventHandler::new::(1, cx, |state, cx| { + let theme = theme::current(cx).clone(); + let style = theme + .workspace + .toolbar + .toggleable_text_tool + .active_state() + .style_for(state); + Label::new("Clear", style.text.clone()) + .aligned() + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(log_view) = this.log_view.as_ref() { + log_view.update(cx, |log_view, cx| { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + }) + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .right(); + + Flex::row() + .with_child(lsp_menu) + .with_child(log_cleanup_button) + .contained() + .aligned() + .left() + .into_any_named("lsp log controls") } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a51f18c4db..a542249788 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -408,6 +408,7 @@ pub struct Toolbar { pub height: f32, pub item_spacing: f32, pub toggleable_tool: Toggleable>, + pub toggleable_text_tool: Toggleable>, pub breadcrumb_height: f32, pub breadcrumbs: Interactive, } diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index 7292a220a8..0145ee2785 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -1,7 +1,8 @@ import { useTheme } from "../common" import { toggleable_icon_button } from "../component/icon_button" -import { interactive } from "../element" +import { interactive, toggleable } from "../element" import { background, border, foreground, text } from "./components" +import { text_button } from "../component"; export const toolbar = () => { const theme = useTheme() @@ -34,5 +35,11 @@ export const toolbar = () => { }, }, }), + toggleable_text_tool: toggleable({ + state: { + inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: 4 }, text_properties: { size: "sm" } }), + active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: 4 }, text_properties: { size: "sm" } }) + } + }), } } diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index ba89c7b05f..8fda5e0117 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -19,6 +19,8 @@ export default function workspace(): any { const { is_light } = theme + const TOOLBAR_ITEM_SPACING = 8; + return { background: background(theme.lowest), blank_pane: { From fe2300fdaa59f12df58736f0f4cf61db2b8ee8c3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 30 Aug 2023 14:49:33 +0300 Subject: [PATCH 127/156] Style the clear button better, add border to button constructor options --- crates/language_tools/src/lsp_log.rs | 5 ++++- styles/src/component/text_button.ts | 6 ++++++ styles/src/style_tree/toolbar.ts | 17 +++++++++++++++-- styles/src/style_tree/workspace.ts | 2 -- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 3275b3ee01..a918e3d151 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -583,6 +583,7 @@ impl View for LspLogToolbarItemView { None } }); + let server_selected = current_server.is_some(); enum Menu {} let lsp_menu = Stack::new() @@ -642,12 +643,14 @@ impl View for LspLogToolbarItemView { .workspace .toolbar .toggleable_text_tool - .active_state() + .in_state(server_selected) .style_for(state); Label::new("Clear", style.text.clone()) .aligned() .contained() .with_style(style.container) + .constrained() + .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0) }) .on_click(MouseButton::Left, move |_, this, cx| { if let Some(log_view) = this.log_view.as_ref() { diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index 8333d9e81a..0e293e403a 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -1,5 +1,6 @@ import { interactive, toggleable } from "../element" import { + Border, TextProperties, background, foreground, @@ -16,6 +17,7 @@ interface TextButtonOptions { margin?: Partial disabled?: boolean text_properties?: TextProperties + border?: Border } type ToggleableTextButtonOptions = TextButtonOptions & { @@ -29,6 +31,7 @@ export function text_button({ margin, disabled, text_properties, + border, }: TextButtonOptions = {}) { const theme = useTheme() if (!color) color = "base" @@ -66,6 +69,7 @@ export function text_button({ }, state: { default: { + border, background: background_color, color: disabled ? foreground(layer ?? theme.lowest, "disabled") @@ -74,6 +78,7 @@ export function text_button({ hovered: disabled ? {} : { + border, background: background( layer ?? theme.lowest, color, @@ -88,6 +93,7 @@ export function text_button({ clicked: disabled ? {} : { + border, background: background( layer ?? theme.lowest, color, diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts index 0145ee2785..01a09a0616 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -37,8 +37,21 @@ export const toolbar = () => { }), toggleable_text_tool: toggleable({ state: { - inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: 4 }, text_properties: { size: "sm" } }), - active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: 4 }, text_properties: { size: "sm" } }) + inactive: text_button({ + disabled: true, + variant: "ghost", + layer: theme.highest, + margin: { left: 4 }, + text_properties: { size: "sm" }, + border: border(theme.middle), + }), + active: text_button({ + variant: "ghost", + layer: theme.highest, + margin: { left: 4 }, + text_properties: { size: "sm" }, + border: border(theme.middle), + }), } }), } diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 8fda5e0117..ba89c7b05f 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -19,8 +19,6 @@ export default function workspace(): any { const { is_light } = theme - const TOOLBAR_ITEM_SPACING = 8; - return { background: background(theme.lowest), blank_pane: { From eecd4e39ccb876cf4bed056385165c5e1a36e772 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Aug 2023 11:09:36 -0700 Subject: [PATCH 128/156] Propagate Cancel action if there is no pending inline assist --- crates/ai/src/assistant.rs | 48 +++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 2aaf75ae39..5cde99af82 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -406,36 +406,30 @@ impl AssistantPanel { _: &editor::Cancel, cx: &mut ViewContext, ) { - let panel = if let Some(panel) = workspace.panel::(cx) { - panel - } else { - return; - }; - let editor = if let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - editor - } else { - return; - }; - - let handled = panel.update(cx, |panel, cx| { - if let Some(assist_id) = panel - .pending_inline_assist_ids_by_editor - .get(&editor.downgrade()) - .and_then(|assist_ids| assist_ids.last().copied()) + if let Some(panel) = workspace.panel::(cx) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) { - panel.close_inline_assist(assist_id, true, cx); - true - } else { - false + let handled = panel.update(cx, |panel, cx| { + if let Some(assist_id) = panel + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .and_then(|assist_ids| assist_ids.last().copied()) + { + panel.close_inline_assist(assist_id, true, cx); + true + } else { + false + } + }); + if handled { + return; + } } - }); - - if !handled { - cx.propagate_action(); } + + cx.propagate_action(); } fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { From 03f0365d4d39134aed4f83381348f84be940f7a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Aug 2023 15:52:16 -0700 Subject: [PATCH 129/156] Remove local timestamps from CRDT operations Use lamport timestamps for everything. --- crates/clock/src/clock.rs | 81 ++-------- crates/collab/src/db/queries/buffers.rs | 62 +++----- crates/collab/src/db/queries/users.rs | 2 - crates/language/src/buffer.rs | 10 +- crates/language/src/proto.rs | 118 +++++++------- crates/rpc/proto/zed.proto | 12 +- crates/text/Cargo.toml | 1 + crates/text/src/anchor.rs | 6 +- crates/text/src/text.rs | 196 +++++++++--------------- crates/text/src/undo_map.rs | 12 +- 10 files changed, 186 insertions(+), 314 deletions(-) diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index bc936fcb99..3cbf8d6594 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -2,70 +2,17 @@ use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, fmt, iter, - ops::{Add, AddAssign}, }; pub type ReplicaId = u16; pub type Seq = u32; -#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)] -pub struct Local { - pub replica_id: ReplicaId, - pub value: Seq, -} - #[derive(Clone, Copy, Default, Eq, Hash, PartialEq)] pub struct Lamport { pub replica_id: ReplicaId, pub value: Seq, } -impl Local { - pub const MIN: Self = Self { - replica_id: ReplicaId::MIN, - value: Seq::MIN, - }; - pub const MAX: Self = Self { - replica_id: ReplicaId::MAX, - value: Seq::MAX, - }; - - pub fn new(replica_id: ReplicaId) -> Self { - Self { - replica_id, - value: 1, - } - } - - pub fn tick(&mut self) -> Self { - let timestamp = *self; - self.value += 1; - timestamp - } - - pub fn observe(&mut self, timestamp: Self) { - if timestamp.replica_id == self.replica_id { - self.value = cmp::max(self.value, timestamp.value + 1); - } - } -} - -impl<'a> Add<&'a Self> for Local { - type Output = Local; - - fn add(self, other: &'a Self) -> Self::Output { - *cmp::max(&self, other) - } -} - -impl<'a> AddAssign<&'a Local> for Local { - fn add_assign(&mut self, other: &Self) { - if *self < *other { - *self = *other; - } - } -} - /// A vector clock #[derive(Clone, Default, Hash, Eq, PartialEq)] pub struct Global(SmallVec<[u32; 8]>); @@ -79,7 +26,7 @@ impl Global { self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq } - pub fn observe(&mut self, timestamp: Local) { + pub fn observe(&mut self, timestamp: Lamport) { if timestamp.value > 0 { let new_len = timestamp.replica_id as usize + 1; if new_len > self.0.len() { @@ -126,7 +73,7 @@ impl Global { self.0.resize(new_len, 0); } - pub fn observed(&self, timestamp: Local) -> bool { + pub fn observed(&self, timestamp: Lamport) -> bool { self.get(timestamp.replica_id) >= timestamp.value } @@ -178,16 +125,16 @@ impl Global { false } - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().enumerate().map(|(replica_id, seq)| Local { + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().enumerate().map(|(replica_id, seq)| Lamport { replica_id: replica_id as ReplicaId, value: *seq, }) } } -impl FromIterator for Global { - fn from_iter>(locals: T) -> Self { +impl FromIterator for Global { + fn from_iter>(locals: T) -> Self { let mut result = Self::new(); for local in locals { result.observe(local); @@ -212,6 +159,16 @@ impl PartialOrd for Lamport { } impl Lamport { + pub const MIN: Self = Self { + replica_id: ReplicaId::MIN, + value: Seq::MIN, + }; + + pub const MAX: Self = Self { + replica_id: ReplicaId::MAX, + value: Seq::MAX, + }; + pub fn new(replica_id: ReplicaId) -> Self { Self { value: 1, @@ -230,12 +187,6 @@ impl Lamport { } } -impl fmt::Debug for Local { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Local {{{}: {}}}", self.replica_id, self.value) - } -} - impl fmt::Debug for Lamport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 354accc01a..f120aea1c5 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,6 +1,6 @@ use super::*; use prost::Message; -use text::{EditOperation, InsertionTimestamp, UndoOperation}; +use text::{EditOperation, UndoOperation}; impl Database { pub async fn join_channel_buffer( @@ -182,7 +182,6 @@ impl Database { .await } - #[cfg(debug_assertions)] pub async fn get_channel_buffer_collaborators( &self, channel_id: ChannelId, @@ -370,7 +369,6 @@ fn operation_to_storage( operation.replica_id, operation.lamport_timestamp, storage::Operation { - local_timestamp: operation.local_timestamp, version: version_to_storage(&operation.version), is_undo: false, edit_ranges: operation @@ -389,7 +387,6 @@ fn operation_to_storage( operation.replica_id, operation.lamport_timestamp, storage::Operation { - local_timestamp: operation.local_timestamp, version: version_to_storage(&operation.version), is_undo: true, edit_ranges: Vec::new(), @@ -399,7 +396,7 @@ fn operation_to_storage( .iter() .map(|entry| storage::UndoCount { replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, + lamport_timestamp: entry.lamport_timestamp, count: entry.count, }) .collect(), @@ -427,7 +424,6 @@ fn operation_from_storage( Ok(if operation.is_undo { proto::operation::Variant::Undo(proto::operation::Undo { replica_id: row.replica_id as u32, - local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, counts: operation @@ -435,7 +431,7 @@ fn operation_from_storage( .iter() .map(|entry| proto::UndoCount { replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, + lamport_timestamp: entry.lamport_timestamp, count: entry.count, }) .collect(), @@ -443,7 +439,6 @@ fn operation_from_storage( } else { proto::operation::Variant::Edit(proto::operation::Edit { replica_id: row.replica_id as u32, - local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, ranges: operation @@ -483,10 +478,9 @@ fn version_from_storage(version: &Vec) -> Vec Option { match operation.variant? { proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { - timestamp: InsertionTimestamp { + timestamp: clock::Lamport { replica_id: edit.replica_id as text::ReplicaId, - local: edit.local_timestamp, - lamport: edit.lamport_timestamp, + value: edit.lamport_timestamp, }, version: version_from_wire(&edit.version), ranges: edit @@ -498,32 +492,26 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option Some(text::Operation::Undo { - lamport_timestamp: clock::Lamport { + proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo(UndoOperation { + timestamp: clock::Lamport { replica_id: undo.replica_id as text::ReplicaId, value: undo.lamport_timestamp, }, - undo: UndoOperation { - id: clock::Local { - replica_id: undo.replica_id as text::ReplicaId, - value: undo.local_timestamp, - }, - version: version_from_wire(&undo.version), - counts: undo - .counts - .into_iter() - .map(|c| { - ( - clock::Local { - replica_id: c.replica_id as text::ReplicaId, - value: c.local_timestamp, - }, - c.count, - ) - }) - .collect(), - }, - }), + version: version_from_wire(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Lamport { + replica_id: c.replica_id as text::ReplicaId, + value: c.lamport_timestamp, + }, + c.count, + ) + }) + .collect(), + })), _ => None, } } @@ -531,7 +519,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option clock::Global { let mut version = clock::Global::new(); for entry in message { - version.observe(clock::Local { + version.observe(clock::Lamport { replica_id: entry.replica_id as text::ReplicaId, value: entry.timestamp, }); @@ -546,8 +534,6 @@ mod storage { #[derive(Message)] pub struct Operation { - #[prost(uint32, tag = "1")] - pub local_timestamp: u32, #[prost(message, repeated, tag = "2")] pub version: Vec, #[prost(bool, tag = "3")] @@ -581,7 +567,7 @@ mod storage { #[prost(uint32, tag = "1")] pub replica_id: u32, #[prost(uint32, tag = "2")] - pub local_timestamp: u32, + pub lamport_timestamp: u32, #[prost(uint32, tag = "3")] pub count: u32, } diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bd7c3e9ffd..5cb1ef6ea3 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -241,7 +241,6 @@ impl Database { result } - #[cfg(debug_assertions)] pub async fn create_user_flag(&self, flag: &str) -> Result { self.transaction(|tx| async move { let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { @@ -257,7 +256,6 @@ impl Database { .await } - #[cfg(debug_assertions)] pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { self.transaction(|tx| async move { user_feature::Entity::insert(user_feature::ActiveModel { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1ded955cd7..38b2842c12 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -439,7 +439,7 @@ impl Buffer { operations.extend( text_operations .iter() - .filter(|(_, op)| !since.observed(op.local_timestamp())) + .filter(|(_, op)| !since.observed(op.timestamp())) .map(|(_, op)| proto::serialize_operation(&Operation::Buffer(op.clone()))), ); operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation); @@ -1304,7 +1304,7 @@ impl Buffer { pub fn wait_for_edits( &mut self, - edit_ids: impl IntoIterator, + edit_ids: impl IntoIterator, ) -> impl Future> { self.text.wait_for_edits(edit_ids) } @@ -1362,7 +1362,7 @@ impl Buffer { } } - pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option + pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option where T: Into>, { @@ -1375,7 +1375,7 @@ impl Buffer { edits_iter: I, autoindent_mode: Option, cx: &mut ModelContext, - ) -> Option + ) -> Option where I: IntoIterator, T)>, S: ToOffset, @@ -1412,7 +1412,7 @@ impl Buffer { .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); let edit_operation = self.text.edit(edits.iter().cloned()); - let edit_id = edit_operation.local_timestamp(); + let edit_id = edit_operation.timestamp(); if let Some((before_edit, mode)) = autoindent_request { let mut delta = 0isize; diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c88abc08ac..80eb972f42 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -41,24 +41,22 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { proto::operation::Variant::Edit(serialize_edit_operation(edit)) } - crate::Operation::Buffer(text::Operation::Undo { - undo, - lamport_timestamp, - }) => proto::operation::Variant::Undo(proto::operation::Undo { - replica_id: undo.id.replica_id as u32, - local_timestamp: undo.id.value, - lamport_timestamp: lamport_timestamp.value, - version: serialize_version(&undo.version), - counts: undo - .counts - .iter() - .map(|(edit_id, count)| proto::UndoCount { - replica_id: edit_id.replica_id as u32, - local_timestamp: edit_id.value, - count: *count, - }) - .collect(), - }), + crate::Operation::Buffer(text::Operation::Undo(undo)) => { + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: undo.timestamp.replica_id as u32, + lamport_timestamp: undo.timestamp.value, + version: serialize_version(&undo.version), + counts: undo + .counts + .iter() + .map(|(edit_id, count)| proto::UndoCount { + replica_id: edit_id.replica_id as u32, + lamport_timestamp: edit_id.value, + count: *count, + }) + .collect(), + }) + } crate::Operation::UpdateSelections { selections, @@ -101,8 +99,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit { proto::operation::Edit { replica_id: operation.timestamp.replica_id as u32, - local_timestamp: operation.timestamp.local, - lamport_timestamp: operation.timestamp.lamport, + lamport_timestamp: operation.timestamp.value, version: serialize_version(&operation.version), ranges: operation.ranges.iter().map(serialize_range).collect(), new_text: operation @@ -114,7 +111,7 @@ pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation:: } pub fn serialize_undo_map_entry( - (edit_id, counts): (&clock::Local, &[(clock::Local, u32)]), + (edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]), ) -> proto::UndoMapEntry { proto::UndoMapEntry { replica_id: edit_id.replica_id as u32, @@ -123,7 +120,7 @@ pub fn serialize_undo_map_entry( .iter() .map(|(undo_id, count)| proto::UndoCount { replica_id: undo_id.replica_id as u32, - local_timestamp: undo_id.value, + lamport_timestamp: undo_id.value, count: *count, }) .collect(), @@ -197,7 +194,7 @@ pub fn serialize_diagnostics<'a>( pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { proto::Anchor { replica_id: anchor.timestamp.replica_id as u32, - local_timestamp: anchor.timestamp.value, + timestamp: anchor.timestamp.value, offset: anchor.offset as u64, bias: match anchor.bias { Bias::Left => proto::Bias::Left as i32, @@ -218,32 +215,26 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { - crate::Operation::Buffer(text::Operation::Undo { - lamport_timestamp: clock::Lamport { + crate::Operation::Buffer(text::Operation::Undo(UndoOperation { + timestamp: clock::Lamport { replica_id: undo.replica_id as ReplicaId, value: undo.lamport_timestamp, }, - undo: UndoOperation { - id: clock::Local { - replica_id: undo.replica_id as ReplicaId, - value: undo.local_timestamp, - }, - version: deserialize_version(&undo.version), - counts: undo - .counts - .into_iter() - .map(|c| { - ( - clock::Local { - replica_id: c.replica_id as ReplicaId, - value: c.local_timestamp, - }, - c.count, - ) - }) - .collect(), - }, - }) + version: deserialize_version(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Lamport { + replica_id: c.replica_id as ReplicaId, + value: c.lamport_timestamp, + }, + c.count, + ) + }) + .collect(), + })) } proto::operation::Variant::UpdateSelections(message) => { let selections = message @@ -298,10 +289,9 @@ pub fn deserialize_operation(message: proto::Operation) -> Result EditOperation { EditOperation { - timestamp: InsertionTimestamp { + timestamp: clock::Lamport { replica_id: edit.replica_id as ReplicaId, - local: edit.local_timestamp, - lamport: edit.lamport_timestamp, + value: edit.lamport_timestamp, }, version: deserialize_version(&edit.version), ranges: edit.ranges.into_iter().map(deserialize_range).collect(), @@ -311,9 +301,9 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation pub fn deserialize_undo_map_entry( entry: proto::UndoMapEntry, -) -> (clock::Local, Vec<(clock::Local, u32)>) { +) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) { ( - clock::Local { + clock::Lamport { replica_id: entry.replica_id as u16, value: entry.local_timestamp, }, @@ -322,9 +312,9 @@ pub fn deserialize_undo_map_entry( .into_iter() .map(|undo_count| { ( - clock::Local { + clock::Lamport { replica_id: undo_count.replica_id as u16, - value: undo_count.local_timestamp, + value: undo_count.lamport_timestamp, }, undo_count.count, ) @@ -384,9 +374,9 @@ pub fn deserialize_diagnostics( pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { Some(Anchor { - timestamp: clock::Local { + timestamp: clock::Lamport { replica_id: anchor.replica_id as ReplicaId, - value: anchor.local_timestamp, + value: anchor.timestamp, }, offset: anchor.offset as usize, bias: match proto::Bias::from_i32(anchor.bias)? { @@ -500,12 +490,12 @@ pub fn deserialize_code_action(action: proto::CodeAction) -> Result pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { proto::Transaction { - id: Some(serialize_local_timestamp(transaction.id)), + id: Some(serialize_timestamp(transaction.id)), edit_ids: transaction .edit_ids .iter() .copied() - .map(serialize_local_timestamp) + .map(serialize_timestamp) .collect(), start: serialize_version(&transaction.start), } @@ -513,7 +503,7 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { pub fn deserialize_transaction(transaction: proto::Transaction) -> Result { Ok(Transaction { - id: deserialize_local_timestamp( + id: deserialize_timestamp( transaction .id .ok_or_else(|| anyhow!("missing transaction id"))?, @@ -521,21 +511,21 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result proto::LocalTimestamp { - proto::LocalTimestamp { +pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp { + proto::LamportTimestamp { replica_id: timestamp.replica_id as u32, value: timestamp.value, } } -pub fn deserialize_local_timestamp(timestamp: proto::LocalTimestamp) -> clock::Local { - clock::Local { +pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport { + clock::Lamport { replica_id: timestamp.replica_id as ReplicaId, value: timestamp.value, } @@ -555,7 +545,7 @@ pub fn deserialize_range(range: proto::Range) -> Range { pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global { let mut version = clock::Global::new(); for entry in message { - version.observe(clock::Local { + version.observe(clock::Lamport { replica_id: entry.replica_id as ReplicaId, value: entry.timestamp, }); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5e96ea043c..61c25f8f84 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -861,12 +861,12 @@ message ProjectTransaction { } message Transaction { - LocalTimestamp id = 1; - repeated LocalTimestamp edit_ids = 2; + LamportTimestamp id = 1; + repeated LamportTimestamp edit_ids = 2; repeated VectorClockEntry start = 3; } -message LocalTimestamp { +message LamportTimestamp { uint32 replica_id = 1; uint32 value = 2; } @@ -1280,7 +1280,7 @@ message Excerpt { message Anchor { uint32 replica_id = 1; - uint32 local_timestamp = 2; + uint32 timestamp = 2; uint64 offset = 3; Bias bias = 4; optional uint64 buffer_id = 5; @@ -1324,7 +1324,6 @@ message Operation { message Edit { uint32 replica_id = 1; - uint32 local_timestamp = 2; uint32 lamport_timestamp = 3; repeated VectorClockEntry version = 4; repeated Range ranges = 5; @@ -1333,7 +1332,6 @@ message Operation { message Undo { uint32 replica_id = 1; - uint32 local_timestamp = 2; uint32 lamport_timestamp = 3; repeated VectorClockEntry version = 4; repeated UndoCount counts = 5; @@ -1362,7 +1360,7 @@ message UndoMapEntry { message UndoCount { uint32 replica_id = 1; - uint32 local_timestamp = 2; + uint32 lamport_timestamp = 2; uint32 count = 3; } diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 65e9b6fcec..d1bc6cc8f8 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -31,6 +31,7 @@ regex.workspace = true [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true rand.workspace = true diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index b5f4fb24ec..084be0e336 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -8,7 +8,7 @@ use sum_tree::Bias; #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] pub struct Anchor { - pub timestamp: clock::Local, + pub timestamp: clock::Lamport, pub offset: usize, pub bias: Bias, pub buffer_id: Option, @@ -16,14 +16,14 @@ pub struct Anchor { impl Anchor { pub const MIN: Self = Self { - timestamp: clock::Local::MIN, + timestamp: clock::Lamport::MIN, offset: usize::MIN, bias: Bias::Left, buffer_id: None, }; pub const MAX: Self = Self { - timestamp: clock::Local::MAX, + timestamp: clock::Lamport::MAX, offset: usize::MAX, bias: Bias::Right, buffer_id: None, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 2fabb0f87f..c05ea1109c 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -46,18 +46,16 @@ lazy_static! { static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); } -pub type TransactionId = clock::Local; +pub type TransactionId = clock::Lamport; pub struct Buffer { snapshot: BufferSnapshot, history: History, deferred_ops: OperationQueue, deferred_replicas: HashSet, - replica_id: ReplicaId, - local_clock: clock::Local, pub lamport_clock: clock::Lamport, subscriptions: Topic, - edit_id_resolvers: HashMap>>, + edit_id_resolvers: HashMap>>, wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>, } @@ -85,7 +83,7 @@ pub struct HistoryEntry { #[derive(Clone, Debug)] pub struct Transaction { pub id: TransactionId, - pub edit_ids: Vec, + pub edit_ids: Vec, pub start: clock::Global, } @@ -97,8 +95,8 @@ impl HistoryEntry { struct History { base_text: Rope, - operations: TreeMap, - insertion_slices: HashMap>, + operations: TreeMap, + insertion_slices: HashMap>, undo_stack: Vec, redo_stack: Vec, transaction_depth: usize, @@ -107,7 +105,7 @@ struct History { #[derive(Clone, Debug)] struct InsertionSlice { - insertion_id: clock::Local, + insertion_id: clock::Lamport, range: Range, } @@ -129,18 +127,18 @@ impl History { } fn push(&mut self, op: Operation) { - self.operations.insert(op.local_timestamp(), op); + self.operations.insert(op.timestamp(), op); } fn start_transaction( &mut self, start: clock::Global, now: Instant, - local_clock: &mut clock::Local, + clock: &mut clock::Lamport, ) -> Option { self.transaction_depth += 1; if self.transaction_depth == 1 { - let id = local_clock.tick(); + let id = clock.tick(); self.undo_stack.push(HistoryEntry { transaction: Transaction { id, @@ -251,7 +249,7 @@ impl History { self.redo_stack.clear(); } - fn push_undo(&mut self, op_id: clock::Local) { + fn push_undo(&mut self, op_id: clock::Lamport) { assert_ne!(self.transaction_depth, 0); if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { let last_transaction = self.undo_stack.last_mut().unwrap(); @@ -412,37 +410,14 @@ impl Edit<(D1, D2)> { } } -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] -pub struct InsertionTimestamp { - pub replica_id: ReplicaId, - pub local: clock::Seq, - pub lamport: clock::Seq, -} - -impl InsertionTimestamp { - pub fn local(&self) -> clock::Local { - clock::Local { - replica_id: self.replica_id, - value: self.local, - } - } - - pub fn lamport(&self) -> clock::Lamport { - clock::Lamport { - replica_id: self.replica_id, - value: self.lamport, - } - } -} - #[derive(Eq, PartialEq, Clone, Debug)] pub struct Fragment { pub id: Locator, - pub insertion_timestamp: InsertionTimestamp, + pub timestamp: clock::Lamport, pub insertion_offset: usize, pub len: usize, pub visible: bool, - pub deletions: HashSet, + pub deletions: HashSet, pub max_undos: clock::Global, } @@ -470,29 +445,26 @@ impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary { #[derive(Eq, PartialEq, Clone, Debug)] struct InsertionFragment { - timestamp: clock::Local, + timestamp: clock::Lamport, split_offset: usize, fragment_id: Locator, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct InsertionFragmentKey { - timestamp: clock::Local, + timestamp: clock::Lamport, split_offset: usize, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Operation { Edit(EditOperation), - Undo { - undo: UndoOperation, - lamport_timestamp: clock::Lamport, - }, + Undo(UndoOperation), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct EditOperation { - pub timestamp: InsertionTimestamp, + pub timestamp: clock::Lamport, pub version: clock::Global, pub ranges: Vec>, pub new_text: Vec>, @@ -500,9 +472,9 @@ pub struct EditOperation { #[derive(Clone, Debug, Eq, PartialEq)] pub struct UndoOperation { - pub id: clock::Local, - pub counts: HashMap, + pub timestamp: clock::Lamport, pub version: clock::Global, + pub counts: HashMap, } impl Buffer { @@ -514,24 +486,21 @@ impl Buffer { let mut fragments = SumTree::new(); let mut insertions = SumTree::new(); - let mut local_clock = clock::Local::new(replica_id); let mut lamport_clock = clock::Lamport::new(replica_id); let mut version = clock::Global::new(); let visible_text = history.base_text.clone(); if !visible_text.is_empty() { - let insertion_timestamp = InsertionTimestamp { + let insertion_timestamp = clock::Lamport { replica_id: 0, - local: 1, - lamport: 1, + value: 1, }; - local_clock.observe(insertion_timestamp.local()); - lamport_clock.observe(insertion_timestamp.lamport()); - version.observe(insertion_timestamp.local()); + lamport_clock.observe(insertion_timestamp); + version.observe(insertion_timestamp); let fragment_id = Locator::between(&Locator::min(), &Locator::max()); let fragment = Fragment { id: fragment_id, - insertion_timestamp, + timestamp: insertion_timestamp, insertion_offset: 0, len: visible_text.len(), visible: true, @@ -557,8 +526,6 @@ impl Buffer { history, deferred_ops: OperationQueue::new(), deferred_replicas: HashSet::default(), - replica_id, - local_clock, lamport_clock, subscriptions: Default::default(), edit_id_resolvers: Default::default(), @@ -575,7 +542,7 @@ impl Buffer { } pub fn replica_id(&self) -> ReplicaId { - self.local_clock.replica_id + self.lamport_clock.replica_id } pub fn remote_id(&self) -> u64 { @@ -602,16 +569,12 @@ impl Buffer { .map(|(range, new_text)| (range, new_text.into())); self.start_transaction(); - let timestamp = InsertionTimestamp { - replica_id: self.replica_id, - local: self.local_clock.tick().value, - lamport: self.lamport_clock.tick().value, - }; + let timestamp = self.lamport_clock.tick(); let operation = Operation::Edit(self.apply_local_edit(edits, timestamp)); self.history.push(operation.clone()); - self.history.push_undo(operation.local_timestamp()); - self.snapshot.version.observe(operation.local_timestamp()); + self.history.push_undo(operation.timestamp()); + self.snapshot.version.observe(operation.timestamp()); self.end_transaction(); operation } @@ -619,7 +582,7 @@ impl Buffer { fn apply_local_edit>>( &mut self, edits: impl ExactSizeIterator, T)>, - timestamp: InsertionTimestamp, + timestamp: clock::Lamport, ) -> EditOperation { let mut edits_patch = Patch::default(); let mut edit_op = EditOperation { @@ -696,7 +659,7 @@ impl Buffer { .item() .map_or(&Locator::max(), |old_fragment| &old_fragment.id), ), - insertion_timestamp: timestamp, + timestamp, insertion_offset, len: new_text.len(), deletions: Default::default(), @@ -726,7 +689,7 @@ impl Buffer { intersection.insertion_offset += fragment_start - old_fragments.start().visible; intersection.id = Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp.local()); + intersection.deletions.insert(timestamp); intersection.visible = false; } if intersection.len > 0 { @@ -781,7 +744,7 @@ impl Buffer { self.subscriptions.publish_mut(&edits_patch); self.history .insertion_slices - .insert(timestamp.local(), insertion_slices); + .insert(timestamp, insertion_slices); edit_op } @@ -808,28 +771,23 @@ impl Buffer { fn apply_op(&mut self, op: Operation) -> Result<()> { match op { Operation::Edit(edit) => { - if !self.version.observed(edit.timestamp.local()) { + if !self.version.observed(edit.timestamp) { self.apply_remote_edit( &edit.version, &edit.ranges, &edit.new_text, edit.timestamp, ); - self.snapshot.version.observe(edit.timestamp.local()); - self.local_clock.observe(edit.timestamp.local()); - self.lamport_clock.observe(edit.timestamp.lamport()); - self.resolve_edit(edit.timestamp.local()); + self.snapshot.version.observe(edit.timestamp); + self.lamport_clock.observe(edit.timestamp); + self.resolve_edit(edit.timestamp); } } - Operation::Undo { - undo, - lamport_timestamp, - } => { - if !self.version.observed(undo.id) { + Operation::Undo(undo) => { + if !self.version.observed(undo.timestamp) { self.apply_undo(&undo)?; - self.snapshot.version.observe(undo.id); - self.local_clock.observe(undo.id); - self.lamport_clock.observe(lamport_timestamp); + self.snapshot.version.observe(undo.timestamp); + self.lamport_clock.observe(undo.timestamp); } } } @@ -849,7 +807,7 @@ impl Buffer { version: &clock::Global, ranges: &[Range], new_text: &[Arc], - timestamp: InsertionTimestamp, + timestamp: clock::Lamport, ) { if ranges.is_empty() { return; @@ -916,9 +874,7 @@ impl Buffer { // Skip over insertions that are concurrent to this edit, but have a lower lamport // timestamp. while let Some(fragment) = old_fragments.item() { - if fragment_start == range.start - && fragment.insertion_timestamp.lamport() > timestamp.lamport() - { + if fragment_start == range.start && fragment.timestamp > timestamp { new_ropes.push_fragment(fragment, fragment.visible); new_fragments.push(fragment.clone(), &None); old_fragments.next(&cx); @@ -955,7 +911,7 @@ impl Buffer { .item() .map_or(&Locator::max(), |old_fragment| &old_fragment.id), ), - insertion_timestamp: timestamp, + timestamp, insertion_offset, len: new_text.len(), deletions: Default::default(), @@ -986,7 +942,7 @@ impl Buffer { fragment_start - old_fragments.start().0.full_offset(); intersection.id = Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp.local()); + intersection.deletions.insert(timestamp); intersection.visible = false; insertion_slices.push(intersection.insertion_slice()); } @@ -1038,13 +994,13 @@ impl Buffer { self.snapshot.insertions.edit(new_insertions, &()); self.history .insertion_slices - .insert(timestamp.local(), insertion_slices); + .insert(timestamp, insertion_slices); self.subscriptions.publish_mut(&edits_patch) } fn fragment_ids_for_edits<'a>( &'a self, - edit_ids: impl Iterator, + edit_ids: impl Iterator, ) -> Vec<&'a Locator> { // Get all of the insertion slices changed by the given edits. let mut insertion_slices = Vec::new(); @@ -1105,7 +1061,7 @@ impl Buffer { let fragment_was_visible = fragment.visible; fragment.visible = fragment.is_visible(&self.undo_map); - fragment.max_undos.observe(undo.id); + fragment.max_undos.observe(undo.timestamp); let old_start = old_fragments.start().1; let new_start = new_fragments.summary().text.visible; @@ -1159,10 +1115,10 @@ impl Buffer { if self.deferred_replicas.contains(&op.replica_id()) { false } else { - match op { - Operation::Edit(edit) => self.version.observed_all(&edit.version), - Operation::Undo { undo, .. } => self.version.observed_all(&undo.version), - } + self.version.observed_all(match op { + Operation::Edit(edit) => &edit.version, + Operation::Undo(undo) => &undo.version, + }) } } @@ -1180,7 +1136,7 @@ impl Buffer { pub fn start_transaction_at(&mut self, now: Instant) -> Option { self.history - .start_transaction(self.version.clone(), now, &mut self.local_clock) + .start_transaction(self.version.clone(), now, &mut self.lamport_clock) } pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> { @@ -1209,7 +1165,7 @@ impl Buffer { &self.history.base_text } - pub fn operations(&self) -> &TreeMap { + pub fn operations(&self) -> &TreeMap { &self.history.operations } @@ -1289,16 +1245,13 @@ impl Buffer { } let undo = UndoOperation { - id: self.local_clock.tick(), + timestamp: self.lamport_clock.tick(), version: self.version(), counts, }; self.apply_undo(&undo)?; - let operation = Operation::Undo { - undo, - lamport_timestamp: self.lamport_clock.tick(), - }; - self.snapshot.version.observe(operation.local_timestamp()); + self.snapshot.version.observe(undo.timestamp); + let operation = Operation::Undo(undo); self.history.push(operation.clone()); Ok(operation) } @@ -1363,7 +1316,7 @@ impl Buffer { pub fn wait_for_edits( &mut self, - edit_ids: impl IntoIterator, + edit_ids: impl IntoIterator, ) -> impl 'static + Future> { let mut futures = Vec::new(); for edit_id in edit_ids { @@ -1435,7 +1388,7 @@ impl Buffer { self.wait_for_version_txs.clear(); } - fn resolve_edit(&mut self, edit_id: clock::Local) { + fn resolve_edit(&mut self, edit_id: clock::Lamport) { for mut tx in self .edit_id_resolvers .remove(&edit_id) @@ -1513,7 +1466,7 @@ impl Buffer { .insertions .get( &InsertionFragmentKey { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, split_offset: fragment.insertion_offset, }, &(), @@ -1996,7 +1949,7 @@ impl BufferSnapshot { let fragment = fragment_cursor.item().unwrap(); let overshoot = offset - *fragment_cursor.start(); Anchor { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, offset: fragment.insertion_offset + overshoot, bias, buffer_id: Some(self.remote_id), @@ -2188,15 +2141,14 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo break; } - let timestamp = fragment.insertion_timestamp.local(); let start_anchor = Anchor { - timestamp, + timestamp: fragment.timestamp, offset: fragment.insertion_offset, bias: Bias::Right, buffer_id: Some(self.buffer_id), }; let end_anchor = Anchor { - timestamp, + timestamp: fragment.timestamp, offset: fragment.insertion_offset + fragment.len, bias: Bias::Left, buffer_id: Some(self.buffer_id), @@ -2269,19 +2221,17 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo impl Fragment { fn insertion_slice(&self) -> InsertionSlice { InsertionSlice { - insertion_id: self.insertion_timestamp.local(), + insertion_id: self.timestamp, range: self.insertion_offset..self.insertion_offset + self.len, } } fn is_visible(&self, undos: &UndoMap) -> bool { - !undos.is_undone(self.insertion_timestamp.local()) - && self.deletions.iter().all(|d| undos.is_undone(*d)) + !undos.is_undone(self.timestamp) && self.deletions.iter().all(|d| undos.is_undone(*d)) } fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool { - (version.observed(self.insertion_timestamp.local()) - && !undos.was_undone(self.insertion_timestamp.local(), version)) + (version.observed(self.timestamp) && !undos.was_undone(self.timestamp, version)) && self .deletions .iter() @@ -2294,14 +2244,14 @@ impl sum_tree::Item for Fragment { fn summary(&self) -> Self::Summary { let mut max_version = clock::Global::new(); - max_version.observe(self.insertion_timestamp.local()); + max_version.observe(self.timestamp); for deletion in &self.deletions { max_version.observe(*deletion); } max_version.join(&self.max_undos); let mut min_insertion_version = clock::Global::new(); - min_insertion_version.observe(self.insertion_timestamp.local()); + min_insertion_version.observe(self.timestamp); let max_insertion_version = min_insertion_version.clone(); if self.visible { FragmentSummary { @@ -2378,7 +2328,7 @@ impl sum_tree::KeyedItem for InsertionFragment { impl InsertionFragment { fn new(fragment: &Fragment) -> Self { Self { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, split_offset: fragment.insertion_offset, fragment_id: fragment.id.clone(), } @@ -2501,10 +2451,10 @@ impl Operation { operation_queue::Operation::lamport_timestamp(self).replica_id } - pub fn local_timestamp(&self) -> clock::Local { + pub fn timestamp(&self) -> clock::Lamport { match self { - Operation::Edit(edit) => edit.timestamp.local(), - Operation::Undo { undo, .. } => undo.id, + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, } } @@ -2523,10 +2473,8 @@ impl Operation { impl operation_queue::Operation for Operation { fn lamport_timestamp(&self) -> clock::Lamport { match self { - Operation::Edit(edit) => edit.timestamp.lamport(), - Operation::Undo { - lamport_timestamp, .. - } => *lamport_timestamp, + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, } } } diff --git a/crates/text/src/undo_map.rs b/crates/text/src/undo_map.rs index ff1b241e73..f95809c02e 100644 --- a/crates/text/src/undo_map.rs +++ b/crates/text/src/undo_map.rs @@ -26,8 +26,8 @@ impl sum_tree::KeyedItem for UndoMapEntry { #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct UndoMapKey { - edit_id: clock::Local, - undo_id: clock::Local, + edit_id: clock::Lamport, + undo_id: clock::Lamport, } impl sum_tree::Summary for UndoMapKey { @@ -50,7 +50,7 @@ impl UndoMap { sum_tree::Edit::Insert(UndoMapEntry { key: UndoMapKey { edit_id: *edit_id, - undo_id: undo.id, + undo_id: undo.timestamp, }, undo_count: *count, }) @@ -59,11 +59,11 @@ impl UndoMap { self.0.edit(edits, &()); } - pub fn is_undone(&self, edit_id: clock::Local) -> bool { + pub fn is_undone(&self, edit_id: clock::Lamport) -> bool { self.undo_count(edit_id) % 2 == 1 } - pub fn was_undone(&self, edit_id: clock::Local, version: &clock::Global) -> bool { + pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool { let mut cursor = self.0.cursor::(); cursor.seek( &UndoMapKey { @@ -88,7 +88,7 @@ impl UndoMap { undo_count % 2 == 1 } - pub fn undo_count(&self, edit_id: clock::Local) -> u32 { + pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 { let mut cursor = self.0.cursor::(); cursor.seek( &UndoMapKey { From 1e604546434bbafa79b33087cc1ddb6eca42dc9e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Aug 2023 16:31:26 -0700 Subject: [PATCH 130/156] Renumber protobuf fields, bump protocol version --- crates/rpc/proto/zed.proto | 14 +++++++------- crates/rpc/src/rpc.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 61c25f8f84..92c85677f6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1324,17 +1324,17 @@ message Operation { message Edit { uint32 replica_id = 1; - uint32 lamport_timestamp = 3; - repeated VectorClockEntry version = 4; - repeated Range ranges = 5; - repeated string new_text = 6; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated Range ranges = 4; + repeated string new_text = 5; } message Undo { uint32 replica_id = 1; - uint32 lamport_timestamp = 3; - repeated VectorClockEntry version = 4; - repeated UndoCount counts = 5; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated UndoCount counts = 4; } message UpdateSelections { diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index bc9dd6f80b..d64cbae929 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 61; +pub const PROTOCOL_VERSION: u32 = 62; From d868ec920f4bb13132921c0d29f451a288befc75 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Sep 2023 09:15:29 +0200 Subject: [PATCH 131/156] Avoid duplicate entries in inline assistant's prompt history --- crates/ai/src/assistant.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 5cde99af82..9b384252fc 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -507,10 +507,13 @@ impl AssistantPanel { return; }; + self.inline_prompt_history + .retain(|prompt| prompt != user_prompt); self.inline_prompt_history.push_back(user_prompt.into()); if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { self.inline_prompt_history.pop_front(); } + let range = pending_assist.range.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let selected_text = snapshot From 6d7949654bdcfed2cb60c6f3faa8c0850edc527c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 1 Sep 2023 11:14:27 -0600 Subject: [PATCH 132/156] 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 2ea2ec7453..d331b0a268 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 f9d09adcf5..cadf37b31d 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 a2bbab0478..1b3dcee6ad 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) + ) + }); } } From af12977d1777fa1af1be6b1d0bbd7127b0752401 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 1 Sep 2023 12:23:45 -0600 Subject: [PATCH 133/156] vim: Add `S` to substitute line For zed-industries/community#1897 --- assets/keymaps/vim.json | 2 + .../LiveKitBridge/Package.resolved | 4 +- crates/vim/src/normal.rs | 9 +- crates/vim/src/normal/substitute.rs | 97 ++++++++++++++++++- .../vim/test_data/test_substitute_line.json | 29 ++++++ 5 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 crates/vim/test_data/test_substitute_line.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c7e6199f44..da094ea7e4 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -371,6 +371,7 @@ "Replace" ], "s": "vim::Substitute", + "shift-s": "vim::SubstituteLine", "> >": "editor::Indent", "< <": "editor::Outdent", "ctrl-pagedown": "pane::ActivateNextItem", @@ -446,6 +447,7 @@ } ], "s": "vim::Substitute", + "shift-s": "vim::SubstituteLine", "c": "vim::Substitute", "~": "vim::ChangeCase", "shift-i": [ diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index b925bc8f0d..85ae088565 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": "ce20dc083ee485524b802669890291c0d8090170", - "version": "1.22.1" + "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e", + "version": "1.21.0" } } ] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index a73c518809..1f8276c327 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -27,7 +27,6 @@ use self::{ case::change_case, change::{change_motion, change_object}, delete::{delete_motion, delete_object}, - substitute::substitute, yank::{yank_motion, yank_object}, }; @@ -44,7 +43,6 @@ actions!( ChangeToEndOfLine, DeleteToEndOfLine, Yank, - Substitute, ChangeCase, ] ); @@ -56,13 +54,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); + substitute::init(cx); search::init(cx); - cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { - Vim::update(cx, |vim, cx| { - let times = vim.pop_number_operator(cx); - substitute(vim, times, cx); - }) - }); cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index b04596240a..efdd43d0a4 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -1,10 +1,32 @@ -use gpui::WindowContext; +use editor::movement; +use gpui::{actions, AppContext, WindowContext}; use language::Point; +use workspace::Workspace; use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; -pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { - let line_mode = vim.state().mode == Mode::VisualLine; +actions!(vim, [Substitute, SubstituteLine]); + +pub(crate) fn init(cx: &mut AppContext) { + cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { + Vim::update(cx, |vim, cx| { + let times = vim.pop_number_operator(cx); + substitute(vim, times, vim.state().mode == Mode::VisualLine, cx); + }) + }); + + cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| { + Vim::update(cx, |vim, cx| { + if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) { + vim.switch_mode(Mode::VisualLine, false, cx) + } + let count = vim.pop_number_operator(cx); + substitute(vim, count, true, cx) + }) + }); +} + +pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { @@ -14,6 +36,11 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { Motion::Right.expand_selection(map, selection, count, true); } if line_mode { + // in Visual mode when the selection contains the newline at the end + // of the line, we should exclude it. + if !selection.is_empty() && selection.end.column() == 0 { + selection.end = movement::left(map, selection.end); + } Motion::CurrentLine.expand_selection(map, selection, None, false); if let Some((point, _)) = (Motion::FirstNonWhitespace { display_lines: false, @@ -166,4 +193,68 @@ mod test { the laˇzy dog"}) .await; } + + #[gpui::test] + async fn test_substitute_line(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + let initial_state = indoc! {" + The quick brown + fox juˇmps over + the lazy dog + "}; + + // normal mode + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["shift-s", "o"]).await; + cx.assert_shared_state(indoc! {" + The quick brown + oˇ + the lazy dog + "}) + .await; + + // visual mode + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"]) + .await; + cx.assert_shared_state(indoc! {" + oˇ + the lazy dog + "}) + .await; + + // visual block mode + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"]) + .await; + cx.assert_shared_state(indoc! {" + The quick brown + oˇ + "}) + .await; + + // visual mode including newline + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"]) + .await; + cx.assert_shared_state(indoc! {" + The quick brown + oˇ + the lazy dog + "}) + .await; + + // indentation + cx.set_neovim_option("shiftwidth=4").await; + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"]) + .await; + cx.assert_shared_state(indoc! {" + The quick brown + oˇ + the lazy dog + "}) + .await; + } } diff --git a/crates/vim/test_data/test_substitute_line.json b/crates/vim/test_data/test_substitute_line.json new file mode 100644 index 0000000000..eb0a9825f8 --- /dev/null +++ b/crates/vim/test_data/test_substitute_line.json @@ -0,0 +1,29 @@ +{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} +{"Key":"shift-s"} +{"Key":"o"} +{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} +{"Key":"v"} +{"Key":"k"} +{"Key":"shift-s"} +{"Key":"o"} +{"Get":{"state":"oˇ\nthe lazy dog\n","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Key":"shift-s"} +{"Key":"o"} +{"Get":{"state":"The quick brown\noˇ\n","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} +{"Key":"v"} +{"Key":"$"} +{"Key":"shift-s"} +{"Key":"o"} +{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}} +{"SetOption":{"value":"shiftwidth=4"}} +{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} +{"Key":">"} +{"Key":">"} +{"Key":"shift-s"} +{"Key":"o"} +{"Get":{"state":"The quick brown\n oˇ\nthe lazy dog\n","mode":"Insert"}} From d370c72fbfbdf9d3fa9448b49bfefb408cc3ecd9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Sep 2023 15:31:52 -0700 Subject: [PATCH 134/156] Start work on rejoining channel buffers --- crates/channel/src/channel_buffer.rs | 27 ++ crates/channel/src/channel_store.rs | 153 ++++++++-- crates/collab/src/db/queries/buffers.rs | 265 +++++++++++++----- crates/collab/src/rpc.rs | 25 +- .../collab/src/tests/channel_buffer_tests.rs | 138 ++++++--- crates/language/src/proto.rs | 25 ++ crates/project/src/project.rs | 27 +- crates/rpc/proto/zed.proto | 26 ++ crates/rpc/src/proto.rs | 3 + 9 files changed, 526 insertions(+), 163 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 29f4d3493c..98ecbc5dcf 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -17,6 +17,7 @@ pub struct ChannelBuffer { connected: bool, collaborators: Vec, buffer: ModelHandle, + buffer_epoch: u64, client: Arc, subscription: Option, } @@ -73,6 +74,7 @@ impl ChannelBuffer { Self { buffer, + buffer_epoch: response.epoch, client, connected: true, collaborators, @@ -82,6 +84,26 @@ impl ChannelBuffer { })) } + pub(crate) fn replace_collaborators( + &mut self, + collaborators: Vec, + cx: &mut ModelContext, + ) { + for old_collaborator in &self.collaborators { + if collaborators + .iter() + .any(|c| c.replica_id == old_collaborator.replica_id) + { + self.buffer.update(cx, |buffer, cx| { + buffer.remove_peer(old_collaborator.replica_id as u16, cx) + }); + } + } + self.collaborators = collaborators; + cx.emit(Event::CollaboratorsChanged); + cx.notify(); + } + async fn handle_update_channel_buffer( this: ModelHandle, update_channel_buffer: TypedEnvelope, @@ -166,6 +188,10 @@ impl ChannelBuffer { } } + pub fn epoch(&self) -> u64 { + self.buffer_epoch + } + pub fn buffer(&self) -> ModelHandle { self.buffer.clone() } @@ -179,6 +205,7 @@ impl ChannelBuffer { } pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { + log::info!("channel buffer {} disconnected", self.channel.id); if self.connected { self.connected = false; self.subscription.take(); diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 861f731331..ec1652581d 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,13 +1,15 @@ use crate::channel_buffer::ChannelBuffer; use anyhow::{anyhow, Result}; -use client::{Client, Status, Subscription, User, UserId, UserStore}; +use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; -use std::sync::Arc; +use std::{mem, sync::Arc, time::Duration}; use util::ResultExt; +pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); + pub type ChannelId = u64; pub struct ChannelStore { @@ -22,7 +24,8 @@ pub struct ChannelStore { client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, - _watch_connection_status: Task<()>, + _watch_connection_status: Task>, + disconnect_channel_buffers_task: Option>, _update_channels: Task<()>, } @@ -67,24 +70,20 @@ impl ChannelStore { let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_channels); - let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded(); let mut connection_status = client.status(); + let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded(); let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { while let Some(status) = connection_status.next().await { - if !status.is_connected() { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if matches!(status, Status::ConnectionLost | Status::SignedOut) { - this.handle_disconnect(cx); - } else { - this.disconnect_buffers(cx); - } - }); - } else { - break; - } + let this = this.upgrade(&cx)?; + if status.is_connected() { + this.update(&mut cx, |this, cx| this.handle_connect(cx)) + .await + .log_err()?; + } else { + this.update(&mut cx, |this, cx| this.handle_disconnect(cx)); } } + Some(()) }); Self { @@ -100,6 +99,7 @@ impl ChannelStore { user_store, _rpc_subscription: rpc_subscription, _watch_connection_status: watch_connection_status, + disconnect_channel_buffers_task: None, _update_channels: cx.spawn_weak(|this, mut cx| async move { while let Some(update_channels) = update_channels_rx.next().await { if let Some(this) = this.upgrade(&cx) { @@ -482,8 +482,102 @@ impl ChannelStore { Ok(()) } - fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) { - self.disconnect_buffers(cx); + fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { + self.disconnect_channel_buffers_task.take(); + + let mut buffer_versions = Vec::new(); + for buffer in self.opened_buffers.values() { + if let OpenedChannelBuffer::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade(cx) { + let channel_buffer = buffer.read(cx); + let buffer = channel_buffer.buffer().read(cx); + buffer_versions.push(proto::ChannelBufferVersion { + channel_id: channel_buffer.channel().id, + epoch: channel_buffer.epoch(), + version: language::proto::serialize_version(&buffer.version()), + }); + } + } + } + + let response = self.client.request(proto::RejoinChannelBuffers { + buffers: buffer_versions, + }); + + cx.spawn(|this, mut cx| async move { + let mut response = response.await?; + + this.update(&mut cx, |this, cx| { + this.opened_buffers.retain(|_, buffer| match buffer { + OpenedChannelBuffer::Open(channel_buffer) => { + let Some(channel_buffer) = channel_buffer.upgrade(cx) else { + return false; + }; + + channel_buffer.update(cx, |channel_buffer, cx| { + let channel_id = channel_buffer.channel().id; + if let Some(remote_buffer) = response + .buffers + .iter_mut() + .find(|buffer| buffer.channel_id == channel_id) + { + let channel_id = channel_buffer.channel().id; + let remote_version = + language::proto::deserialize_version(&remote_buffer.version); + + channel_buffer.replace_collaborators( + mem::take(&mut remote_buffer.collaborators), + cx, + ); + + let operations = channel_buffer + .buffer() + .update(cx, |buffer, cx| { + let outgoing_operations = + buffer.serialize_ops(Some(remote_version), cx); + let incoming_operations = + mem::take(&mut remote_buffer.operations) + .into_iter() + .map(language::proto::deserialize_operation) + .collect::>>()?; + buffer.apply_ops(incoming_operations, cx)?; + anyhow::Ok(outgoing_operations) + }) + .log_err(); + + if let Some(operations) = operations { + let client = this.client.clone(); + cx.background() + .spawn(async move { + let operations = operations.await; + for chunk in + language::proto::split_operations(operations) + { + client + .send(proto::UpdateChannelBuffer { + channel_id, + operations: chunk, + }) + .ok(); + } + }) + .detach(); + return true; + } + } + + channel_buffer.disconnect(cx); + false + }) + } + OpenedChannelBuffer::Loading(_) => true, + }); + }); + anyhow::Ok(()) + }) + } + + fn handle_disconnect(&mut self, cx: &mut ModelContext) { self.channels_by_id.clear(); self.channel_invitations.clear(); self.channel_participants.clear(); @@ -491,16 +585,23 @@ impl ChannelStore { self.channel_paths.clear(); self.outgoing_invites.clear(); cx.notify(); - } - fn disconnect_buffers(&mut self, cx: &mut ModelContext) { - for (_, buffer) in self.opened_buffers.drain() { - if let OpenedChannelBuffer::Open(buffer) = buffer { - if let Some(buffer) = buffer.upgrade(cx) { - buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); + self.disconnect_channel_buffers_task.get_or_insert_with(|| { + cx.spawn_weak(|this, mut cx| async move { + cx.background().timer(RECONNECT_TIMEOUT).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + for (_, buffer) in this.opened_buffers.drain() { + if let OpenedChannelBuffer::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); + } + } + } + }); } - } - } + }) + }); } pub(crate) fn update_channels( diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index f120aea1c5..587ed058ff 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -10,8 +10,6 @@ impl Database { connection: ConnectionId, ) -> Result { self.transaction(|tx| async move { - let tx = tx; - self.check_user_is_channel_member(channel_id, user_id, &tx) .await?; @@ -70,7 +68,6 @@ impl Database { .await?; collaborators.push(collaborator); - // Assemble the buffer state let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?; Ok(proto::JoinChannelBufferResponse { @@ -78,6 +75,7 @@ impl Database { replica_id: replica_id.to_proto() as u32, base_text, operations, + epoch: buffer.epoch as u64, collaborators: collaborators .into_iter() .map(|collaborator| proto::Collaborator { @@ -91,6 +89,113 @@ impl Database { .await } + pub async fn rejoin_channel_buffers( + &self, + buffers: &[proto::ChannelBufferVersion], + user_id: UserId, + connection_id: ConnectionId, + ) -> Result { + self.transaction(|tx| async move { + let mut response = proto::RejoinChannelBuffersResponse::default(); + for client_buffer in buffers { + let channel_id = ChannelId::from_proto(client_buffer.channel_id); + if self + .check_user_is_channel_member(channel_id, user_id, &*tx) + .await + .is_err() + { + log::info!("user is not a member of channel"); + continue; + } + + let buffer = self.get_channel_buffer(channel_id, &*tx).await?; + let mut collaborators = channel_buffer_collaborator::Entity::find() + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .all(&*tx) + .await?; + + // If the buffer epoch hasn't changed since the client lost + // connection, then the client's buffer can be syncronized with + // the server's buffer. + if buffer.epoch as u64 != client_buffer.epoch { + continue; + } + + // If there is still a disconnected collaborator for the user, + // update the connection associated with that collaborator, and reuse + // that replica id. + if let Some(ix) = collaborators + .iter() + .position(|c| c.user_id == user_id && c.connection_lost) + { + let self_collaborator = &mut collaborators[ix]; + *self_collaborator = channel_buffer_collaborator::ActiveModel { + id: ActiveValue::Unchanged(self_collaborator.id), + connection_id: ActiveValue::Set(connection_id.id as i32), + connection_server_id: ActiveValue::Set(ServerId( + connection_id.owner_id as i32, + )), + connection_lost: ActiveValue::Set(false), + ..Default::default() + } + .update(&*tx) + .await?; + } else { + continue; + } + + let client_version = version_from_wire(&client_buffer.version); + let serialization_version = self + .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx) + .await?; + + let mut rows = buffer_operation::Entity::find() + .filter( + buffer_operation::Column::BufferId + .eq(buffer.id) + .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), + ) + .stream(&*tx) + .await?; + + // Find the server's version vector and any operations + // that the client has not seen. + let mut server_version = clock::Global::new(); + let mut operations = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + let timestamp = clock::Lamport { + replica_id: row.replica_id as u16, + value: row.lamport_timestamp as u32, + }; + server_version.observe(timestamp); + if !client_version.observed(timestamp) { + operations.push(proto::Operation { + variant: Some(operation_from_storage(row, serialization_version)?), + }) + } + } + + response.buffers.push(proto::RejoinedChannelBuffer { + channel_id: client_buffer.channel_id, + version: version_to_wire(&server_version), + operations, + collaborators: collaborators + .into_iter() + .map(|collaborator| proto::Collaborator { + peer_id: Some(collaborator.connection().into()), + user_id: collaborator.user_id.to_proto(), + replica_id: collaborator.replica_id.0 as u32, + }) + .collect(), + }); + } + + Ok(response) + }) + .await + } + pub async fn leave_channel_buffer( &self, channel_id: ChannelId, @@ -103,6 +208,39 @@ impl Database { .await } + pub async fn leave_channel_buffers( + &self, + connection: ConnectionId, + ) -> Result)>> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, + } + + let channel_ids: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::ChannelId) + .filter(Condition::all().add( + channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32), + )) + .into_values::<_, QueryChannelIds>() + .all(&*tx) + .await?; + + let mut result = Vec::new(); + for channel_id in channel_ids { + let collaborators = self + .leave_channel_buffer_internal(channel_id, connection, &*tx) + .await?; + result.push((channel_id, collaborators)); + } + + Ok(result) + }) + .await + } + pub async fn leave_channel_buffer_internal( &self, channel_id: ChannelId, @@ -143,45 +281,12 @@ impl Database { drop(rows); if connections.is_empty() { - self.snapshot_buffer(channel_id, &tx).await?; + self.snapshot_channel_buffer(channel_id, &tx).await?; } Ok(connections) } - pub async fn leave_channel_buffers( - &self, - connection: ConnectionId, - ) -> Result)>> { - self.transaction(|tx| async move { - #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] - enum QueryChannelIds { - ChannelId, - } - - let channel_ids: Vec = channel_buffer_collaborator::Entity::find() - .select_only() - .column(channel_buffer_collaborator::Column::ChannelId) - .filter(Condition::all().add( - channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32), - )) - .into_values::<_, QueryChannelIds>() - .all(&*tx) - .await?; - - let mut result = Vec::new(); - for channel_id in channel_ids { - let collaborators = self - .leave_channel_buffer_internal(channel_id, connection, &*tx) - .await?; - result.push((channel_id, collaborators)); - } - - Ok(result) - }) - .await - } - pub async fn get_channel_buffer_collaborators( &self, channel_id: ChannelId, @@ -224,20 +329,9 @@ impl Database { .await? .ok_or_else(|| anyhow!("no such buffer"))?; - #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] - enum QueryVersion { - OperationSerializationVersion, - } - - let serialization_version: i32 = buffer - .find_related(buffer_snapshot::Entity) - .select_only() - .column(buffer_snapshot::Column::OperationSerializationVersion) - .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch)) - .into_values::<_, QueryVersion>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("missing buffer snapshot"))?; + let serialization_version = self + .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx) + .await?; let operations = operations .iter() @@ -270,6 +364,38 @@ impl Database { .await } + async fn get_buffer_operation_serialization_version( + &self, + buffer_id: BufferId, + epoch: i32, + tx: &DatabaseTransaction, + ) -> Result { + Ok(buffer_snapshot::Entity::find() + .filter(buffer_snapshot::Column::BufferId.eq(buffer_id)) + .filter(buffer_snapshot::Column::Epoch.eq(epoch)) + .select_only() + .column(buffer_snapshot::Column::OperationSerializationVersion) + .into_values::<_, QueryOperationSerializationVersion>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("missing buffer snapshot"))?) + } + + async fn get_channel_buffer( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result { + Ok(channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?) + } + async fn get_buffer_state( &self, buffer: &buffer::Model, @@ -303,27 +429,20 @@ impl Database { .await?; let mut operations = Vec::new(); while let Some(row) = rows.next().await { - let row = row?; - - let operation = operation_from_storage(row, version)?; operations.push(proto::Operation { - variant: Some(operation), + variant: Some(operation_from_storage(row?, version)?), }) } Ok((base_text, operations)) } - async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> { - let buffer = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(buffer::Entity) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such buffer"))?; - + async fn snapshot_channel_buffer( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result<()> { + let buffer = self.get_channel_buffer(channel_id, tx).await?; let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; if operations.is_empty() { return Ok(()); @@ -527,6 +646,22 @@ fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global { version } +fn version_to_wire(version: &clock::Global) -> Vec { + let mut message = Vec::new(); + for entry in version.iter() { + message.push(proto::VectorClockEntry { + replica_id: entry.replica_id as u32, + timestamp: entry.value, + }); + } + message +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] +enum QueryOperationSerializationVersion { + OperationSerializationVersion, +} + mod storage { #![allow(non_snake_case)] use prost::Message; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6b44711c42..06aa00c9b8 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -251,6 +251,7 @@ impl Server { .add_request_handler(join_channel_buffer) .add_request_handler(leave_channel_buffer) .add_message_handler(update_channel_buffer) + .add_request_handler(rejoin_channel_buffers) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -854,13 +855,12 @@ async fn connection_lost( .await .trace_err(); - leave_channel_buffers_for_session(&session) - .await - .trace_err(); - futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { leave_room_for_session(&session).await.trace_err(); + leave_channel_buffers_for_session(&session) + .await + .trace_err(); if !session .connection_pool() @@ -2547,6 +2547,23 @@ async fn update_channel_buffer( Ok(()) } +async fn rejoin_channel_buffers( + request: proto::RejoinChannelBuffers, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let rejoin_response = db + .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id) + .await?; + + // TODO: inform channel buffer collaborators that this user has rejoined. + + response.send(rejoin_response)?; + + Ok(()) +} + async fn leave_channel_buffer( request: proto::LeaveChannelBuffer, response: Response, diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 8ac4dbbd3f..5ba5b50429 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -21,20 +21,19 @@ async fn test_core_channel_buffers( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let zed_id = server + let channel_id = server .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Client A joins the channel buffer let channel_buffer_a = client_a .channel_store() - .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); // Client A edits the buffer let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - buffer_a.update(cx_a, |buffer, cx| { buffer.edit([(0..0, "hello world")], None, cx) }); @@ -45,17 +44,15 @@ async fn test_core_channel_buffers( buffer.edit([(0..5, "goodbye")], None, cx) }); buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); - deterministic.run_until_parked(); - assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + deterministic.run_until_parked(); // Client B joins the channel buffer let channel_buffer_b = client_b .channel_store() - .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); - channel_buffer_b.read_with(cx_b, |buffer, _| { assert_collaborators( buffer.collaborators(), @@ -91,9 +88,7 @@ async fn test_core_channel_buffers( // Client A rejoins the channel buffer let _channel_buffer_a = client_a .channel_store() - .update(cx_a, |channels, cx| { - channels.open_channel_buffer(zed_id, cx) - }) + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -136,7 +131,7 @@ async fn test_channel_buffer_replica_ids( let channel_id = server .make_channel( - "zed", + "the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -160,23 +155,17 @@ async fn test_channel_buffer_replica_ids( // C first so that the replica IDs in the project and the channel buffer are different let channel_buffer_c = client_c .channel_store() - .update(cx_c, |channel, cx| { - channel.open_channel_buffer(channel_id, cx) - }) + .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); let channel_buffer_b = client_b .channel_store() - .update(cx_b, |channel, cx| { - channel.open_channel_buffer(channel_id, cx) - }) + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); let channel_buffer_a = client_a .channel_store() - .update(cx_a, |channel, cx| { - channel.open_channel_buffer(channel_id, cx) - }) + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); @@ -286,28 +275,30 @@ async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mu let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; - let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + let channel_id = server + .make_channel("the-channel", (&client_a, cx_a), &mut []) + .await; let channel_buffer_1 = client_a .channel_store() - .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); let channel_buffer_2 = client_a .channel_store() - .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); let channel_buffer_3 = client_a .channel_store() - .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); // All concurrent tasks for opening a channel buffer return the same model handle. - let (channel_buffer_1, channel_buffer_2, channel_buffer_3) = + let (channel_buffer, channel_buffer_2, channel_buffer_3) = future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) .await .unwrap(); - let model_id = channel_buffer_1.id(); - assert_eq!(channel_buffer_1, channel_buffer_2); - assert_eq!(channel_buffer_1, channel_buffer_3); + let channel_buffer_model_id = channel_buffer.id(); + assert_eq!(channel_buffer, channel_buffer_2); + assert_eq!(channel_buffer, channel_buffer_3); - channel_buffer_1.update(cx_a, |buffer, cx| { + channel_buffer.update(cx_a, |buffer, cx| { buffer.buffer().update(cx, |buffer, cx| { buffer.edit([(0..0, "hello")], None, cx); }) @@ -315,7 +306,7 @@ async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mu deterministic.run_until_parked(); cx_a.update(|_| { - drop(channel_buffer_1); + drop(channel_buffer); drop(channel_buffer_2); drop(channel_buffer_3); }); @@ -324,10 +315,10 @@ async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mu // The channel buffer can be reopened after dropping it. let channel_buffer = client_a .channel_store() - .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); - assert_ne!(channel_buffer.id(), model_id); + assert_ne!(channel_buffer.id(), channel_buffer_model_id); channel_buffer.update(cx_a, |buffer, cx| { buffer.buffer().update(cx, |buffer, _| { assert_eq!(buffer.text(), "hello"); @@ -347,22 +338,17 @@ async fn test_channel_buffer_disconnect( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let channel_buffer_a = client_a .channel_store() - .update(cx_a, |channel, cx| { - channel.open_channel_buffer(channel_id, cx) - }) + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); - let channel_buffer_b = client_b .channel_store() - .update(cx_b, |channel, cx| { - channel.open_channel_buffer(channel_id, cx) - }) + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); @@ -375,7 +361,7 @@ async fn test_channel_buffer_disconnect( buffer.channel().as_ref(), &Channel { id: channel_id, - name: "zed".to_string() + name: "the-channel".to_string() } ); assert!(!buffer.is_connected()); @@ -403,13 +389,81 @@ async fn test_channel_buffer_disconnect( buffer.channel().as_ref(), &Channel { id: channel_id, - name: "zed".to_string() + name: "the-channel".to_string() } ); assert!(!buffer.is_connected()); }); } +#[gpui::test] +async fn test_rejoin_channel_buffer( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client A disconnects. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + // deterministic.advance_clock(RECEIVE_TIMEOUT); + + // Both clients make an edit. Both clients see their own edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + deterministic.run_until_parked(); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "12"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "01"); + }); + + // Client A reconnects. + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 80eb972f42..c4abe39d47 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -127,6 +127,31 @@ pub fn serialize_undo_map_entry( } } +pub fn split_operations( + mut operations: Vec, +) -> impl Iterator> { + #[cfg(any(test, feature = "test-support"))] + const CHUNK_SIZE: usize = 5; + + #[cfg(not(any(test, feature = "test-support")))] + const CHUNK_SIZE: usize = 100; + + let mut done = false; + std::iter::from_fn(move || { + if done { + return None; + } + + let operations = operations + .drain(..std::cmp::min(CHUNK_SIZE, operations.len())) + .collect::>(); + if operations.is_empty() { + done = true; + } + Some(operations) + }) +} + pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec { selections.iter().map(serialize_selection).collect() } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5cd13b8be8..0690cc9188 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -35,7 +35,7 @@ use language::{ point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, - serialize_anchor, serialize_version, + serialize_anchor, serialize_version, split_operations, }, range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, @@ -8200,31 +8200,6 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { } } -fn split_operations( - mut operations: Vec, -) -> impl Iterator> { - #[cfg(any(test, feature = "test-support"))] - const CHUNK_SIZE: usize = 5; - - #[cfg(not(any(test, feature = "test-support")))] - const CHUNK_SIZE: usize = 100; - - let mut done = false; - std::iter::from_fn(move || { - if done { - return None; - } - - let operations = operations - .drain(..cmp::min(CHUNK_SIZE, operations.len())) - .collect::>(); - if operations.is_empty() { - done = true; - } - Some(operations) - }) -} - fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { language_server_name: symbol.language_server_name.0.to_string(), diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 92c85677f6..fe9093245e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1,6 +1,8 @@ syntax = "proto3"; package zed.messages; +// Looking for a number? Search "// Current max" + message PeerId { uint32 owner_id = 1; uint32 id = 2; @@ -151,6 +153,8 @@ message Envelope { LeaveChannelBuffer leave_channel_buffer = 134; AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; + RejoinChannelBuffers rejoin_channel_buffers = 139; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 140; // Current max } } @@ -616,6 +620,12 @@ message BufferVersion { repeated VectorClockEntry version = 2; } +message ChannelBufferVersion { + uint64 channel_id = 1; + repeated VectorClockEntry version = 2; + uint64 epoch = 3; +} + enum FormatTrigger { Save = 0; Manual = 1; @@ -1008,12 +1018,28 @@ message JoinChannelBuffer { uint64 channel_id = 1; } +message RejoinChannelBuffers { + repeated ChannelBufferVersion buffers = 1; +} + +message RejoinChannelBuffersResponse { + repeated RejoinedChannelBuffer buffers = 1; +} + message JoinChannelBufferResponse { uint64 buffer_id = 1; uint32 replica_id = 2; string base_text = 3; repeated Operation operations = 4; repeated Collaborator collaborators = 5; + uint64 epoch = 6; +} + +message RejoinedChannelBuffer { + uint64 channel_id = 1; + repeated VectorClockEntry version = 2; + repeated Operation operations = 3; + repeated Collaborator collaborators = 4; } message LeaveChannelBuffer { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 2e4dce01e1..a600bc4970 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -229,6 +229,8 @@ messages!( (StartLanguageServer, Foreground), (SynchronizeBuffers, Foreground), (SynchronizeBuffersResponse, Foreground), + (RejoinChannelBuffers, Foreground), + (RejoinChannelBuffersResponse, Foreground), (Test, Foreground), (Unfollow, Foreground), (UnshareProject, Foreground), @@ -319,6 +321,7 @@ request_messages!( (SearchProject, SearchProjectResponse), (ShareProject, ShareProjectResponse), (SynchronizeBuffers, SynchronizeBuffersResponse), + (RejoinChannelBuffers, RejoinChannelBuffersResponse), (Test, Test), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), From d7e4cb4ab10da4f5a6ac936e26db67f5956349de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Sep 2023 16:52:41 -0700 Subject: [PATCH 135/156] executor: timers must be used --- crates/gpui/src/executor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 712c854488..474ea8364f 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -106,6 +106,7 @@ pub struct Deterministic { parker: parking_lot::Mutex, } +#[must_use] pub enum Timer { Production(smol::Timer), #[cfg(any(test, feature = "test-support"))] From e6babce556d07d21faa60f013e9290f50516b157 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Sep 2023 17:23:55 -0700 Subject: [PATCH 136/156] Broadcast new peer ids for rejoined channel collaborators --- crates/channel/src/channel_buffer.rs | 21 ++++++ crates/collab/src/db.rs | 5 ++ crates/collab/src/db/queries/buffers.rs | 69 +++++++++---------- crates/collab/src/rpc.rs | 24 ++++++- .../collab/src/tests/channel_buffer_tests.rs | 14 +++- crates/rpc/proto/zed.proto | 11 ++- crates/rpc/src/proto.rs | 4 +- 7 files changed, 104 insertions(+), 44 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 98ecbc5dcf..e11282cf79 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -10,6 +10,7 @@ pub(crate) fn init(client: &Arc) { client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator); client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator); } pub struct ChannelBuffer { @@ -171,6 +172,26 @@ impl ChannelBuffer { Ok(()) } + async fn handle_update_channel_buffer_collaborator( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + for collaborator in &mut this.collaborators { + if collaborator.peer_id == message.payload.old_peer_id { + collaborator.peer_id = message.payload.new_peer_id; + break; + } + } + cx.emit(Event::CollaboratorsChanged); + cx.notify(); + }); + + Ok(()) + } + fn on_buffer_update( &mut self, _: ModelHandle, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 888158188f..4a9983e600 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -435,6 +435,11 @@ pub struct ChannelsForUser { pub channels_with_admin_privileges: HashSet, } +pub struct RejoinedChannelBuffer { + pub buffer: proto::RejoinedChannelBuffer, + pub old_connection_id: ConnectionId, +} + #[derive(Clone)] pub struct JoinRoom { pub room: proto::Room, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 587ed058ff..79e20a2622 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -94,9 +94,9 @@ impl Database { buffers: &[proto::ChannelBufferVersion], user_id: UserId, connection_id: ConnectionId, - ) -> Result { + ) -> Result> { self.transaction(|tx| async move { - let mut response = proto::RejoinChannelBuffersResponse::default(); + let mut results = Vec::new(); for client_buffer in buffers { let channel_id = ChannelId::from_proto(client_buffer.channel_id); if self @@ -121,28 +121,24 @@ impl Database { continue; } - // If there is still a disconnected collaborator for the user, - // update the connection associated with that collaborator, and reuse - // that replica id. - if let Some(ix) = collaborators - .iter() - .position(|c| c.user_id == user_id && c.connection_lost) - { - let self_collaborator = &mut collaborators[ix]; - *self_collaborator = channel_buffer_collaborator::ActiveModel { - id: ActiveValue::Unchanged(self_collaborator.id), - connection_id: ActiveValue::Set(connection_id.id as i32), - connection_server_id: ActiveValue::Set(ServerId( - connection_id.owner_id as i32, - )), - connection_lost: ActiveValue::Set(false), - ..Default::default() - } - .update(&*tx) - .await?; - } else { + // Find the collaborator record for this user's previous lost + // connection. Update it with the new connection id. + let Some(self_collaborator) = collaborators + .iter_mut() + .find(|c| c.user_id == user_id && c.connection_lost) + else { continue; + }; + let old_connection_id = self_collaborator.connection(); + *self_collaborator = channel_buffer_collaborator::ActiveModel { + id: ActiveValue::Unchanged(self_collaborator.id), + connection_id: ActiveValue::Set(connection_id.id as i32), + connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)), + connection_lost: ActiveValue::Set(false), + ..Default::default() } + .update(&*tx) + .await?; let client_version = version_from_wire(&client_buffer.version); let serialization_version = self @@ -176,22 +172,25 @@ impl Database { } } - response.buffers.push(proto::RejoinedChannelBuffer { - channel_id: client_buffer.channel_id, - version: version_to_wire(&server_version), - operations, - collaborators: collaborators - .into_iter() - .map(|collaborator| proto::Collaborator { - peer_id: Some(collaborator.connection().into()), - user_id: collaborator.user_id.to_proto(), - replica_id: collaborator.replica_id.0 as u32, - }) - .collect(), + results.push(RejoinedChannelBuffer { + old_connection_id, + buffer: proto::RejoinedChannelBuffer { + channel_id: client_buffer.channel_id, + version: version_to_wire(&server_version), + operations, + collaborators: collaborators + .into_iter() + .map(|collaborator| proto::Collaborator { + peer_id: Some(collaborator.connection().into()), + user_id: collaborator.user_id.to_proto(), + replica_id: collaborator.replica_id.0 as u32, + }) + .collect(), + }, }); } - Ok(response) + Ok(results) }) .await } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 06aa00c9b8..d221d1c99e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2553,13 +2553,31 @@ async fn rejoin_channel_buffers( session: Session, ) -> Result<()> { let db = session.db().await; - let rejoin_response = db + let buffers = db .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id) .await?; - // TODO: inform channel buffer collaborators that this user has rejoined. + for buffer in &buffers { + let collaborators_to_notify = buffer + .buffer + .collaborators + .iter() + .filter_map(|c| Some(c.peer_id?.into())); + channel_buffer_updated( + session.connection_id, + collaborators_to_notify, + &proto::UpdateChannelBufferCollaborator { + channel_id: buffer.buffer.channel_id, + old_peer_id: Some(buffer.old_connection_id.into()), + new_peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + } - response.send(rejoin_response)?; + response.send(proto::RejoinChannelBuffersResponse { + buffers: buffers.into_iter().map(|b| b.buffer).collect(), + })?; Ok(()) } diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 5ba5b50429..236771c2a5 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -432,9 +432,8 @@ async fn test_rejoin_channel_buffer( // Client A disconnects. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); - // deterministic.advance_clock(RECEIVE_TIMEOUT); - // Both clients make an edit. Both clients see their own edit. + // Both clients make an edit. channel_buffer_a.update(cx_a, |buffer, cx| { buffer.buffer().update(cx, |buffer, cx| { buffer.edit([(1..1, "2")], None, cx); @@ -445,6 +444,8 @@ async fn test_rejoin_channel_buffer( buffer.edit([(0..0, "0")], None, cx); }) }); + + // Both clients see their own edit. deterministic.run_until_parked(); channel_buffer_a.read_with(cx_a, |buffer, cx| { assert_eq!(buffer.buffer().read(cx).text(), "12"); @@ -453,7 +454,8 @@ async fn test_rejoin_channel_buffer( assert_eq!(buffer.buffer().read(cx).text(), "01"); }); - // Client A reconnects. + // Client A reconnects. Both clients see each other's edits, and see + // the same collaborators. server.allow_connections(); deterministic.advance_clock(RECEIVE_TIMEOUT); channel_buffer_a.read_with(cx_a, |buffer, cx| { @@ -462,6 +464,12 @@ async fn test_rejoin_channel_buffer( channel_buffer_b.read_with(cx_b, |buffer, cx| { assert_eq!(buffer.buffer().read(cx).text(), "012"); }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); } #[track_caller] diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fe9093245e..2e96d79f5e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -153,8 +153,9 @@ message Envelope { LeaveChannelBuffer leave_channel_buffer = 134; AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; - RejoinChannelBuffers rejoin_channel_buffers = 139; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 140; // Current max + UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139; + RejoinChannelBuffers rejoin_channel_buffers = 140; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; // Current max } } @@ -434,6 +435,12 @@ message RemoveChannelBufferCollaborator { PeerId peer_id = 2; } +message UpdateChannelBufferCollaborator { + uint64 channel_id = 1; + PeerId old_peer_id = 2; + PeerId new_peer_id = 3; +} + message GetDefinition { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index a600bc4970..f643a8c168 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -259,6 +259,7 @@ messages!( (UpdateChannelBuffer, Foreground), (RemoveChannelBufferCollaborator, Foreground), (AddChannelBufferCollaborator, Foreground), + (UpdateChannelBufferCollaborator, Foreground), ); request_messages!( @@ -389,7 +390,8 @@ entity_messages!( channel_id, UpdateChannelBuffer, RemoveChannelBufferCollaborator, - AddChannelBufferCollaborator + AddChannelBufferCollaborator, + UpdateChannelBufferCollaborator ); const KIB: usize = 1024; From 6827ddf97d93bfcde40a5e1fdfd36024a5e85cba Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Sep 2023 17:51:00 -0700 Subject: [PATCH 137/156] Start work on refreshing channel buffer collaborators on server restart --- crates/collab/src/db.rs | 5 +++ crates/collab/src/db/queries/buffers.rs | 28 ++++++++++++++--- crates/collab/src/db/queries/servers.rs | 31 ++++++++++++++----- crates/collab/src/rpc.rs | 20 ++++++++++-- .../src/tests/randomized_integration_tests.rs | 4 +-- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 4a9983e600..823990eaf8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -503,6 +503,11 @@ pub struct RefreshedRoom { pub canceled_calls_to_user_ids: Vec, } +pub struct RefreshedChannelBuffer { + pub connection_ids: Vec, + pub removed_collaborators: Vec, +} + pub struct Project { pub collaborators: Vec, pub worktrees: BTreeMap, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 79e20a2622..813255b80e 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -123,10 +123,11 @@ impl Database { // Find the collaborator record for this user's previous lost // connection. Update it with the new connection id. - let Some(self_collaborator) = collaborators - .iter_mut() - .find(|c| c.user_id == user_id && c.connection_lost) - else { + let server_id = ServerId(connection_id.owner_id as i32); + let Some(self_collaborator) = collaborators.iter_mut().find(|c| { + c.user_id == user_id + && (c.connection_lost || c.connection_server_id != server_id) + }) else { continue; }; let old_connection_id = self_collaborator.connection(); @@ -195,6 +196,25 @@ impl Database { .await } + pub async fn refresh_channel_buffer( + &self, + channel_id: ChannelId, + server_id: ServerId, + ) -> Result { + self.transaction(|tx| async move { + let mut connection_ids = Vec::new(); + let mut removed_collaborators = Vec::new(); + + // TODO + + Ok(RefreshedChannelBuffer { + connection_ids, + removed_collaborators, + }) + }) + .await + } + pub async fn leave_channel_buffer( &self, channel_id: ChannelId, diff --git a/crates/collab/src/db/queries/servers.rs b/crates/collab/src/db/queries/servers.rs index 08a2bda16a..2b1d0d2c0c 100644 --- a/crates/collab/src/db/queries/servers.rs +++ b/crates/collab/src/db/queries/servers.rs @@ -14,31 +14,48 @@ impl Database { .await } - pub async fn stale_room_ids( + pub async fn stale_server_resource_ids( &self, environment: &str, new_server_id: ServerId, - ) -> Result> { + ) -> Result<(Vec, Vec)> { self.transaction(|tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { + enum QueryRoomIds { RoomId, } + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, + } + let stale_server_epochs = self .stale_server_ids(environment, new_server_id, &tx) .await?; - Ok(room_participant::Entity::find() + let room_ids = room_participant::Entity::find() .select_only() .column(room_participant::Column::RoomId) .distinct() .filter( room_participant::Column::AnsweringConnectionServerId - .is_in(stale_server_epochs), + .is_in(stale_server_epochs.iter().copied()), ) - .into_values::<_, QueryAs>() + .into_values::<_, QueryRoomIds>() .all(&*tx) - .await?) + .await?; + let channel_ids = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::ChannelId) + .distinct() + .filter( + channel_buffer_collaborator::Column::ConnectionServerId + .is_in(stale_server_epochs.iter().copied()), + ) + .into_values::<_, QueryChannelIds>() + .all(&*tx) + .await?; + Ok((room_ids, channel_ids)) }) .await } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index d221d1c99e..95307ba725 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -278,13 +278,29 @@ impl Server { tracing::info!("waiting for cleanup timeout"); timeout.await; tracing::info!("cleanup timeout expired, retrieving stale rooms"); - if let Some(room_ids) = app_state + if let Some((room_ids, channel_ids)) = app_state .db - .stale_room_ids(&app_state.config.zed_environment, server_id) + .stale_server_resource_ids(&app_state.config.zed_environment, server_id) .await .trace_err() { tracing::info!(stale_room_count = room_ids.len(), "retrieved stale rooms"); + + for channel_id in channel_ids { + if let Some(refreshed_channel_buffer) = app_state + .db + .refresh_channel_buffer(channel_id, server_id) + .await + .trace_err() + { + for connection_id in refreshed_channel_buffer.connection_ids { + for message in &refreshed_channel_buffer.removed_collaborators { + peer.send(connection_id, message.clone()).trace_err(); + } + } + } + } + for room_id in room_ids { let mut contacts_to_update = HashSet::default(); let mut canceled_calls_to_user_ids = Vec::new(); diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index e48753ed41..309fcf7e44 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -307,10 +307,10 @@ async fn apply_server_operation( server.start().await.unwrap(); deterministic.advance_clock(CLEANUP_TIMEOUT); let environment = &server.app_state.config.zed_environment; - let stale_room_ids = server + let (stale_room_ids, _) = server .app_state .db - .stale_room_ids(environment, server.id()) + .stale_server_resource_ids(environment, server.id()) .await .unwrap(); assert_eq!(stale_room_ids, vec![]); From 3a7b551e337826c5514eadeba81f1228a05557a9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 2 Sep 2023 19:43:05 -0600 Subject: [PATCH 138/156] Fix tests with no neovim --- crates/vim/src/test/neovim_connection.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 3e59080b13..e44e8d0e4c 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -237,6 +237,9 @@ impl NeovimConnection { #[cfg(not(feature = "neovim"))] pub async fn set_option(&mut self, value: &str) { + if let Some(NeovimData::Get { .. }) = self.data.front() { + self.data.pop_front(); + }; assert_eq!( self.data.pop_front(), Some(NeovimData::SetOption { From 55dd0b176c47f782d6c1a23c471077ff38823866 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 2 Sep 2023 19:52:18 -0600 Subject: [PATCH 139/156] Use consistent naming --- crates/vim/src/normal/substitute.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index efdd43d0a4..23b545abd8 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -10,8 +10,8 @@ actions!(vim, [Substitute, SubstituteLine]); pub(crate) fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { - let times = vim.pop_number_operator(cx); - substitute(vim, times, vim.state().mode == Mode::VisualLine, cx); + let count = vim.pop_number_operator(cx); + substitute(vim, count, vim.state().mode == Mode::VisualLine, cx); }) }); From 56db21d54bbd0dc83aa1a756152a18092c1ef8be Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 14:38:42 -0600 Subject: [PATCH 140/156] Split ContextMenu actions This should have no user-visible impact. For vim `.` to repeat it's important that actions are replayable. Currently editor::MoveDown *sometimes* moves the cursor down, and *sometimes* selects the next completion. For replay we need to be able to separate the two. --- assets/keymaps/default.json | 11 ++++ crates/editor/src/editor.rs | 74 ++++++++++++++--------- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/scroll.rs | 4 -- crates/editor/src/scroll/scroll_amount.rs | 24 +------- 5 files changed, 57 insertions(+), 58 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1b2d8ce419..fa62a74f3f 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -515,6 +515,17 @@ "enter": "editor::ConfirmCodeAction" } }, + { + "context": "Editor && (showing_code_actions || showing_completions)", + "bindings": { + "up": "editor::ContextMenuPrev", + "ctrl-p": "editor::ContextMenuPrev", + "down": "editor::ContextMenuNext", + "ctrl-n": "editor::ContextMenuNext", + "pageup": "editor::ContextMenuFirst", + "pagedown": "editor::ContextMenuLast" + } + }, // Custom bindings { "bindings": { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d331b0a268..bdd29b04fa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -312,6 +312,10 @@ actions!( CopyPath, CopyRelativePath, CopyHighlightJson, + ContextMenuFirst, + ContextMenuPrev, + ContextMenuNext, + ContextMenuLast, ] ); @@ -468,6 +472,10 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::next_copilot_suggestion); cx.add_action(Editor::previous_copilot_suggestion); cx.add_action(Editor::copilot_suggest); + cx.add_action(Editor::context_menu_first); + cx.add_action(Editor::context_menu_prev); + cx.add_action(Editor::context_menu_next); + cx.add_action(Editor::context_menu_last); hover_popover::init(cx); scroll::actions::init(cx); @@ -5166,12 +5174,6 @@ impl Editor { return; } - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_prev(cx) { - return; - } - } - if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); return; @@ -5194,15 +5196,6 @@ impl Editor { return; } - if self - .context_menu - .as_mut() - .map(|menu| menu.select_first(cx)) - .unwrap_or(false) - { - return; - } - if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); return; @@ -5242,12 +5235,6 @@ impl Editor { pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { self.take_rename(true, cx); - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_next(cx) { - return; - } - } - if self.mode == EditorMode::SingleLine { cx.propagate_action(); return; @@ -5315,6 +5302,30 @@ impl Editor { }); } + pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.as_mut() { + context_menu.select_first(cx); + } + } + + pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.as_mut() { + context_menu.select_prev(cx); + } + } + + pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.as_mut() { + context_menu.select_next(cx); + } + } + + pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.as_mut() { + context_menu.select_last(cx); + } + } + pub fn move_to_previous_word_start( &mut self, _: &MoveToPreviousWordStart, @@ -8666,17 +8677,20 @@ impl View for Editor { if self.pending_rename.is_some() { keymap.add_identifier("renaming"); } - match self.context_menu.as_ref() { - Some(ContextMenu::Completions(_)) => { - keymap.add_identifier("menu"); - keymap.add_identifier("showing_completions") + if self.context_menu_visible() { + match self.context_menu.as_ref() { + Some(ContextMenu::Completions(_)) => { + keymap.add_identifier("menu"); + keymap.add_identifier("showing_completions") + } + Some(ContextMenu::CodeActions(_)) => { + keymap.add_identifier("menu"); + keymap.add_identifier("showing_code_actions") + } + None => {} } - Some(ContextMenu::CodeActions(_)) => { - keymap.add_identifier("menu"); - keymap.add_identifier("showing_code_actions") - } - None => {} } + for layer in self.keymap_context_layers.values() { keymap.extend(layer); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ad97639d0b..74bd67e03a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5340,7 +5340,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { cx.condition(|editor, _| editor.context_menu_visible()) .await; let apply_additional_edits = cx.update_editor(|editor, cx| { - editor.move_down(&MoveDown, cx); + editor.context_menu_next(&Default::default(), cx); editor .confirm_completion(&ConfirmCompletion::default(), cx) .unwrap() diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index d87bc0ae4f..8233f92a1a 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -378,10 +378,6 @@ impl Editor { return; } - if amount.move_context_menu_selection(self, cx) { - return; - } - let cur_position = self.scroll_position(cx); let new_pos = cur_position + vec2f(0., amount.lines(self)); self.set_scroll_position(new_pos, cx); diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index cadf37b31d..0edab2bdfc 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -1,8 +1,5 @@ -use gpui::ViewContext; -use serde::Deserialize; -use util::iife; - use crate::Editor; +use serde::Deserialize; #[derive(Clone, PartialEq, Deserialize)] pub enum ScrollAmount { @@ -13,25 +10,6 @@ pub enum ScrollAmount { } impl ScrollAmount { - pub fn move_context_menu_selection( - &self, - editor: &mut Editor, - cx: &mut ViewContext, - ) -> bool { - iife!({ - let context_menu = editor.context_menu.as_mut()?; - - match self { - Self::Line(c) if *c > 0. => context_menu.select_next(cx), - Self::Line(_) => context_menu.select_prev(cx), - Self::Page(c) if *c > 0. => context_menu.select_last(cx), - Self::Page(_) => context_menu.select_first(cx), - } - .then_some(()) - }) - .is_some() - } - pub fn lines(&self, editor: &mut Editor) -> f32 { match self { Self::Line(count) => *count, From e2479a7172a617bb61804973c3dbb2f3b924d87a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Sep 2023 10:24:49 -0400 Subject: [PATCH 141/156] Fix cropped search filters --- styles/src/style_tree/search.ts | 4 ++-- styles/src/style_tree/toolbar.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index a93aab4ea8..8174690fde 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -48,7 +48,7 @@ export default function search(): any { } return { - padding: { top: 0, bottom: 0 }, + padding: { top: 4, bottom: 4 }, option_button: toggleable({ base: interactive({ @@ -394,7 +394,7 @@ export default function search(): any { }), }, }), - search_bar_row_height: 32, + search_bar_row_height: 34, 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 01a09a0616..adf8fb866f 100644 --- a/styles/src/style_tree/toolbar.ts +++ b/styles/src/style_tree/toolbar.ts @@ -8,8 +8,8 @@ export const toolbar = () => { const theme = useTheme() return { - height: 32, - padding: { left: 4, right: 4, top: 4, bottom: 4 }, + height: 42, + padding: { left: 4, right: 4 }, background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 4, From ec5ff20b4ca31561c49590f5f61cc65ee5551588 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Sep 2023 11:34:24 -0700 Subject: [PATCH 142/156] Implement clearing stale channel buffer participants on server restart Co-authored-by: Mikayla --- crates/channel/src/channel_store.rs | 4 + crates/collab/src/db.rs | 1 + crates/collab/src/db/queries/buffers.rs | 26 ++++- crates/collab/src/db/queries/rooms.rs | 2 +- crates/collab/src/db/queries/servers.rs | 1 + crates/collab/src/rpc.rs | 9 +- .../collab/src/tests/channel_buffer_tests.rs | 96 ++++++++++++++++++- 7 files changed, 133 insertions(+), 6 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index ec1652581d..3d2f61d61f 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -500,6 +500,10 @@ impl ChannelStore { } } + if buffer_versions.is_empty() { + return Task::ready(Ok(())); + } + let response = self.client.request(proto::RejoinChannelBuffers { buffers: buffer_versions, }); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 823990eaf8..b5d968ddf3 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -435,6 +435,7 @@ pub struct ChannelsForUser { pub channels_with_admin_privileges: HashSet, } +#[derive(Debug)] pub struct RejoinedChannelBuffer { pub buffer: proto::RejoinedChannelBuffer, pub old_connection_id: ConnectionId, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 813255b80e..8236eb9c3b 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -118,6 +118,7 @@ impl Database { // connection, then the client's buffer can be syncronized with // the server's buffer. if buffer.epoch as u64 != client_buffer.epoch { + log::info!("can't rejoin buffer, epoch has changed"); continue; } @@ -128,6 +129,7 @@ impl Database { c.user_id == user_id && (c.connection_lost || c.connection_server_id != server_id) }) else { + log::info!("can't rejoin buffer, no previous collaborator found"); continue; }; let old_connection_id = self_collaborator.connection(); @@ -196,16 +198,36 @@ impl Database { .await } - pub async fn refresh_channel_buffer( + pub async fn clear_stale_channel_buffer_collaborators( &self, channel_id: ChannelId, server_id: ServerId, ) -> Result { self.transaction(|tx| async move { + let collaborators = channel_buffer_collaborator::Entity::find() + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .all(&*tx) + .await?; + let mut connection_ids = Vec::new(); let mut removed_collaborators = Vec::new(); + let mut collaborator_ids_to_remove = Vec::new(); + for collaborator in &collaborators { + if !collaborator.connection_lost && collaborator.connection_server_id == server_id { + connection_ids.push(collaborator.connection()); + } else { + removed_collaborators.push(proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(collaborator.connection().into()), + }); + collaborator_ids_to_remove.push(collaborator.id); + } + } - // TODO + channel_buffer_collaborator::Entity::delete_many() + .filter(channel_buffer_collaborator::Column::Id.is_in(collaborator_ids_to_remove)) + .exec(&*tx) + .await?; Ok(RefreshedChannelBuffer { connection_ids, diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 435e729fed..e348b50bee 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1,7 +1,7 @@ use super::*; impl Database { - pub async fn refresh_room( + pub async fn clear_stale_room_participants( &self, room_id: RoomId, new_server_id: ServerId, diff --git a/crates/collab/src/db/queries/servers.rs b/crates/collab/src/db/queries/servers.rs index 2b1d0d2c0c..e5ceee8887 100644 --- a/crates/collab/src/db/queries/servers.rs +++ b/crates/collab/src/db/queries/servers.rs @@ -55,6 +55,7 @@ impl Database { .into_values::<_, QueryChannelIds>() .all(&*tx) .await?; + Ok((room_ids, channel_ids)) }) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 95307ba725..e454fcbb9e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -285,11 +285,15 @@ impl Server { .trace_err() { tracing::info!(stale_room_count = room_ids.len(), "retrieved stale rooms"); + tracing::info!( + stale_channel_buffer_count = channel_ids.len(), + "retrieved stale channel buffers" + ); for channel_id in channel_ids { if let Some(refreshed_channel_buffer) = app_state .db - .refresh_channel_buffer(channel_id, server_id) + .clear_stale_channel_buffer_collaborators(channel_id, server_id) .await .trace_err() { @@ -309,7 +313,7 @@ impl Server { if let Some(mut refreshed_room) = app_state .db - .refresh_room(room_id, server_id) + .clear_stale_room_participants(room_id, server_id) .await .trace_err() { @@ -873,6 +877,7 @@ async fn connection_lost( futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { + log::info!("connection lost, removing all resources for user:{}, connection:{:?}", session.user_id, session.connection_id); leave_room_for_session(&session).await.trace_err(); leave_channel_buffers_for_session(&session) .await diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 236771c2a5..fe286895b4 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,4 +1,7 @@ -use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use crate::{ + rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, + tests::TestServer, +}; use call::ActiveCall; use channel::Channel; use client::UserId; @@ -472,6 +475,97 @@ async fn test_rejoin_channel_buffer( }); } +#[gpui::test] +async fn test_channel_buffers_and_server_restarts( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "the-channel", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let _channel_buffer_c = client_c + .channel_store() + .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client C can't reconnect. + client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); + + // Server stops. + server.reset().await; + deterministic.advance_clock(RECEIVE_TIMEOUT); + + // While the server is down, both clients make an edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + + // Server restarts. + server.start().await.unwrap(); + deterministic.advance_clock(CLEANUP_TIMEOUT); + + // Clients reconnects. Clients A and B see each other's edits, and see + // that client C has disconnected. + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_eq!( + buffer_a + .collaborators() + .iter() + .map(|c| c.user_id) + .collect::>(), + vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()] + ); + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( From 653d4976cd4a03e1043b3c4a50453a1ed5e27aeb Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 5 Sep 2023 17:13:09 -0400 Subject: [PATCH 143/156] Add operation for opening channel notes in channel based calls --- crates/call/src/call.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 5af094df05..5886462ccf 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -403,7 +403,7 @@ impl ActiveCall { &self.pending_invites } - fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { if let Some(room) = self.room() { let room = room.read(cx); Self::report_call_event_for_room( diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index daaa483975..d27cdc8acf 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2249,6 +2249,9 @@ impl CollabPanel { anyhow::Ok(()) }) .detach(); + ActiveCall::global(cx).update(cx, |call, cx| { + call.report_call_event("open channel notes", cx) + }); } } From 8d672f5d4cd532217209e6826728afc641e7c6d9 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 9 Aug 2023 21:32:41 -0400 Subject: [PATCH 144/156] Remove NodeRuntime static & add fake implementation for tests --- Cargo.lock | 1 + crates/copilot/src/copilot.rs | 13 +- .../LiveKitBridge/Package.resolved | 4 +- crates/node_runtime/Cargo.toml | 1 + crates/node_runtime/src/node_runtime.rs | 159 +++++++++++------- crates/zed/src/languages.rs | 2 +- crates/zed/src/languages/css.rs | 12 +- crates/zed/src/languages/html.rs | 12 +- crates/zed/src/languages/json.rs | 12 +- crates/zed/src/languages/php.rs | 12 +- crates/zed/src/languages/python.rs | 12 +- crates/zed/src/languages/svelte.rs | 12 +- crates/zed/src/languages/tailwind.rs | 12 +- crates/zed/src/languages/typescript.rs | 22 +-- crates/zed/src/languages/yaml.rs | 15 +- crates/zed/src/main.rs | 4 +- crates/zed/src/zed.rs | 5 +- 17 files changed, 179 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8786c8ed6e..05cd0ec21c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4582,6 +4582,7 @@ dependencies = [ "anyhow", "async-compression", "async-tar", + "async-trait", "futures 0.3.28", "gpui", "log", diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 499ae2e808..28c20d95bb 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -41,7 +41,7 @@ actions!( [Suggest, NextSuggestion, PreviousSuggestion, Reinstall] ); -pub fn init(http: Arc, node_runtime: Arc, cx: &mut AppContext) { +pub fn init(http: Arc, node_runtime: Arc, cx: &mut AppContext) { let copilot = cx.add_model({ let node_runtime = node_runtime.clone(); move |cx| Copilot::start(http, node_runtime, cx) @@ -265,7 +265,7 @@ pub struct Completion { pub struct Copilot { http: Arc, - node_runtime: Arc, + node_runtime: Arc, server: CopilotServer, buffers: HashSet>, } @@ -299,7 +299,7 @@ impl Copilot { fn start( http: Arc, - node_runtime: Arc, + node_runtime: Arc, cx: &mut ModelContext, ) -> Self { let mut this = Self { @@ -335,12 +335,15 @@ impl Copilot { #[cfg(any(test, feature = "test-support"))] pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle, lsp::FakeLanguageServer) { + use node_runtime::FakeNodeRuntime; + let (server, fake_server) = LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); + let node_runtime = FakeNodeRuntime::new(); let this = cx.add_model(|_| Self { http: http.clone(), - node_runtime: NodeRuntime::instance(http), + node_runtime, server: CopilotServer::Running(RunningCopilotServer { lsp: Arc::new(server), sign_in_status: SignInStatus::Authorized, @@ -353,7 +356,7 @@ impl Copilot { fn start_language_server( http: Arc, - node_runtime: Arc, + node_runtime: Arc, this: ModelHandle, mut cx: AsyncAppContext, ) -> impl Future { diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index 85ae088565..b925bc8f0d 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e", - "version": "1.21.0" + "revision": "ce20dc083ee485524b802669890291c0d8090170", + "version": "1.22.1" } } ] diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index 53635f2725..2b9503468a 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -14,6 +14,7 @@ util = { path = "../util" } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-tar = "0.4.2" futures.workspace = true +async-trait.workspace = true anyhow.workspace = true parking_lot.workspace = true serde.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index d43c14ec7b..820a8b6f81 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -7,14 +7,12 @@ use std::process::{Output, Stdio}; use std::{ env::consts, path::{Path, PathBuf}, - sync::{Arc, OnceLock}, + sync::Arc, }; use util::http::HttpClient; const VERSION: &str = "v18.15.0"; -static RUNTIME_INSTANCE: OnceLock> = OnceLock::new(); - #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct NpmInfo { @@ -28,23 +26,88 @@ pub struct NpmInfoDistTags { latest: Option, } -pub struct NodeRuntime { +#[async_trait::async_trait] +pub trait NodeRuntime: Send + Sync { + async fn binary_path(&self) -> Result; + + async fn run_npm_subcommand( + &self, + directory: Option<&Path>, + subcommand: &str, + args: &[&str], + ) -> Result; + + async fn npm_package_latest_version(&self, name: &str) -> Result; + + async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)]) + -> Result<()>; +} + +pub struct RealNodeRuntime { http: Arc, } -impl NodeRuntime { - pub fn instance(http: Arc) -> Arc { - RUNTIME_INSTANCE - .get_or_init(|| Arc::new(NodeRuntime { http })) - .clone() +impl RealNodeRuntime { + pub fn new(http: Arc) -> Arc { + Arc::new(RealNodeRuntime { http }) } - pub async fn binary_path(&self) -> Result { + async fn install_if_needed(&self) -> Result { + log::info!("Node runtime install_if_needed"); + + let arch = match consts::ARCH { + "x86_64" => "x64", + "aarch64" => "arm64", + other => bail!("Running on unsupported platform: {other}"), + }; + + let folder_name = format!("node-{VERSION}-darwin-{arch}"); + let node_containing_dir = util::paths::SUPPORT_DIR.join("node"); + let node_dir = node_containing_dir.join(folder_name); + let node_binary = node_dir.join("bin/node"); + let npm_file = node_dir.join("bin/npm"); + + let result = Command::new(&node_binary) + .arg(npm_file) + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await; + let valid = matches!(result, Ok(status) if status.success()); + + if !valid { + _ = fs::remove_dir_all(&node_containing_dir).await; + fs::create_dir(&node_containing_dir) + .await + .context("error creating node containing dir")?; + + let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz"); + let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}"); + let mut response = self + .http + .get(&url, Default::default(), true) + .await + .context("error downloading Node binary tarball")?; + + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&node_containing_dir).await?; + } + + anyhow::Ok(node_dir) + } +} + +#[async_trait::async_trait] +impl NodeRuntime for RealNodeRuntime { + async fn binary_path(&self) -> Result { let installation_path = self.install_if_needed().await?; Ok(installation_path.join("bin/node")) } - pub async fn run_npm_subcommand( + async fn run_npm_subcommand( &self, directory: Option<&Path>, subcommand: &str, @@ -106,7 +169,7 @@ impl NodeRuntime { output.map_err(|e| anyhow!("{e}")) } - pub async fn npm_package_latest_version(&self, name: &str) -> Result { + async fn npm_package_latest_version(&self, name: &str) -> Result { let output = self .run_npm_subcommand( None, @@ -131,10 +194,10 @@ impl NodeRuntime { .ok_or_else(|| anyhow!("no version found for npm package {}", name)) } - pub async fn npm_install_packages( + async fn npm_install_packages( &self, directory: &Path, - packages: impl IntoIterator, + packages: &[(&str, &str)], ) -> Result<()> { let packages: Vec<_> = packages .into_iter() @@ -155,51 +218,31 @@ impl NodeRuntime { .await?; Ok(()) } +} - async fn install_if_needed(&self) -> Result { - log::info!("Node runtime install_if_needed"); +pub struct FakeNodeRuntime; - let arch = match consts::ARCH { - "x86_64" => "x64", - "aarch64" => "arm64", - other => bail!("Running on unsupported platform: {other}"), - }; - - let folder_name = format!("node-{VERSION}-darwin-{arch}"); - let node_containing_dir = util::paths::SUPPORT_DIR.join("node"); - let node_dir = node_containing_dir.join(folder_name); - let node_binary = node_dir.join("bin/node"); - let npm_file = node_dir.join("bin/npm"); - - let result = Command::new(&node_binary) - .arg(npm_file) - .arg("--version") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await; - let valid = matches!(result, Ok(status) if status.success()); - - if !valid { - _ = fs::remove_dir_all(&node_containing_dir).await; - fs::create_dir(&node_containing_dir) - .await - .context("error creating node containing dir")?; - - let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz"); - let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}"); - let mut response = self - .http - .get(&url, Default::default(), true) - .await - .context("error downloading Node binary tarball")?; - - let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(&node_containing_dir).await?; - } - - anyhow::Ok(node_dir) +impl FakeNodeRuntime { + pub fn new() -> Arc { + Arc::new(FakeNodeRuntime) + } +} + +#[async_trait::async_trait] +impl NodeRuntime for FakeNodeRuntime { + async fn binary_path(&self) -> Result { + unreachable!() + } + + async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result { + unreachable!() + } + + async fn npm_package_latest_version(&self, _: &str) -> Result { + unreachable!() + } + + async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> { + unreachable!() } } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index f0b8a1444a..3fbb5aa14f 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -37,7 +37,7 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; -pub fn init(languages: Arc, node_runtime: Arc) { +pub fn init(languages: Arc, node_runtime: Arc) { let language = |name, grammar, adapters| { languages.register(name, load_config(name), grammar, adapters, load_queries) }; diff --git a/crates/zed/src/languages/css.rs b/crates/zed/src/languages/css.rs index f2103050f3..fdbc179209 100644 --- a/crates/zed/src/languages/css.rs +++ b/crates/zed/src/languages/css.rs @@ -22,11 +22,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct CssLspAdapter { - node: Arc, + node: Arc, } impl CssLspAdapter { - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { CssLspAdapter { node } } } @@ -65,7 +65,7 @@ impl LspAdapter for CssLspAdapter { self.node .npm_install_packages( &container_dir, - [("vscode-langservers-extracted", version.as_str())], + &[("vscode-langservers-extracted", version.as_str())], ) .await?; } @@ -81,14 +81,14 @@ impl LspAdapter for CssLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } async fn initialization_options(&self) -> Option { @@ -100,7 +100,7 @@ impl LspAdapter for CssLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index cfb6a5dde9..b8f1c70cce 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -22,11 +22,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct HtmlLspAdapter { - node: Arc, + node: Arc, } impl HtmlLspAdapter { - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { HtmlLspAdapter { node } } } @@ -65,7 +65,7 @@ impl LspAdapter for HtmlLspAdapter { self.node .npm_install_packages( &container_dir, - [("vscode-langservers-extracted", version.as_str())], + &[("vscode-langservers-extracted", version.as_str())], ) .await?; } @@ -81,14 +81,14 @@ impl LspAdapter for HtmlLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } async fn initialization_options(&self) -> Option { @@ -100,7 +100,7 @@ impl LspAdapter for HtmlLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 049549ac5d..63f909ae2a 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -27,12 +27,12 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct JsonLspAdapter { - node: Arc, + node: Arc, languages: Arc, } impl JsonLspAdapter { - pub fn new(node: Arc, languages: Arc) -> Self { + pub fn new(node: Arc, languages: Arc) -> Self { JsonLspAdapter { node, languages } } } @@ -71,7 +71,7 @@ impl LspAdapter for JsonLspAdapter { self.node .npm_install_packages( &container_dir, - [("vscode-json-languageserver", version.as_str())], + &[("vscode-json-languageserver", version.as_str())], ) .await?; } @@ -87,14 +87,14 @@ impl LspAdapter for JsonLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } async fn initialization_options(&self) -> Option { @@ -148,7 +148,7 @@ impl LspAdapter for JsonLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index 73bb4b019c..3096fd16e6 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -23,14 +23,14 @@ fn intelephense_server_binary_arguments(server_path: &Path) -> Vec { pub struct IntelephenseVersion(String); pub struct IntelephenseLspAdapter { - node: Arc, + node: Arc, } impl IntelephenseLspAdapter { const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js"; #[allow(unused)] - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { Self { node } } } @@ -65,7 +65,7 @@ impl LspAdapter for IntelephenseLspAdapter { if fs::metadata(&server_path).await.is_err() { self.node - .npm_install_packages(&container_dir, [("intelephense", version.0.as_str())]) + .npm_install_packages(&container_dir, &[("intelephense", version.0.as_str())]) .await?; } Ok(LanguageServerBinary { @@ -79,14 +79,14 @@ impl LspAdapter for IntelephenseLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } async fn label_for_completion( @@ -107,7 +107,7 @@ impl LspAdapter for IntelephenseLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 956cf49551..c1539e9590 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -20,11 +20,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct PythonLspAdapter { - node: Arc, + node: Arc, } impl PythonLspAdapter { - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { PythonLspAdapter { node } } } @@ -57,7 +57,7 @@ impl LspAdapter for PythonLspAdapter { if fs::metadata(&server_path).await.is_err() { self.node - .npm_install_packages(&container_dir, [("pyright", version.as_str())]) + .npm_install_packages(&container_dir, &[("pyright", version.as_str())]) .await?; } @@ -72,14 +72,14 @@ impl LspAdapter for PythonLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } async fn process_completion(&self, item: &mut lsp::CompletionItem) { @@ -162,7 +162,7 @@ impl LspAdapter for PythonLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/languages/svelte.rs b/crates/zed/src/languages/svelte.rs index 35665e864f..5e42d80e77 100644 --- a/crates/zed/src/languages/svelte.rs +++ b/crates/zed/src/languages/svelte.rs @@ -21,11 +21,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct SvelteLspAdapter { - node: Arc, + node: Arc, } impl SvelteLspAdapter { - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { SvelteLspAdapter { node } } } @@ -64,7 +64,7 @@ impl LspAdapter for SvelteLspAdapter { self.node .npm_install_packages( &container_dir, - [("svelte-language-server", version.as_str())], + &[("svelte-language-server", version.as_str())], ) .await?; } @@ -80,14 +80,14 @@ impl LspAdapter for SvelteLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } async fn initialization_options(&self) -> Option { @@ -99,7 +99,7 @@ impl LspAdapter for SvelteLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 12a0a4e3b8..cf07fa71c9 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -26,11 +26,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct TailwindLspAdapter { - node: Arc, + node: Arc, } impl TailwindLspAdapter { - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { TailwindLspAdapter { node } } } @@ -69,7 +69,7 @@ impl LspAdapter for TailwindLspAdapter { self.node .npm_install_packages( &container_dir, - [("@tailwindcss/language-server", version.as_str())], + &[("@tailwindcss/language-server", version.as_str())], ) .await?; } @@ -85,14 +85,14 @@ impl LspAdapter for TailwindLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } async fn initialization_options(&self) -> Option { @@ -131,7 +131,7 @@ impl LspAdapter for TailwindLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 27074e164b..676d0fd4c0 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -33,14 +33,14 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec { } pub struct TypeScriptLspAdapter { - node: Arc, + node: Arc, } impl TypeScriptLspAdapter { const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js"; const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs"; - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { TypeScriptLspAdapter { node } } } @@ -86,7 +86,7 @@ impl LspAdapter for TypeScriptLspAdapter { self.node .npm_install_packages( &container_dir, - [ + &[ ("typescript", version.typescript_version.as_str()), ( "typescript-language-server", @@ -108,14 +108,14 @@ impl LspAdapter for TypeScriptLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_ts_server_binary(container_dir, &self.node).await + get_cached_ts_server_binary(container_dir, &*self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_ts_server_binary(container_dir, &self.node).await + get_cached_ts_server_binary(container_dir, &*self.node).await } fn code_action_kinds(&self) -> Option> { @@ -165,7 +165,7 @@ impl LspAdapter for TypeScriptLspAdapter { async fn get_cached_ts_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH); @@ -192,14 +192,14 @@ async fn get_cached_ts_server_binary( } pub struct EsLintLspAdapter { - node: Arc, + node: Arc, } impl EsLintLspAdapter { const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js"; #[allow(unused)] - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { EsLintLspAdapter { node } } } @@ -288,14 +288,14 @@ impl LspAdapter for EsLintLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_eslint_server_binary(container_dir, &self.node).await + get_cached_eslint_server_binary(container_dir, &*self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_eslint_server_binary(container_dir, &self.node).await + get_cached_eslint_server_binary(container_dir, &*self.node).await } async fn label_for_completion( @@ -313,7 +313,7 @@ impl LspAdapter for EsLintLspAdapter { async fn get_cached_eslint_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { // This is unfortunate but we don't know what the version is to build a path directly diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 21155cc231..8b438d0949 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -25,11 +25,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct YamlLspAdapter { - node: Arc, + node: Arc, } impl YamlLspAdapter { - pub fn new(node: Arc) -> Self { + pub fn new(node: Arc) -> Self { YamlLspAdapter { node } } } @@ -66,7 +66,10 @@ impl LspAdapter for YamlLspAdapter { if fs::metadata(&server_path).await.is_err() { self.node - .npm_install_packages(&container_dir, [("yaml-language-server", version.as_str())]) + .npm_install_packages( + &container_dir, + &[("yaml-language-server", version.as_str())], + ) .await?; } @@ -81,14 +84,14 @@ impl LspAdapter for YamlLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + 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 + get_cached_server_binary(container_dir, &*self.node).await } fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { let tab_size = all_language_settings(None, cx) @@ -109,7 +112,7 @@ impl LspAdapter for YamlLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &NodeRuntime, + node: &dyn NodeRuntime, ) -> Option { (|| async move { let mut last_version_dir = None; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3e0a8a7a07..f78a4f6419 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -19,7 +19,7 @@ use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task}; use isahc::{config::Configurable, Request}; use language::{LanguageRegistry, Point}; use log::LevelFilter; -use node_runtime::NodeRuntime; +use node_runtime::RealNodeRuntime; use parking_lot::Mutex; use project::Fs; use serde::{Deserialize, Serialize}; @@ -138,7 +138,7 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - let node_runtime = NodeRuntime::instance(http.clone()); + let node_runtime = RealNodeRuntime::new(http.clone()); languages::init(languages.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ba8fa840f5..424bce60f2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -723,7 +723,6 @@ mod tests { AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, }; use language::LanguageRegistry; - use node_runtime::NodeRuntime; use project::{Project, ProjectPath}; use serde_json::json; use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; @@ -732,7 +731,6 @@ mod tests { path::{Path, PathBuf}, }; use theme::{ThemeRegistry, ThemeSettings}; - use util::http::FakeHttpClient; use workspace::{ item::{Item, ItemHandle}, open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle, @@ -2364,8 +2362,7 @@ mod tests { let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); - let http = FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::instance(http); + let node_runtime = node_runtime::FakeNodeRuntime::new(); languages::init(languages.clone(), node_runtime); for name in languages.language_names() { languages.language_for_name(&name); From 29e35531af141472ca10824114e69d1969c0e026 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Sep 2023 12:52:23 -0400 Subject: [PATCH 145/156] Temporarily comment out cargo check commands --- script/bump-zed-minor-versions | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/bump-zed-minor-versions b/script/bump-zed-minor-versions index 8dcf7e334e..2f1ad7345e 100755 --- a/script/bump-zed-minor-versions +++ b/script/bump-zed-minor-versions @@ -31,7 +31,7 @@ preview_tag_name="v${major}.${minor}.${patch}-pre" git fetch origin ${prev_minor_branch_name}:${prev_minor_branch_name} git fetch origin --tags -cargo check -q +# cargo check -q function cleanup { git checkout -q main @@ -89,7 +89,7 @@ git checkout -q main git clean -q -dff old_main_sha=$(git rev-parse HEAD) cargo set-version --package zed --bump minor -cargo check -q +# cargo check -q git commit -q --all --message "${next_minor_branch_name} dev" cat < Date: Wed, 6 Sep 2023 12:52:41 -0400 Subject: [PATCH 146/156] v0.104.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 05cd0ec21c..eb8ba7675f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9761,7 +9761,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.103.0" +version = "0.104.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 66d55b38f0..e102a66519 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.103.0" +version = "0.104.0" publish = false [lib] From 5b5c232cd13f087698da51e5ecd0104a1a10ee9a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Sep 2023 12:54:53 -0400 Subject: [PATCH 147/156] Revert "Temporarily comment out cargo check commands" This reverts commit 29e35531af141472ca10824114e69d1969c0e026. --- script/bump-zed-minor-versions | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/bump-zed-minor-versions b/script/bump-zed-minor-versions index 2f1ad7345e..8dcf7e334e 100755 --- a/script/bump-zed-minor-versions +++ b/script/bump-zed-minor-versions @@ -31,7 +31,7 @@ preview_tag_name="v${major}.${minor}.${patch}-pre" git fetch origin ${prev_minor_branch_name}:${prev_minor_branch_name} git fetch origin --tags -# cargo check -q +cargo check -q function cleanup { git checkout -q main @@ -89,7 +89,7 @@ git checkout -q main git clean -q -dff old_main_sha=$(git rev-parse HEAD) cargo set-version --package zed --bump minor -# cargo check -q +cargo check -q git commit -q --all --message "${next_minor_branch_name} dev" cat < Date: Wed, 6 Sep 2023 13:33:39 -0400 Subject: [PATCH 148/156] collab 0.20.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- crates/live_kit_client/LiveKitBridge/Package.resolved | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb8ba7675f..0b11bce1fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,7 +1453,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 914e3f2dfb..fbdfbd2fe3 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.19.0" +version = "0.20.0" publish = false [[bin]] diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index b925bc8f0d..85ae088565 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": "ce20dc083ee485524b802669890291c0d8090170", - "version": "1.22.1" + "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e", + "version": "1.21.0" } } ] From 17237f748ce984a6285fe91ace515ba5830e4916 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 6 Sep 2023 15:09:15 -0400 Subject: [PATCH 149/156] update token_count for OpenAIEmbeddings to accomodate for truncation --- crates/semantic_index/src/embedding.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 97c25ca170..8140e244bd 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -181,18 +181,17 @@ impl EmbeddingProvider for OpenAIEmbeddings { fn truncate(&self, span: &str) -> (String, usize) { let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span); - let token_count = tokens.len(); - let output = if token_count > OPENAI_INPUT_LIMIT { + let output = if tokens.len() > OPENAI_INPUT_LIMIT { tokens.truncate(OPENAI_INPUT_LIMIT); OPENAI_BPE_TOKENIZER - .decode(tokens) + .decode(tokens.clone()) .ok() .unwrap_or_else(|| span.to_string()) } else { span.to_string() }; - (output, token_count) + (output, tokens.len()) } async fn embed_batch(&self, spans: Vec) -> Result> { From 265d02a583b01dd7b5e829f77efbfd415bec603e Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 6 Sep 2023 15:09:46 -0400 Subject: [PATCH 150/156] update request timeout for open ai embeddings --- crates/semantic_index/src/embedding.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 8140e244bd..7228738525 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -203,7 +203,7 @@ impl EmbeddingProvider for OpenAIEmbeddings { .ok_or_else(|| anyhow!("no api key"))?; let mut request_number = 0; - let mut request_timeout: u64 = 10; + let mut request_timeout: u64 = 15; let mut response: Response; while request_number < MAX_RETRIES { response = self From 66c3879306cb00f319ed78604cf689466e4f8ed8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Sep 2023 09:27:57 -0700 Subject: [PATCH 151/156] Extract randomized test infrastructure for use in other tests --- Cargo.lock | 1 + crates/collab/Cargo.toml | 1 + crates/collab/src/tests.rs | 557 +---- .../src/tests/random_channel_buffer_tests.rs | 49 + .../random_project_collaboration_tests.rs | 1573 ++++++++++++ .../src/tests/randomized_integration_tests.rs | 2199 ----------------- .../src/tests/randomized_test_helpers.rs | 694 ++++++ crates/collab/src/tests/test_server.rs | 551 +++++ crates/gpui_macros/src/gpui_macros.rs | 10 +- 9 files changed, 2887 insertions(+), 2748 deletions(-) create mode 100644 crates/collab/src/tests/random_channel_buffer_tests.rs create mode 100644 crates/collab/src/tests/random_project_collaboration_tests.rs delete mode 100644 crates/collab/src/tests/randomized_integration_tests.rs create mode 100644 crates/collab/src/tests/randomized_test_helpers.rs create mode 100644 crates/collab/src/tests/test_server.rs diff --git a/Cargo.lock b/Cargo.lock index a185542c63..4f68c54433 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1456,6 +1456,7 @@ name = "collab" version = "0.19.0" dependencies = [ "anyhow", + "async-trait", "async-tungstenite", "audio", "axum", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 914e3f2dfb..0346558407 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -80,6 +80,7 @@ theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } collab_ui = { path = "../collab_ui", features = ["test-support"] } +async-trait.workspace = true ctor.workspace = true env_logger.workspace = true indoc.workspace = true diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 25f059c0aa..3000f0d8c3 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,555 +1,18 @@ -use crate::{ - db::{tests::TestDb, NewUserParams, UserId}, - executor::Executor, - rpc::{Server, CLEANUP_TIMEOUT}, - AppState, -}; -use anyhow::anyhow; -use call::{ActiveCall, Room}; -use channel::ChannelStore; -use client::{ - self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, -}; -use collections::{HashMap, HashSet}; -use fs::FakeFs; -use futures::{channel::oneshot, StreamExt as _}; -use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; -use language::LanguageRegistry; -use parking_lot::Mutex; -use project::{Project, WorktreeId}; -use settings::SettingsStore; -use std::{ - cell::{Ref, RefCell, RefMut}, - env, - ops::{Deref, DerefMut}, - path::Path, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - Arc, - }, -}; -use util::http::FakeHttpClient; -use workspace::Workspace; +use call::Room; +use gpui::{ModelHandle, TestAppContext}; mod channel_buffer_tests; mod channel_tests; mod integration_tests; -mod randomized_integration_tests; +mod random_channel_buffer_tests; +mod random_project_collaboration_tests; +mod randomized_test_helpers; +mod test_server; -struct TestServer { - app_state: Arc, - server: Arc, - connection_killers: Arc>>>, - forbid_connections: Arc, - _test_db: TestDb, - test_live_kit_server: Arc, -} - -impl TestServer { - async fn start(deterministic: &Arc) -> Self { - static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0); - - let use_postgres = env::var("USE_POSTGRES").ok(); - let use_postgres = use_postgres.as_deref(); - let test_db = if use_postgres == Some("true") || use_postgres == Some("1") { - TestDb::postgres(deterministic.build_background()) - } else { - TestDb::sqlite(deterministic.build_background()) - }; - let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst); - let live_kit_server = live_kit_client::TestServer::create( - format!("http://livekit.{}.test", live_kit_server_id), - format!("devkey-{}", live_kit_server_id), - format!("secret-{}", live_kit_server_id), - deterministic.build_background(), - ) - .unwrap(); - let app_state = Self::build_app_state(&test_db, &live_kit_server).await; - let epoch = app_state - .db - .create_server(&app_state.config.zed_environment) - .await - .unwrap(); - let server = Server::new( - epoch, - app_state.clone(), - Executor::Deterministic(deterministic.build_background()), - ); - server.start().await.unwrap(); - // Advance clock to ensure the server's cleanup task is finished. - deterministic.advance_clock(CLEANUP_TIMEOUT); - Self { - app_state, - server, - connection_killers: Default::default(), - forbid_connections: Default::default(), - _test_db: test_db, - test_live_kit_server: live_kit_server, - } - } - - async fn reset(&self) { - self.app_state.db.reset(); - let epoch = self - .app_state - .db - .create_server(&self.app_state.config.zed_environment) - .await - .unwrap(); - self.server.reset(epoch); - } - - async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { - cx.update(|cx| { - if cx.has_global::() { - panic!("Same cx used to create two test clients") - } - cx.set_global(SettingsStore::test(cx)); - }); - - let http = FakeHttpClient::with_404_response(); - let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await - { - user.id - } else { - self.app_state - .db - .create_user( - &format!("{name}@example.com"), - false, - NewUserParams { - github_login: name.into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .expect("creating user failed") - .user_id - }; - let client_name = name.to_string(); - let mut client = cx.read(|cx| Client::new(http.clone(), cx)); - let server = self.server.clone(); - let db = self.app_state.db.clone(); - let connection_killers = self.connection_killers.clone(); - let forbid_connections = self.forbid_connections.clone(); - - Arc::get_mut(&mut client) - .unwrap() - .set_id(user_id.0 as usize) - .override_authenticate(move |cx| { - cx.spawn(|_| async move { - let access_token = "the-token".to_string(); - Ok(Credentials { - user_id: user_id.0 as u64, - access_token, - }) - }) - }) - .override_establish_connection(move |credentials, cx| { - assert_eq!(credentials.user_id, user_id.0 as u64); - assert_eq!(credentials.access_token, "the-token"); - - let server = server.clone(); - let db = db.clone(); - let connection_killers = connection_killers.clone(); - let forbid_connections = forbid_connections.clone(); - let client_name = client_name.clone(); - cx.spawn(move |cx| async move { - if forbid_connections.load(SeqCst) { - Err(EstablishConnectionError::other(anyhow!( - "server is forbidding connections" - ))) - } else { - let (client_conn, server_conn, killed) = - Connection::in_memory(cx.background()); - let (connection_id_tx, connection_id_rx) = oneshot::channel(); - let user = db - .get_user_by_id(user_id) - .await - .expect("retrieving user failed") - .unwrap(); - cx.background() - .spawn(server.handle_connection( - server_conn, - client_name, - user, - Some(connection_id_tx), - Executor::Deterministic(cx.background()), - )) - .detach(); - let connection_id = connection_id_rx.await.unwrap(); - connection_killers - .lock() - .insert(connection_id.into(), killed); - Ok(client_conn) - } - }) - }); - - let fs = FakeFs::new(cx.background()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); - let app_state = Arc::new(workspace::AppState { - client: client.clone(), - user_store: user_store.clone(), - channel_store: channel_store.clone(), - languages: Arc::new(LanguageRegistry::test()), - fs: fs.clone(), - build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _, _| Task::ready(Ok(())), - background_actions: || &[], - }); - - cx.update(|cx| { - theme::init((), cx); - Project::init(&client, cx); - client::init(&client, cx); - language::init(cx); - editor::init_settings(cx); - workspace::init(app_state.clone(), cx); - audio::init((), cx); - call::init(client.clone(), user_store.clone(), cx); - channel::init(&client); - }); - - client - .authenticate_and_connect(false, &cx.to_async()) - .await - .unwrap(); - - let client = TestClient { - app_state, - username: name.to_string(), - state: Default::default(), - }; - client.wait_for_current_user(cx).await; - client - } - - fn disconnect_client(&self, peer_id: PeerId) { - self.connection_killers - .lock() - .remove(&peer_id) - .unwrap() - .store(true, SeqCst); - } - - fn forbid_connections(&self) { - self.forbid_connections.store(true, SeqCst); - } - - fn allow_connections(&self) { - self.forbid_connections.store(false, SeqCst); - } - - async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { - for ix in 1..clients.len() { - let (left, right) = clients.split_at_mut(ix); - let (client_a, cx_a) = left.last_mut().unwrap(); - for (client_b, cx_b) in right { - client_a - .app_state - .user_store - .update(*cx_a, |store, cx| { - store.request_contact(client_b.user_id().unwrap(), cx) - }) - .await - .unwrap(); - cx_a.foreground().run_until_parked(); - client_b - .app_state - .user_store - .update(*cx_b, |store, cx| { - store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) - }) - .await - .unwrap(); - } - } - } - - async fn make_channel( - &self, - channel: &str, - admin: (&TestClient, &mut TestAppContext), - members: &mut [(&TestClient, &mut TestAppContext)], - ) -> u64 { - let (admin_client, admin_cx) = admin; - let channel_id = admin_client - .app_state - .channel_store - .update(admin_cx, |channel_store, cx| { - channel_store.create_channel(channel, None, cx) - }) - .await - .unwrap(); - - for (member_client, member_cx) in members { - admin_client - .app_state - .channel_store - .update(admin_cx, |channel_store, cx| { - channel_store.invite_member( - channel_id, - member_client.user_id().unwrap(), - false, - cx, - ) - }) - .await - .unwrap(); - - admin_cx.foreground().run_until_parked(); - - member_client - .app_state - .channel_store - .update(*member_cx, |channels, _| { - channels.respond_to_channel_invite(channel_id, true) - }) - .await - .unwrap(); - } - - channel_id - } - - async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { - self.make_contacts(clients).await; - - let (left, right) = clients.split_at_mut(1); - let (_client_a, cx_a) = &mut left[0]; - let active_call_a = cx_a.read(ActiveCall::global); - - for (client_b, cx_b) in right { - let user_id_b = client_b.current_user_id(*cx_b).to_proto(); - active_call_a - .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx)) - .await - .unwrap(); - - cx_b.foreground().run_until_parked(); - let active_call_b = cx_b.read(ActiveCall::global); - active_call_b - .update(*cx_b, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - } - } - - async fn build_app_state( - test_db: &TestDb, - fake_server: &live_kit_client::TestServer, - ) -> Arc { - Arc::new(AppState { - db: test_db.db().clone(), - live_kit_client: Some(Arc::new(fake_server.create_api_client())), - config: Default::default(), - }) - } -} - -impl Deref for TestServer { - type Target = Server; - - fn deref(&self) -> &Self::Target { - &self.server - } -} - -impl Drop for TestServer { - fn drop(&mut self) { - self.server.teardown(); - self.test_live_kit_server.teardown().unwrap(); - } -} - -struct TestClient { - username: String, - state: RefCell, - app_state: Arc, -} - -#[derive(Default)] -struct TestClientState { - local_projects: Vec>, - remote_projects: Vec>, - buffers: HashMap, HashSet>>, -} - -impl Deref for TestClient { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.app_state.client - } -} - -struct ContactsSummary { - pub current: Vec, - pub outgoing_requests: Vec, - pub incoming_requests: Vec, -} - -impl TestClient { - pub fn fs(&self) -> &FakeFs { - self.app_state.fs.as_fake() - } - - pub fn channel_store(&self) -> &ModelHandle { - &self.app_state.channel_store - } - - pub fn user_store(&self) -> &ModelHandle { - &self.app_state.user_store - } - - pub fn language_registry(&self) -> &Arc { - &self.app_state.languages - } - - pub fn client(&self) -> &Arc { - &self.app_state.client - } - - pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { - UserId::from_proto( - self.app_state - .user_store - .read_with(cx, |user_store, _| user_store.current_user().unwrap().id), - ) - } - - async fn wait_for_current_user(&self, cx: &TestAppContext) { - let mut authed_user = self - .app_state - .user_store - .read_with(cx, |user_store, _| user_store.watch_current_user()); - while authed_user.next().await.unwrap().is_none() {} - } - - async fn clear_contacts(&self, cx: &mut TestAppContext) { - self.app_state - .user_store - .update(cx, |store, _| store.clear_contacts()) - .await; - } - - fn local_projects<'a>(&'a self) -> impl Deref>> + 'a { - Ref::map(self.state.borrow(), |state| &state.local_projects) - } - - fn remote_projects<'a>(&'a self) -> impl Deref>> + 'a { - Ref::map(self.state.borrow(), |state| &state.remote_projects) - } - - fn local_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { - RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects) - } - - fn remote_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { - RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects) - } - - fn buffers_for_project<'a>( - &'a self, - project: &ModelHandle, - ) -> impl DerefMut>> + 'a { - RefMut::map(self.state.borrow_mut(), |state| { - state.buffers.entry(project.clone()).or_default() - }) - } - - fn buffers<'a>( - &'a self, - ) -> impl DerefMut, HashSet>>> + 'a - { - RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers) - } - - fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { - self.app_state - .user_store - .read_with(cx, |store, _| ContactsSummary { - current: store - .contacts() - .iter() - .map(|contact| contact.user.github_login.clone()) - .collect(), - outgoing_requests: store - .outgoing_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - incoming_requests: store - .incoming_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - }) - } - - async fn build_local_project( - &self, - root_path: impl AsRef, - cx: &mut TestAppContext, - ) -> (ModelHandle, WorktreeId) { - let project = cx.update(|cx| { - Project::local( - self.client().clone(), - self.app_state.user_store.clone(), - self.app_state.languages.clone(), - self.app_state.fs.clone(), - cx, - ) - }); - let (worktree, _) = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree(root_path, true, cx) - }) - .await - .unwrap(); - worktree - .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - (project, worktree.read_with(cx, |tree, _| tree.id())) - } - - async fn build_remote_project( - &self, - host_project_id: u64, - guest_cx: &mut TestAppContext, - ) -> ModelHandle { - let active_call = guest_cx.read(ActiveCall::global); - let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone()); - room.update(guest_cx, |room, cx| { - room.join_project( - host_project_id, - self.app_state.languages.clone(), - self.app_state.fs.clone(), - cx, - ) - }) - .await - .unwrap() - } - - fn build_workspace( - &self, - project: &ModelHandle, - cx: &mut TestAppContext, - ) -> WindowHandle { - cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) - } -} - -impl Drop for TestClient { - fn drop(&mut self) { - self.app_state.client.teardown(); - } -} +pub use randomized_test_helpers::{ + run_randomized_test, save_randomized_test_plan, RandomizedTest, TestError, UserTestPlan, +}; +pub use test_server::{TestClient, TestServer}; #[derive(Debug, Eq, PartialEq)] struct RoomParticipants { diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs new file mode 100644 index 0000000000..929e567977 --- /dev/null +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -0,0 +1,49 @@ +use crate::tests::{run_randomized_test, RandomizedTest, TestClient, TestError, UserTestPlan}; +use anyhow::Result; +use async_trait::async_trait; +use gpui::{executor::Deterministic, TestAppContext}; +use rand::rngs::StdRng; +use serde_derive::{Deserialize, Serialize}; +use std::{rc::Rc, sync::Arc}; + +#[gpui::test] +async fn test_random_channel_buffers( + cx: &mut TestAppContext, + deterministic: Arc, + rng: StdRng, +) { + run_randomized_test::(cx, deterministic, rng).await; +} + +struct RandomChannelBufferTest; + +#[derive(Clone, Serialize, Deserialize)] +enum ChannelBufferOperation { + Join, +} + +#[async_trait(?Send)] +impl RandomizedTest for RandomChannelBufferTest { + type Operation = ChannelBufferOperation; + + fn generate_operation( + client: &TestClient, + rng: &mut StdRng, + plan: &mut UserTestPlan, + cx: &TestAppContext, + ) -> ChannelBufferOperation { + ChannelBufferOperation::Join + } + + async fn apply_operation( + client: &TestClient, + operation: ChannelBufferOperation, + cx: &mut TestAppContext, + ) -> Result<(), TestError> { + Ok(()) + } + + async fn on_client_added(client: &Rc) {} + + fn on_clients_quiesced(clients: &[(Rc, TestAppContext)]) {} +} diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs new file mode 100644 index 0000000000..242cfbc162 --- /dev/null +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -0,0 +1,1573 @@ +use crate::{ + db::UserId, + tests::{run_randomized_test, RandomizedTest, TestClient, TestError, UserTestPlan}, +}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use call::ActiveCall; +use collections::{BTreeMap, HashMap}; +use editor::Bias; +use fs::{repository::GitFileStatus, FakeFs, Fs as _}; +use futures::StreamExt; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; +use lsp::FakeLanguageServer; +use pretty_assertions::assert_eq; +use project::{search::SearchQuery, Project, ProjectPath}; +use rand::{ + distributions::{Alphanumeric, DistString}, + prelude::*, +}; +use serde::{Deserialize, Serialize}; +use std::{ + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +use util::ResultExt; + +#[gpui::test( + iterations = 100, + on_failure = "crate::tests::save_randomized_test_plan" +)] +async fn test_random_project_collaboration( + cx: &mut TestAppContext, + deterministic: Arc, + rng: StdRng, +) { + run_randomized_test::(cx, deterministic, rng).await; +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum ClientOperation { + AcceptIncomingCall, + RejectIncomingCall, + LeaveCall, + InviteContactToCall { + user_id: UserId, + }, + OpenLocalProject { + first_root_name: String, + }, + OpenRemoteProject { + host_id: UserId, + first_root_name: String, + }, + AddWorktreeToProject { + project_root_name: String, + new_root_path: PathBuf, + }, + CloseRemoteProject { + project_root_name: String, + }, + OpenBuffer { + project_root_name: String, + is_local: bool, + full_path: PathBuf, + }, + SearchProject { + project_root_name: String, + is_local: bool, + query: String, + detach: bool, + }, + EditBuffer { + project_root_name: String, + is_local: bool, + full_path: PathBuf, + edits: Vec<(Range, Arc)>, + }, + CloseBuffer { + project_root_name: String, + is_local: bool, + full_path: PathBuf, + }, + SaveBuffer { + project_root_name: String, + is_local: bool, + full_path: PathBuf, + detach: bool, + }, + RequestLspDataInBuffer { + project_root_name: String, + is_local: bool, + full_path: PathBuf, + offset: usize, + kind: LspRequestKind, + detach: bool, + }, + CreateWorktreeEntry { + project_root_name: String, + is_local: bool, + full_path: PathBuf, + is_dir: bool, + }, + WriteFsEntry { + path: PathBuf, + is_dir: bool, + content: String, + }, + GitOperation { + operation: GitOperation, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum GitOperation { + WriteGitIndex { + repo_path: PathBuf, + contents: Vec<(PathBuf, String)>, + }, + WriteGitBranch { + repo_path: PathBuf, + new_branch: Option, + }, + WriteGitStatuses { + repo_path: PathBuf, + statuses: Vec<(PathBuf, GitFileStatus)>, + git_operation: bool, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum LspRequestKind { + Rename, + Completion, + CodeAction, + Definition, + Highlights, +} + +struct ProjectCollaborationTest; + +#[async_trait(?Send)] +impl RandomizedTest for ProjectCollaborationTest { + type Operation = ClientOperation; + + fn generate_operation( + client: &TestClient, + rng: &mut StdRng, + plan: &mut UserTestPlan, + cx: &TestAppContext, + ) -> ClientOperation { + let call = cx.read(ActiveCall::global); + loop { + match rng.gen_range(0..100_u32) { + // Mutate the call + 0..=29 => { + // Respond to an incoming call + if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) { + break if rng.gen_bool(0.7) { + ClientOperation::AcceptIncomingCall + } else { + ClientOperation::RejectIncomingCall + }; + } + + match rng.gen_range(0..100_u32) { + // Invite a contact to the current call + 0..=70 => { + let available_contacts = + client.user_store().read_with(cx, |user_store, _| { + user_store + .contacts() + .iter() + .filter(|contact| contact.online && !contact.busy) + .cloned() + .collect::>() + }); + if !available_contacts.is_empty() { + let contact = available_contacts.choose(rng).unwrap(); + break ClientOperation::InviteContactToCall { + user_id: UserId(contact.user.id as i32), + }; + } + } + + // Leave the current call + 71.. => { + if plan.allow_client_disconnection + && call.read_with(cx, |call, _| call.room().is_some()) + { + break ClientOperation::LeaveCall; + } + } + } + } + + // Mutate projects + 30..=59 => match rng.gen_range(0..100_u32) { + // Open a new project + 0..=70 => { + // Open a remote project + if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) { + let existing_remote_project_ids = cx.read(|cx| { + client + .remote_projects() + .iter() + .map(|p| p.read(cx).remote_id().unwrap()) + .collect::>() + }); + let new_remote_projects = room.read_with(cx, |room, _| { + room.remote_participants() + .values() + .flat_map(|participant| { + participant.projects.iter().filter_map(|project| { + if existing_remote_project_ids.contains(&project.id) { + None + } else { + Some(( + UserId::from_proto(participant.user.id), + project.worktree_root_names[0].clone(), + )) + } + }) + }) + .collect::>() + }); + if !new_remote_projects.is_empty() { + let (host_id, first_root_name) = + new_remote_projects.choose(rng).unwrap().clone(); + break ClientOperation::OpenRemoteProject { + host_id, + first_root_name, + }; + } + } + // Open a local project + else { + let first_root_name = plan.next_root_dir_name(); + break ClientOperation::OpenLocalProject { first_root_name }; + } + } + + // Close a remote project + 71..=80 => { + if !client.remote_projects().is_empty() { + let project = client.remote_projects().choose(rng).unwrap().clone(); + let first_root_name = root_name_for_project(&project, cx); + break ClientOperation::CloseRemoteProject { + project_root_name: first_root_name, + }; + } + } + + // Mutate project worktrees + 81.. => match rng.gen_range(0..100_u32) { + // Add a worktree to a local project + 0..=50 => { + let Some(project) = client.local_projects().choose(rng).cloned() else { + continue; + }; + let project_root_name = root_name_for_project(&project, cx); + let mut paths = client.fs().paths(false); + paths.remove(0); + let new_root_path = if paths.is_empty() || rng.gen() { + Path::new("/").join(&plan.next_root_dir_name()) + } else { + paths.choose(rng).unwrap().clone() + }; + break ClientOperation::AddWorktreeToProject { + project_root_name, + new_root_path, + }; + } + + // Add an entry to a worktree + _ => { + let Some(project) = choose_random_project(client, rng) else { + continue; + }; + let project_root_name = root_name_for_project(&project, cx); + let is_local = project.read_with(cx, |project, _| project.is_local()); + let worktree = project.read_with(cx, |project, cx| { + project + .worktrees(cx) + .filter(|worktree| { + let worktree = worktree.read(cx); + worktree.is_visible() + && worktree.entries(false).any(|e| e.is_file()) + && worktree.root_entry().map_or(false, |e| e.is_dir()) + }) + .choose(rng) + }); + let Some(worktree) = worktree else { continue }; + let is_dir = rng.gen::(); + let mut full_path = + worktree.read_with(cx, |w, _| PathBuf::from(w.root_name())); + full_path.push(gen_file_name(rng)); + if !is_dir { + full_path.set_extension("rs"); + } + break ClientOperation::CreateWorktreeEntry { + project_root_name, + is_local, + full_path, + is_dir, + }; + } + }, + }, + + // Query and mutate buffers + 60..=90 => { + let Some(project) = choose_random_project(client, rng) else { + continue; + }; + let project_root_name = root_name_for_project(&project, cx); + let is_local = project.read_with(cx, |project, _| project.is_local()); + + match rng.gen_range(0..100_u32) { + // Manipulate an existing buffer + 0..=70 => { + let Some(buffer) = client + .buffers_for_project(&project) + .iter() + .choose(rng) + .cloned() + else { + continue; + }; + + let full_path = buffer + .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); + + match rng.gen_range(0..100_u32) { + // Close the buffer + 0..=15 => { + break ClientOperation::CloseBuffer { + project_root_name, + is_local, + full_path, + }; + } + // Save the buffer + 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => { + let detach = rng.gen_bool(0.3); + break ClientOperation::SaveBuffer { + project_root_name, + is_local, + full_path, + detach, + }; + } + // Edit the buffer + 30..=69 => { + let edits = buffer + .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); + break ClientOperation::EditBuffer { + project_root_name, + is_local, + full_path, + edits, + }; + } + // Make an LSP request + _ => { + let offset = buffer.read_with(cx, |buffer, _| { + buffer.clip_offset( + rng.gen_range(0..=buffer.len()), + language::Bias::Left, + ) + }); + let detach = rng.gen(); + break ClientOperation::RequestLspDataInBuffer { + project_root_name, + full_path, + offset, + is_local, + kind: match rng.gen_range(0..5_u32) { + 0 => LspRequestKind::Rename, + 1 => LspRequestKind::Highlights, + 2 => LspRequestKind::Definition, + 3 => LspRequestKind::CodeAction, + 4.. => LspRequestKind::Completion, + }, + detach, + }; + } + } + } + + 71..=80 => { + let query = rng.gen_range('a'..='z').to_string(); + let detach = rng.gen_bool(0.3); + break ClientOperation::SearchProject { + project_root_name, + is_local, + query, + detach, + }; + } + + // Open a buffer + 81.. => { + let worktree = project.read_with(cx, |project, cx| { + project + .worktrees(cx) + .filter(|worktree| { + let worktree = worktree.read(cx); + worktree.is_visible() + && worktree.entries(false).any(|e| e.is_file()) + }) + .choose(rng) + }); + let Some(worktree) = worktree else { continue }; + let full_path = worktree.read_with(cx, |worktree, _| { + let entry = worktree + .entries(false) + .filter(|e| e.is_file()) + .choose(rng) + .unwrap(); + if entry.path.as_ref() == Path::new("") { + Path::new(worktree.root_name()).into() + } else { + Path::new(worktree.root_name()).join(&entry.path) + } + }); + break ClientOperation::OpenBuffer { + project_root_name, + is_local, + full_path, + }; + } + } + } + + // Update a git related action + 91..=95 => { + break ClientOperation::GitOperation { + operation: generate_git_operation(rng, client), + }; + } + + // Create or update a file or directory + 96.. => { + let is_dir = rng.gen::(); + let content; + let mut path; + let dir_paths = client.fs().directories(false); + + if is_dir { + content = String::new(); + path = dir_paths.choose(rng).unwrap().clone(); + path.push(gen_file_name(rng)); + } else { + content = Alphanumeric.sample_string(rng, 16); + + // Create a new file or overwrite an existing file + let file_paths = client.fs().files(); + if file_paths.is_empty() || rng.gen_bool(0.5) { + path = dir_paths.choose(rng).unwrap().clone(); + path.push(gen_file_name(rng)); + path.set_extension("rs"); + } else { + path = file_paths.choose(rng).unwrap().clone() + }; + } + break ClientOperation::WriteFsEntry { + path, + is_dir, + content, + }; + } + } + } + } + + async fn apply_operation( + client: &TestClient, + operation: ClientOperation, + cx: &mut TestAppContext, + ) -> Result<(), TestError> { + match operation { + ClientOperation::AcceptIncomingCall => { + let active_call = cx.read(ActiveCall::global); + if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) { + Err(TestError::Inapplicable)?; + } + + log::info!("{}: accepting incoming call", client.username); + active_call + .update(cx, |call, cx| call.accept_incoming(cx)) + .await?; + } + + ClientOperation::RejectIncomingCall => { + let active_call = cx.read(ActiveCall::global); + if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) { + Err(TestError::Inapplicable)?; + } + + log::info!("{}: declining incoming call", client.username); + active_call.update(cx, |call, cx| call.decline_incoming(cx))?; + } + + ClientOperation::LeaveCall => { + let active_call = cx.read(ActiveCall::global); + if active_call.read_with(cx, |call, _| call.room().is_none()) { + Err(TestError::Inapplicable)?; + } + + log::info!("{}: hanging up", client.username); + active_call.update(cx, |call, cx| call.hang_up(cx)).await?; + } + + ClientOperation::InviteContactToCall { user_id } => { + let active_call = cx.read(ActiveCall::global); + + log::info!("{}: inviting {}", client.username, user_id,); + active_call + .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx)) + .await + .log_err(); + } + + ClientOperation::OpenLocalProject { first_root_name } => { + log::info!( + "{}: opening local project at {:?}", + client.username, + first_root_name + ); + + let root_path = Path::new("/").join(&first_root_name); + client.fs().create_dir(&root_path).await.unwrap(); + client + .fs() + .create_file(&root_path.join("main.rs"), Default::default()) + .await + .unwrap(); + let project = client.build_local_project(root_path, cx).await.0; + ensure_project_shared(&project, client, cx).await; + client.local_projects_mut().push(project.clone()); + } + + ClientOperation::AddWorktreeToProject { + project_root_name, + new_root_path, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: finding/creating local worktree at {:?} to project with root path {}", + client.username, + new_root_path, + project_root_name + ); + + ensure_project_shared(&project, client, cx).await; + if !client.fs().paths(false).contains(&new_root_path) { + client.fs().create_dir(&new_root_path).await.unwrap(); + } + project + .update(cx, |project, cx| { + project.find_or_create_local_worktree(&new_root_path, true, cx) + }) + .await + .unwrap(); + } + + ClientOperation::CloseRemoteProject { project_root_name } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: closing remote project with root path {}", + client.username, + project_root_name, + ); + + let ix = client + .remote_projects() + .iter() + .position(|p| p == &project) + .unwrap(); + cx.update(|_| { + client.remote_projects_mut().remove(ix); + client.buffers().retain(|p, _| *p != project); + drop(project); + }); + } + + ClientOperation::OpenRemoteProject { + host_id, + first_root_name, + } => { + let active_call = cx.read(ActiveCall::global); + let project = active_call + .update(cx, |call, cx| { + let room = call.room().cloned()?; + let participant = room + .read(cx) + .remote_participants() + .get(&host_id.to_proto())?; + let project_id = participant + .projects + .iter() + .find(|project| project.worktree_root_names[0] == first_root_name)? + .id; + Some(room.update(cx, |room, cx| { + room.join_project( + project_id, + client.language_registry().clone(), + FakeFs::new(cx.background().clone()), + cx, + ) + })) + }) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: joining remote project of user {}, root name {}", + client.username, + host_id, + first_root_name, + ); + + let project = project.await?; + client.remote_projects_mut().push(project.clone()); + } + + ClientOperation::CreateWorktreeEntry { + project_root_name, + is_local, + full_path, + is_dir, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + let project_path = project_path_for_full_path(&project, &full_path, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: creating {} at path {:?} in {} project {}", + client.username, + if is_dir { "dir" } else { "file" }, + full_path, + if is_local { "local" } else { "remote" }, + project_root_name, + ); + + ensure_project_shared(&project, client, cx).await; + project + .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) + .unwrap() + .await?; + } + + ClientOperation::OpenBuffer { + project_root_name, + is_local, + full_path, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + let project_path = project_path_for_full_path(&project, &full_path, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: opening buffer {:?} in {} project {}", + client.username, + full_path, + if is_local { "local" } else { "remote" }, + project_root_name, + ); + + ensure_project_shared(&project, client, cx).await; + let buffer = project + .update(cx, |project, cx| project.open_buffer(project_path, cx)) + .await?; + client.buffers_for_project(&project).insert(buffer); + } + + ClientOperation::EditBuffer { + project_root_name, + is_local, + full_path, + edits, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + let buffer = buffer_for_full_path(client, &project, &full_path, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: editing buffer {:?} in {} project {} with {:?}", + client.username, + full_path, + if is_local { "local" } else { "remote" }, + project_root_name, + edits + ); + + ensure_project_shared(&project, client, cx).await; + buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(); + buffer.edit( + edits.into_iter().map(|(range, text)| { + let start = snapshot.clip_offset(range.start, Bias::Left); + let end = snapshot.clip_offset(range.end, Bias::Right); + (start..end, text) + }), + None, + cx, + ); + }); + } + + ClientOperation::CloseBuffer { + project_root_name, + is_local, + full_path, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + let buffer = buffer_for_full_path(client, &project, &full_path, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: closing buffer {:?} in {} project {}", + client.username, + full_path, + if is_local { "local" } else { "remote" }, + project_root_name + ); + + ensure_project_shared(&project, client, cx).await; + cx.update(|_| { + client.buffers_for_project(&project).remove(&buffer); + drop(buffer); + }); + } + + ClientOperation::SaveBuffer { + project_root_name, + is_local, + full_path, + detach, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + let buffer = buffer_for_full_path(client, &project, &full_path, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: saving buffer {:?} in {} project {}, {}", + client.username, + full_path, + if is_local { "local" } else { "remote" }, + project_root_name, + if detach { "detaching" } else { "awaiting" } + ); + + ensure_project_shared(&project, client, cx).await; + let requested_version = buffer.read_with(cx, |buffer, _| buffer.version()); + let save = + project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); + let save = cx.spawn(|cx| async move { + save.await + .map_err(|err| anyhow!("save request failed: {:?}", err))?; + assert!(buffer + .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() }) + .observed_all(&requested_version)); + anyhow::Ok(()) + }); + if detach { + cx.update(|cx| save.detach_and_log_err(cx)); + } else { + save.await?; + } + } + + ClientOperation::RequestLspDataInBuffer { + project_root_name, + is_local, + full_path, + offset, + kind, + detach, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + let buffer = buffer_for_full_path(client, &project, &full_path, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: request LSP {:?} for buffer {:?} in {} project {}, {}", + client.username, + kind, + full_path, + if is_local { "local" } else { "remote" }, + project_root_name, + if detach { "detaching" } else { "awaiting" } + ); + + use futures::{FutureExt as _, TryFutureExt as _}; + let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left)); + let request = cx.foreground().spawn(project.update(cx, |project, cx| { + match kind { + LspRequestKind::Rename => project + .prepare_rename(buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::Completion => project + .completions(&buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::CodeAction => project + .code_actions(&buffer, offset..offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::Definition => project + .definition(&buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::Highlights => project + .document_highlights(&buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + } + })); + if detach { + request.detach(); + } else { + request.await?; + } + } + + ClientOperation::SearchProject { + project_root_name, + is_local, + query, + detach, + } => { + let project = project_for_root_name(client, &project_root_name, cx) + .ok_or(TestError::Inapplicable)?; + + log::info!( + "{}: search {} project {} for {:?}, {}", + client.username, + if is_local { "local" } else { "remote" }, + project_root_name, + query, + if detach { "detaching" } else { "awaiting" } + ); + + let mut search = project.update(cx, |project, cx| { + project.search( + SearchQuery::text(query, false, false, Vec::new(), Vec::new()), + cx, + ) + }); + drop(project); + let search = cx.background().spawn(async move { + let mut results = HashMap::default(); + while let Some((buffer, ranges)) = search.next().await { + results.entry(buffer).or_insert(ranges); + } + results + }); + search.await; + } + + ClientOperation::WriteFsEntry { + path, + is_dir, + content, + } => { + if !client + .fs() + .directories(false) + .contains(&path.parent().unwrap().to_owned()) + { + return Err(TestError::Inapplicable); + } + + if is_dir { + log::info!("{}: creating dir at {:?}", client.username, path); + client.fs().create_dir(&path).await.unwrap(); + } else { + let exists = client.fs().metadata(&path).await?.is_some(); + let verb = if exists { "updating" } else { "creating" }; + log::info!("{}: {} file at {:?}", verb, client.username, path); + + client + .fs() + .save(&path, &content.as_str().into(), text::LineEnding::Unix) + .await + .unwrap(); + } + } + + ClientOperation::GitOperation { operation } => match operation { + GitOperation::WriteGitIndex { + repo_path, + contents, + } => { + if !client.fs().directories(false).contains(&repo_path) { + return Err(TestError::Inapplicable); + } + + for (path, _) in contents.iter() { + if !client.fs().files().contains(&repo_path.join(path)) { + return Err(TestError::Inapplicable); + } + } + + log::info!( + "{}: writing git index for repo {:?}: {:?}", + client.username, + repo_path, + contents + ); + + let dot_git_dir = repo_path.join(".git"); + let contents = contents + .iter() + .map(|(path, contents)| (path.as_path(), contents.clone())) + .collect::>(); + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; + } + client.fs().set_index_for_repo(&dot_git_dir, &contents); + } + GitOperation::WriteGitBranch { + repo_path, + new_branch, + } => { + if !client.fs().directories(false).contains(&repo_path) { + return Err(TestError::Inapplicable); + } + + log::info!( + "{}: writing git branch for repo {:?}: {:?}", + client.username, + repo_path, + new_branch + ); + + let dot_git_dir = repo_path.join(".git"); + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; + } + client + .fs() + .set_branch_name(&dot_git_dir, new_branch.clone()); + } + GitOperation::WriteGitStatuses { + repo_path, + statuses, + git_operation, + } => { + if !client.fs().directories(false).contains(&repo_path) { + return Err(TestError::Inapplicable); + } + for (path, _) in statuses.iter() { + if !client.fs().files().contains(&repo_path.join(path)) { + return Err(TestError::Inapplicable); + } + } + + log::info!( + "{}: writing git statuses for repo {:?}: {:?}", + client.username, + repo_path, + statuses + ); + + let dot_git_dir = repo_path.join(".git"); + + let statuses = statuses + .iter() + .map(|(path, val)| (path.as_path(), val.clone())) + .collect::>(); + + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; + } + + if git_operation { + client.fs().set_status_for_repo_via_git_operation( + &dot_git_dir, + statuses.as_slice(), + ); + } else { + client.fs().set_status_for_repo_via_working_copy_change( + &dot_git_dir, + statuses.as_slice(), + ); + } + } + }, + } + Ok(()) + } + + async fn on_client_added(client: &Rc) { + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-fake-language-server", + capabilities: lsp::LanguageServer::full_capabilities(), + initializer: Some(Box::new({ + let fs = client.app_state.fs.clone(); + move |fake_server: &mut FakeLanguageServer| { + fake_server.handle_request::( + |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit( + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "the-new-text".to_string(), + }, + )), + ..Default::default() + }, + ]))) + }, + ); + + fake_server.handle_request::( + |_, _| async move { + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "the-code-action".to_string(), + ..Default::default() + }, + )])) + }, + ); + + fake_server.handle_request::( + |params, _| async move { + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + params.position, + params.position, + )))) + }, + ); + + fake_server.handle_request::({ + let fs = fs.clone(); + move |_, cx| { + let background = cx.background(); + let mut rng = background.rng(); + let count = rng.gen_range::(1..3); + let files = fs.as_fake().files(); + let files = (0..count) + .map(|_| files.choose(&mut *rng).unwrap().clone()) + .collect::>(); + async move { + log::info!("LSP: Returning definitions in files {:?}", &files); + Ok(Some(lsp::GotoDefinitionResponse::Array( + files + .into_iter() + .map(|file| lsp::Location { + uri: lsp::Url::from_file_path(file).unwrap(), + range: Default::default(), + }) + .collect(), + ))) + } + } + }); + + fake_server.handle_request::( + move |_, cx| { + let mut highlights = Vec::new(); + let background = cx.background(); + let mut rng = background.rng(); + + let highlight_count = rng.gen_range(1..=5); + for _ in 0..highlight_count { + let start_row = rng.gen_range(0..100); + let start_column = rng.gen_range(0..100); + let end_row = rng.gen_range(0..100); + let end_column = rng.gen_range(0..100); + let start = PointUtf16::new(start_row, start_column); + let end = PointUtf16::new(end_row, end_column); + let range = if start > end { end..start } else { start..end }; + highlights.push(lsp::DocumentHighlight { + range: range_to_lsp(range.clone()), + kind: Some(lsp::DocumentHighlightKind::READ), + }); + } + highlights.sort_unstable_by_key(|highlight| { + (highlight.range.start, highlight.range.end) + }); + async move { Ok(Some(highlights)) } + }, + ); + } + })), + ..Default::default() + })) + .await; + client.app_state.languages.add(Arc::new(language)); + } + + fn on_clients_quiesced(clients: &[(Rc, TestAppContext)]) { + for (client, client_cx) in clients { + for guest_project in client.remote_projects().iter() { + guest_project.read_with(client_cx, |guest_project, cx| { + let host_project = clients.iter().find_map(|(client, cx)| { + let project = client + .local_projects() + .iter() + .find(|host_project| { + host_project.read_with(cx, |host_project, _| { + host_project.remote_id() == guest_project.remote_id() + }) + })? + .clone(); + Some((project, cx)) + }); + + if !guest_project.is_read_only() { + if let Some((host_project, host_cx)) = host_project { + let host_worktree_snapshots = + host_project.read_with(host_cx, |host_project, cx| { + host_project + .worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + (worktree.id(), worktree.snapshot()) + }) + .collect::>() + }); + let guest_worktree_snapshots = guest_project + .worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + (worktree.id(), worktree.snapshot()) + }) + .collect::>(); + + assert_eq!( + guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::>(), + host_worktree_snapshots.values().map(|w| w.abs_path()).collect::>(), + "{} has different worktrees than the host for project {:?}", + client.username, guest_project.remote_id(), + ); + + for (id, host_snapshot) in &host_worktree_snapshots { + let guest_snapshot = &guest_worktree_snapshots[id]; + assert_eq!( + guest_snapshot.root_name(), + host_snapshot.root_name(), + "{} has different root name than the host for worktree {}, project {:?}", + client.username, + id, + guest_project.remote_id(), + ); + assert_eq!( + guest_snapshot.abs_path(), + host_snapshot.abs_path(), + "{} has different abs path than the host for worktree {}, project: {:?}", + client.username, + id, + guest_project.remote_id(), + ); + assert_eq!( + guest_snapshot.entries(false).collect::>(), + host_snapshot.entries(false).collect::>(), + "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}", + client.username, + host_snapshot.abs_path(), + id, + guest_project.remote_id(), + ); + assert_eq!(guest_snapshot.repositories().collect::>(), host_snapshot.repositories().collect::>(), + "{} has different repositories than the host for worktree {:?} and project {:?}", + client.username, + host_snapshot.abs_path(), + guest_project.remote_id(), + ); + assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(), + "{} has different scan id than the host for worktree {:?} and project {:?}", + client.username, + host_snapshot.abs_path(), + guest_project.remote_id(), + ); + } + } + } + + for buffer in guest_project.opened_buffers(cx) { + let buffer = buffer.read(cx); + assert_eq!( + buffer.deferred_ops_len(), + 0, + "{} has deferred operations for buffer {:?} in project {:?}", + client.username, + buffer.file().unwrap().full_path(cx), + guest_project.remote_id(), + ); + } + }); + } + + let buffers = client.buffers().clone(); + for (guest_project, guest_buffers) in &buffers { + let project_id = if guest_project.read_with(client_cx, |project, _| { + project.is_local() || project.is_read_only() + }) { + continue; + } else { + guest_project + .read_with(client_cx, |project, _| project.remote_id()) + .unwrap() + }; + let guest_user_id = client.user_id().unwrap(); + + let host_project = clients.iter().find_map(|(client, cx)| { + let project = client + .local_projects() + .iter() + .find(|host_project| { + host_project.read_with(cx, |host_project, _| { + host_project.remote_id() == Some(project_id) + }) + })? + .clone(); + Some((client.user_id().unwrap(), project, cx)) + }); + + let (host_user_id, host_project, host_cx) = + if let Some((host_user_id, host_project, host_cx)) = host_project { + (host_user_id, host_project, host_cx) + } else { + continue; + }; + + for guest_buffer in guest_buffers { + let buffer_id = + guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id()); + let host_buffer = host_project.read_with(host_cx, |project, cx| { + project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| { + panic!( + "host does not have buffer for guest:{}, peer:{:?}, id:{}", + client.username, + client.peer_id(), + buffer_id + ) + }) + }); + let path = host_buffer + .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); + + assert_eq!( + guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()), + 0, + "{}, buffer {}, path {:?} has deferred operations", + client.username, + buffer_id, + path, + ); + assert_eq!( + guest_buffer.read_with(client_cx, |buffer, _| buffer.text()), + host_buffer.read_with(host_cx, |buffer, _| buffer.text()), + "{}, buffer {}, path {:?}, differs from the host's buffer", + client.username, + buffer_id, + path + ); + + let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned()); + let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned()); + match (host_file, guest_file) { + (Some(host_file), Some(guest_file)) => { + assert_eq!(guest_file.path(), host_file.path()); + assert_eq!(guest_file.is_deleted(), host_file.is_deleted()); + assert_eq!( + guest_file.mtime(), + host_file.mtime(), + "guest {} mtime does not match host {} for path {:?} in project {}", + guest_user_id, + host_user_id, + guest_file.path(), + project_id, + ); + } + (None, None) => {} + (None, _) => panic!("host's file is None, guest's isn't"), + (_, None) => panic!("guest's file is None, hosts's isn't"), + } + + let host_diff_base = host_buffer + .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string)); + let guest_diff_base = guest_buffer + .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string)); + assert_eq!( + guest_diff_base, host_diff_base, + "guest {} diff base does not match host's for path {path:?} in project {project_id}", + client.username + ); + + let host_saved_version = + host_buffer.read_with(host_cx, |b, _| b.saved_version().clone()); + let guest_saved_version = + guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone()); + assert_eq!( + guest_saved_version, host_saved_version, + "guest {} saved version does not match host's for path {path:?} in project {project_id}", + client.username + ); + + let host_saved_version_fingerprint = + host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint()); + let guest_saved_version_fingerprint = + guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint()); + assert_eq!( + guest_saved_version_fingerprint, host_saved_version_fingerprint, + "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}", + client.username + ); + + let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime()); + let guest_saved_mtime = + guest_buffer.read_with(client_cx, |b, _| b.saved_mtime()); + assert_eq!( + guest_saved_mtime, host_saved_mtime, + "guest {} saved mtime does not match host's for path {path:?} in project {project_id}", + client.username + ); + + let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty()); + let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty()); + assert_eq!(guest_is_dirty, host_is_dirty, + "guest {} dirty status does not match host's for path {path:?} in project {project_id}", + client.username + ); + + let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict()); + let guest_has_conflict = + guest_buffer.read_with(client_cx, |b, _| b.has_conflict()); + assert_eq!(guest_has_conflict, host_has_conflict, + "guest {} conflict status does not match host's for path {path:?} in project {project_id}", + client.username + ); + } + } + } + } +} + +fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation { + fn generate_file_paths( + repo_path: &Path, + rng: &mut StdRng, + client: &TestClient, + ) -> Vec { + let mut paths = client + .fs() + .files() + .into_iter() + .filter(|path| path.starts_with(repo_path)) + .collect::>(); + + let count = rng.gen_range(0..=paths.len()); + paths.shuffle(rng); + paths.truncate(count); + + paths + .iter() + .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf()) + .collect::>() + } + + let repo_path = client.fs().directories(false).choose(rng).unwrap().clone(); + + match rng.gen_range(0..100_u32) { + 0..=25 => { + let file_paths = generate_file_paths(&repo_path, rng, client); + + let contents = file_paths + .into_iter() + .map(|path| (path, Alphanumeric.sample_string(rng, 16))) + .collect(); + + GitOperation::WriteGitIndex { + repo_path, + contents, + } + } + 26..=63 => { + let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8)); + + GitOperation::WriteGitBranch { + repo_path, + new_branch, + } + } + 64..=100 => { + let file_paths = generate_file_paths(&repo_path, rng, client); + + let statuses = file_paths + .into_iter() + .map(|paths| { + ( + paths, + match rng.gen_range(0..3_u32) { + 0 => GitFileStatus::Added, + 1 => GitFileStatus::Modified, + 2 => GitFileStatus::Conflict, + _ => unreachable!(), + }, + ) + }) + .collect::>(); + + let git_operation = rng.gen::(); + + GitOperation::WriteGitStatuses { + repo_path, + statuses, + git_operation, + } + } + _ => unreachable!(), + } +} + +fn buffer_for_full_path( + client: &TestClient, + project: &ModelHandle, + full_path: &PathBuf, + cx: &TestAppContext, +) -> Option> { + client + .buffers_for_project(project) + .iter() + .find(|buffer| { + buffer.read_with(cx, |buffer, cx| { + buffer.file().unwrap().full_path(cx) == *full_path + }) + }) + .cloned() +} + +fn project_for_root_name( + client: &TestClient, + root_name: &str, + cx: &TestAppContext, +) -> Option> { + if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) { + return Some(client.local_projects()[ix].clone()); + } + if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) { + return Some(client.remote_projects()[ix].clone()); + } + None +} + +fn project_ix_for_root_name( + projects: &[ModelHandle], + root_name: &str, + cx: &TestAppContext, +) -> Option { + projects.iter().position(|project| { + project.read_with(cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next().unwrap(); + worktree.read(cx).root_name() == root_name + }) + }) +} + +fn root_name_for_project(project: &ModelHandle, cx: &TestAppContext) -> String { + project.read_with(cx, |project, cx| { + project + .visible_worktrees(cx) + .next() + .unwrap() + .read(cx) + .root_name() + .to_string() + }) +} + +fn project_path_for_full_path( + project: &ModelHandle, + full_path: &Path, + cx: &TestAppContext, +) -> Option { + let mut components = full_path.components(); + let root_name = components.next().unwrap().as_os_str().to_str().unwrap(); + let path = components.as_path().into(); + let worktree_id = project.read_with(cx, |project, cx| { + project.worktrees(cx).find_map(|worktree| { + let worktree = worktree.read(cx); + if worktree.root_name() == root_name { + Some(worktree.id()) + } else { + None + } + }) + })?; + Some(ProjectPath { worktree_id, path }) +} + +async fn ensure_project_shared( + project: &ModelHandle, + client: &TestClient, + cx: &mut TestAppContext, +) { + let first_root_name = root_name_for_project(project, cx); + let active_call = cx.read(ActiveCall::global); + if active_call.read_with(cx, |call, _| call.room().is_some()) + && project.read_with(cx, |project, _| project.is_local() && !project.is_shared()) + { + match active_call + .update(cx, |call, cx| call.share_project(project.clone(), cx)) + .await + { + Ok(project_id) => { + log::info!( + "{}: shared project {} with id {}", + client.username, + first_root_name, + project_id + ); + } + Err(error) => { + log::error!( + "{}: error sharing project {}: {:?}", + client.username, + first_root_name, + error + ); + } + } + } +} + +fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option> { + client + .local_projects() + .iter() + .chain(client.remote_projects().iter()) + .choose(rng) + .cloned() +} + +fn gen_file_name(rng: &mut StdRng) -> String { + let mut name = String::new(); + for _ in 0..10 { + let letter = rng.gen_range('a'..='z'); + name.push(letter); + } + name +} diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs deleted file mode 100644 index 309fcf7e44..0000000000 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ /dev/null @@ -1,2199 +0,0 @@ -use crate::{ - db::{self, NewUserParams, UserId}, - rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::{TestClient, TestServer}, -}; -use anyhow::{anyhow, Result}; -use call::ActiveCall; -use client::RECEIVE_TIMEOUT; -use collections::{BTreeMap, HashMap}; -use editor::Bias; -use fs::{repository::GitFileStatus, FakeFs, Fs as _}; -use futures::StreamExt as _; -use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext}; -use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; -use lsp::FakeLanguageServer; -use parking_lot::Mutex; -use pretty_assertions::assert_eq; -use project::{search::SearchQuery, Project, ProjectPath}; -use rand::{ - distributions::{Alphanumeric, DistString}, - prelude::*, -}; -use serde::{Deserialize, Serialize}; -use settings::SettingsStore; -use std::{ - env, - ops::Range, - path::{Path, PathBuf}, - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, - Arc, - }, -}; -use util::ResultExt; - -lazy_static::lazy_static! { - static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); - static ref PLAN_SAVE_PATH: Option = path_env_var("SAVE_PLAN"); -} -static LOADED_PLAN_JSON: Mutex>> = Mutex::new(None); -static PLAN: Mutex>>> = Mutex::new(None); - -#[gpui::test(iterations = 100, on_failure = "on_failure")] -async fn test_random_collaboration( - cx: &mut TestAppContext, - deterministic: Arc, - rng: StdRng, -) { - deterministic.forbid_parking(); - - let max_peers = env::var("MAX_PEERS") - .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) - .unwrap_or(3); - let max_operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let mut server = TestServer::start(&deterministic).await; - let db = server.app_state.db.clone(); - - let mut users = Vec::new(); - for ix in 0..max_peers { - let username = format!("user-{}", ix + 1); - let user_id = db - .create_user( - &format!("{username}@example.com"), - false, - NewUserParams { - github_login: username.clone(), - github_user_id: (ix + 1) as i32, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - users.push(UserTestPlan { - user_id, - username, - online: false, - next_root_id: 0, - operation_ix: 0, - }); - } - - for (ix, user_a) in users.iter().enumerate() { - for user_b in &users[ix + 1..] { - server - .app_state - .db - .send_contact_request(user_a.user_id, user_b.user_id) - .await - .unwrap(); - server - .app_state - .db - .respond_to_contact_request(user_b.user_id, user_a.user_id, true) - .await - .unwrap(); - } - } - - let plan = Arc::new(Mutex::new(TestPlan::new(rng, users, max_operations))); - - if let Some(path) = &*PLAN_LOAD_PATH { - let json = LOADED_PLAN_JSON - .lock() - .get_or_insert_with(|| { - eprintln!("loaded test plan from path {:?}", path); - std::fs::read(path).unwrap() - }) - .clone(); - plan.lock().deserialize(json); - } - - PLAN.lock().replace(plan.clone()); - - let mut clients = Vec::new(); - let mut client_tasks = Vec::new(); - let mut operation_channels = Vec::new(); - - loop { - let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { - break; - }; - applied.store(true, SeqCst); - let did_apply = apply_server_operation( - deterministic.clone(), - &mut server, - &mut clients, - &mut client_tasks, - &mut operation_channels, - plan.clone(), - next_operation, - cx, - ) - .await; - if !did_apply { - applied.store(false, SeqCst); - } - } - - drop(operation_channels); - deterministic.start_waiting(); - futures::future::join_all(client_tasks).await; - deterministic.finish_waiting(); - deterministic.run_until_parked(); - - check_consistency_between_clients(&clients); - - for (client, mut cx) in clients { - cx.update(|cx| { - let store = cx.remove_global::(); - cx.clear_globals(); - cx.set_global(store); - drop(client); - }); - } - - deterministic.run_until_parked(); -} - -fn on_failure() { - if let Some(plan) = PLAN.lock().clone() { - if let Some(path) = &*PLAN_SAVE_PATH { - eprintln!("saved test plan to path {:?}", path); - std::fs::write(path, plan.lock().serialize()).unwrap(); - } - } -} - -async fn apply_server_operation( - deterministic: Arc, - server: &mut TestServer, - clients: &mut Vec<(Rc, TestAppContext)>, - client_tasks: &mut Vec>, - operation_channels: &mut Vec>, - plan: Arc>, - operation: Operation, - cx: &mut TestAppContext, -) -> bool { - match operation { - Operation::AddConnection { user_id } => { - let username; - { - let mut plan = plan.lock(); - let user = plan.user(user_id); - if user.online { - return false; - } - user.online = true; - username = user.username.clone(); - }; - log::info!("Adding new connection for {}", username); - let next_entity_id = (user_id.0 * 10_000) as usize; - let mut client_cx = TestAppContext::new( - cx.foreground_platform(), - cx.platform(), - deterministic.build_foreground(user_id.0 as usize), - deterministic.build_background(), - cx.font_cache(), - cx.leak_detector(), - next_entity_id, - cx.function_name.clone(), - ); - - let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded(); - let client = Rc::new(server.create_client(&mut client_cx, &username).await); - operation_channels.push(operation_tx); - clients.push((client.clone(), client_cx.clone())); - client_tasks.push(client_cx.foreground().spawn(simulate_client( - client, - operation_rx, - plan.clone(), - client_cx, - ))); - - log::info!("Added connection for {}", username); - } - - Operation::RemoveConnection { - user_id: removed_user_id, - } => { - log::info!("Simulating full disconnection of user {}", removed_user_id); - let client_ix = clients - .iter() - .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); - let Some(client_ix) = client_ix else { - return false; - }; - let user_connection_ids = server - .connection_pool - .lock() - .user_connection_ids(removed_user_id) - .collect::>(); - assert_eq!(user_connection_ids.len(), 1); - let removed_peer_id = user_connection_ids[0].into(); - let (client, mut client_cx) = clients.remove(client_ix); - let client_task = client_tasks.remove(client_ix); - operation_channels.remove(client_ix); - server.forbid_connections(); - server.disconnect_client(removed_peer_id); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - deterministic.start_waiting(); - log::info!("Waiting for user {} to exit...", removed_user_id); - client_task.await; - deterministic.finish_waiting(); - server.allow_connections(); - - for project in client.remote_projects().iter() { - project.read_with(&client_cx, |project, _| { - assert!( - project.is_read_only(), - "project {:?} should be read only", - project.remote_id() - ) - }); - } - - for (client, cx) in clients { - let contacts = server - .app_state - .db - .get_contacts(client.current_user_id(cx)) - .await - .unwrap(); - let pool = server.connection_pool.lock(); - for contact in contacts { - if let db::Contact::Accepted { user_id, busy, .. } = contact { - if user_id == removed_user_id { - assert!(!pool.is_user_online(user_id)); - assert!(!busy); - } - } - } - } - - log::info!("{} removed", client.username); - plan.lock().user(removed_user_id).online = false; - client_cx.update(|cx| { - cx.clear_globals(); - drop(client); - }); - } - - Operation::BounceConnection { user_id } => { - log::info!("Simulating temporary disconnection of user {}", user_id); - let user_connection_ids = server - .connection_pool - .lock() - .user_connection_ids(user_id) - .collect::>(); - if user_connection_ids.is_empty() { - return false; - } - assert_eq!(user_connection_ids.len(), 1); - let peer_id = user_connection_ids[0].into(); - server.disconnect_client(peer_id); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - } - - Operation::RestartServer => { - log::info!("Simulating server restart"); - server.reset().await; - deterministic.advance_clock(RECEIVE_TIMEOUT); - server.start().await.unwrap(); - deterministic.advance_clock(CLEANUP_TIMEOUT); - let environment = &server.app_state.config.zed_environment; - let (stale_room_ids, _) = server - .app_state - .db - .stale_server_resource_ids(environment, server.id()) - .await - .unwrap(); - assert_eq!(stale_room_ids, vec![]); - } - - Operation::MutateClients { - user_ids, - batch_id, - quiesce, - } => { - let mut applied = false; - for user_id in user_ids { - let client_ix = clients - .iter() - .position(|(client, cx)| client.current_user_id(cx) == user_id); - let Some(client_ix) = client_ix else { continue }; - applied = true; - if let Err(err) = operation_channels[client_ix].unbounded_send(batch_id) { - log::error!("error signaling user {user_id}: {err}"); - } - } - - if quiesce && applied { - deterministic.run_until_parked(); - check_consistency_between_clients(&clients); - } - - return applied; - } - } - true -} - -async fn apply_client_operation( - client: &TestClient, - operation: ClientOperation, - cx: &mut TestAppContext, -) -> Result<(), TestError> { - match operation { - ClientOperation::AcceptIncomingCall => { - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) { - Err(TestError::Inapplicable)?; - } - - log::info!("{}: accepting incoming call", client.username); - active_call - .update(cx, |call, cx| call.accept_incoming(cx)) - .await?; - } - - ClientOperation::RejectIncomingCall => { - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) { - Err(TestError::Inapplicable)?; - } - - log::info!("{}: declining incoming call", client.username); - active_call.update(cx, |call, cx| call.decline_incoming(cx))?; - } - - ClientOperation::LeaveCall => { - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.room().is_none()) { - Err(TestError::Inapplicable)?; - } - - log::info!("{}: hanging up", client.username); - active_call.update(cx, |call, cx| call.hang_up(cx)).await?; - } - - ClientOperation::InviteContactToCall { user_id } => { - let active_call = cx.read(ActiveCall::global); - - log::info!("{}: inviting {}", client.username, user_id,); - active_call - .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx)) - .await - .log_err(); - } - - ClientOperation::OpenLocalProject { first_root_name } => { - log::info!( - "{}: opening local project at {:?}", - client.username, - first_root_name - ); - - let root_path = Path::new("/").join(&first_root_name); - client.fs().create_dir(&root_path).await.unwrap(); - client - .fs() - .create_file(&root_path.join("main.rs"), Default::default()) - .await - .unwrap(); - let project = client.build_local_project(root_path, cx).await.0; - ensure_project_shared(&project, client, cx).await; - client.local_projects_mut().push(project.clone()); - } - - ClientOperation::AddWorktreeToProject { - project_root_name, - new_root_path, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: finding/creating local worktree at {:?} to project with root path {}", - client.username, - new_root_path, - project_root_name - ); - - ensure_project_shared(&project, client, cx).await; - if !client.fs().paths(false).contains(&new_root_path) { - client.fs().create_dir(&new_root_path).await.unwrap(); - } - project - .update(cx, |project, cx| { - project.find_or_create_local_worktree(&new_root_path, true, cx) - }) - .await - .unwrap(); - } - - ClientOperation::CloseRemoteProject { project_root_name } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: closing remote project with root path {}", - client.username, - project_root_name, - ); - - let ix = client - .remote_projects() - .iter() - .position(|p| p == &project) - .unwrap(); - cx.update(|_| { - client.remote_projects_mut().remove(ix); - client.buffers().retain(|p, _| *p != project); - drop(project); - }); - } - - ClientOperation::OpenRemoteProject { - host_id, - first_root_name, - } => { - let active_call = cx.read(ActiveCall::global); - let project = active_call - .update(cx, |call, cx| { - let room = call.room().cloned()?; - let participant = room - .read(cx) - .remote_participants() - .get(&host_id.to_proto())?; - let project_id = participant - .projects - .iter() - .find(|project| project.worktree_root_names[0] == first_root_name)? - .id; - Some(room.update(cx, |room, cx| { - room.join_project( - project_id, - client.language_registry().clone(), - FakeFs::new(cx.background().clone()), - cx, - ) - })) - }) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: joining remote project of user {}, root name {}", - client.username, - host_id, - first_root_name, - ); - - let project = project.await?; - client.remote_projects_mut().push(project.clone()); - } - - ClientOperation::CreateWorktreeEntry { - project_root_name, - is_local, - full_path, - is_dir, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let project_path = project_path_for_full_path(&project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: creating {} at path {:?} in {} project {}", - client.username, - if is_dir { "dir" } else { "file" }, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - ); - - ensure_project_shared(&project, client, cx).await; - project - .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) - .unwrap() - .await?; - } - - ClientOperation::OpenBuffer { - project_root_name, - is_local, - full_path, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let project_path = project_path_for_full_path(&project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: opening buffer {:?} in {} project {}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - ); - - ensure_project_shared(&project, client, cx).await; - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx)) - .await?; - client.buffers_for_project(&project).insert(buffer); - } - - ClientOperation::EditBuffer { - project_root_name, - is_local, - full_path, - edits, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: editing buffer {:?} in {} project {} with {:?}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - edits - ); - - ensure_project_shared(&project, client, cx).await; - buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(); - buffer.edit( - edits.into_iter().map(|(range, text)| { - let start = snapshot.clip_offset(range.start, Bias::Left); - let end = snapshot.clip_offset(range.end, Bias::Right); - (start..end, text) - }), - None, - cx, - ); - }); - } - - ClientOperation::CloseBuffer { - project_root_name, - is_local, - full_path, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: closing buffer {:?} in {} project {}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name - ); - - ensure_project_shared(&project, client, cx).await; - cx.update(|_| { - client.buffers_for_project(&project).remove(&buffer); - drop(buffer); - }); - } - - ClientOperation::SaveBuffer { - project_root_name, - is_local, - full_path, - detach, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: saving buffer {:?} in {} project {}, {}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - if detach { "detaching" } else { "awaiting" } - ); - - ensure_project_shared(&project, client, cx).await; - let requested_version = buffer.read_with(cx, |buffer, _| buffer.version()); - let save = project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); - let save = cx.spawn(|cx| async move { - save.await - .map_err(|err| anyhow!("save request failed: {:?}", err))?; - assert!(buffer - .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() }) - .observed_all(&requested_version)); - anyhow::Ok(()) - }); - if detach { - cx.update(|cx| save.detach_and_log_err(cx)); - } else { - save.await?; - } - } - - ClientOperation::RequestLspDataInBuffer { - project_root_name, - is_local, - full_path, - offset, - kind, - detach, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: request LSP {:?} for buffer {:?} in {} project {}, {}", - client.username, - kind, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - if detach { "detaching" } else { "awaiting" } - ); - - use futures::{FutureExt as _, TryFutureExt as _}; - let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left)); - let request = cx.foreground().spawn(project.update(cx, |project, cx| { - match kind { - LspRequestKind::Rename => project - .prepare_rename(buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Completion => project - .completions(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::CodeAction => project - .code_actions(&buffer, offset..offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Definition => project - .definition(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Highlights => project - .document_highlights(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - } - })); - if detach { - request.detach(); - } else { - request.await?; - } - } - - ClientOperation::SearchProject { - project_root_name, - is_local, - query, - detach, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: search {} project {} for {:?}, {}", - client.username, - if is_local { "local" } else { "remote" }, - project_root_name, - query, - if detach { "detaching" } else { "awaiting" } - ); - - let mut search = project.update(cx, |project, cx| { - project.search( - SearchQuery::text(query, false, false, Vec::new(), Vec::new()), - cx, - ) - }); - drop(project); - let search = cx.background().spawn(async move { - let mut results = HashMap::default(); - while let Some((buffer, ranges)) = search.next().await { - results.entry(buffer).or_insert(ranges); - } - results - }); - search.await; - } - - ClientOperation::WriteFsEntry { - path, - is_dir, - content, - } => { - if !client - .fs() - .directories(false) - .contains(&path.parent().unwrap().to_owned()) - { - return Err(TestError::Inapplicable); - } - - if is_dir { - log::info!("{}: creating dir at {:?}", client.username, path); - client.fs().create_dir(&path).await.unwrap(); - } else { - let exists = client.fs().metadata(&path).await?.is_some(); - let verb = if exists { "updating" } else { "creating" }; - log::info!("{}: {} file at {:?}", verb, client.username, path); - - client - .fs() - .save(&path, &content.as_str().into(), text::LineEnding::Unix) - .await - .unwrap(); - } - } - - ClientOperation::GitOperation { operation } => match operation { - GitOperation::WriteGitIndex { - repo_path, - contents, - } => { - if !client.fs().directories(false).contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - for (path, _) in contents.iter() { - if !client.fs().files().contains(&repo_path.join(path)) { - return Err(TestError::Inapplicable); - } - } - - log::info!( - "{}: writing git index for repo {:?}: {:?}", - client.username, - repo_path, - contents - ); - - let dot_git_dir = repo_path.join(".git"); - let contents = contents - .iter() - .map(|(path, contents)| (path.as_path(), contents.clone())) - .collect::>(); - if client.fs().metadata(&dot_git_dir).await?.is_none() { - client.fs().create_dir(&dot_git_dir).await?; - } - client.fs().set_index_for_repo(&dot_git_dir, &contents); - } - GitOperation::WriteGitBranch { - repo_path, - new_branch, - } => { - if !client.fs().directories(false).contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - log::info!( - "{}: writing git branch for repo {:?}: {:?}", - client.username, - repo_path, - new_branch - ); - - let dot_git_dir = repo_path.join(".git"); - if client.fs().metadata(&dot_git_dir).await?.is_none() { - client.fs().create_dir(&dot_git_dir).await?; - } - client.fs().set_branch_name(&dot_git_dir, new_branch); - } - GitOperation::WriteGitStatuses { - repo_path, - statuses, - git_operation, - } => { - if !client.fs().directories(false).contains(&repo_path) { - return Err(TestError::Inapplicable); - } - for (path, _) in statuses.iter() { - if !client.fs().files().contains(&repo_path.join(path)) { - return Err(TestError::Inapplicable); - } - } - - log::info!( - "{}: writing git statuses for repo {:?}: {:?}", - client.username, - repo_path, - statuses - ); - - let dot_git_dir = repo_path.join(".git"); - - let statuses = statuses - .iter() - .map(|(path, val)| (path.as_path(), val.clone())) - .collect::>(); - - if client.fs().metadata(&dot_git_dir).await?.is_none() { - client.fs().create_dir(&dot_git_dir).await?; - } - - if git_operation { - client - .fs() - .set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice()); - } else { - client.fs().set_status_for_repo_via_working_copy_change( - &dot_git_dir, - statuses.as_slice(), - ); - } - } - }, - } - Ok(()) -} - -fn check_consistency_between_clients(clients: &[(Rc, TestAppContext)]) { - for (client, client_cx) in clients { - for guest_project in client.remote_projects().iter() { - guest_project.read_with(client_cx, |guest_project, cx| { - let host_project = clients.iter().find_map(|(client, cx)| { - let project = client - .local_projects() - .iter() - .find(|host_project| { - host_project.read_with(cx, |host_project, _| { - host_project.remote_id() == guest_project.remote_id() - }) - })? - .clone(); - Some((project, cx)) - }); - - if !guest_project.is_read_only() { - if let Some((host_project, host_cx)) = host_project { - let host_worktree_snapshots = - host_project.read_with(host_cx, |host_project, cx| { - host_project - .worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - (worktree.id(), worktree.snapshot()) - }) - .collect::>() - }); - let guest_worktree_snapshots = guest_project - .worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - (worktree.id(), worktree.snapshot()) - }) - .collect::>(); - - assert_eq!( - guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::>(), - host_worktree_snapshots.values().map(|w| w.abs_path()).collect::>(), - "{} has different worktrees than the host for project {:?}", - client.username, guest_project.remote_id(), - ); - - for (id, host_snapshot) in &host_worktree_snapshots { - let guest_snapshot = &guest_worktree_snapshots[id]; - assert_eq!( - guest_snapshot.root_name(), - host_snapshot.root_name(), - "{} has different root name than the host for worktree {}, project {:?}", - client.username, - id, - guest_project.remote_id(), - ); - assert_eq!( - guest_snapshot.abs_path(), - host_snapshot.abs_path(), - "{} has different abs path than the host for worktree {}, project: {:?}", - client.username, - id, - guest_project.remote_id(), - ); - assert_eq!( - guest_snapshot.entries(false).collect::>(), - host_snapshot.entries(false).collect::>(), - "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}", - client.username, - host_snapshot.abs_path(), - id, - guest_project.remote_id(), - ); - assert_eq!(guest_snapshot.repositories().collect::>(), host_snapshot.repositories().collect::>(), - "{} has different repositories than the host for worktree {:?} and project {:?}", - client.username, - host_snapshot.abs_path(), - guest_project.remote_id(), - ); - assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(), - "{} has different scan id than the host for worktree {:?} and project {:?}", - client.username, - host_snapshot.abs_path(), - guest_project.remote_id(), - ); - } - } - } - - for buffer in guest_project.opened_buffers(cx) { - let buffer = buffer.read(cx); - assert_eq!( - buffer.deferred_ops_len(), - 0, - "{} has deferred operations for buffer {:?} in project {:?}", - client.username, - buffer.file().unwrap().full_path(cx), - guest_project.remote_id(), - ); - } - }); - } - - let buffers = client.buffers().clone(); - for (guest_project, guest_buffers) in &buffers { - let project_id = if guest_project.read_with(client_cx, |project, _| { - project.is_local() || project.is_read_only() - }) { - continue; - } else { - guest_project - .read_with(client_cx, |project, _| project.remote_id()) - .unwrap() - }; - let guest_user_id = client.user_id().unwrap(); - - let host_project = clients.iter().find_map(|(client, cx)| { - let project = client - .local_projects() - .iter() - .find(|host_project| { - host_project.read_with(cx, |host_project, _| { - host_project.remote_id() == Some(project_id) - }) - })? - .clone(); - Some((client.user_id().unwrap(), project, cx)) - }); - - let (host_user_id, host_project, host_cx) = - if let Some((host_user_id, host_project, host_cx)) = host_project { - (host_user_id, host_project, host_cx) - } else { - continue; - }; - - for guest_buffer in guest_buffers { - let buffer_id = guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id()); - let host_buffer = host_project.read_with(host_cx, |project, cx| { - project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| { - panic!( - "host does not have buffer for guest:{}, peer:{:?}, id:{}", - client.username, - client.peer_id(), - buffer_id - ) - }) - }); - let path = host_buffer - .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); - - assert_eq!( - guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()), - 0, - "{}, buffer {}, path {:?} has deferred operations", - client.username, - buffer_id, - path, - ); - assert_eq!( - guest_buffer.read_with(client_cx, |buffer, _| buffer.text()), - host_buffer.read_with(host_cx, |buffer, _| buffer.text()), - "{}, buffer {}, path {:?}, differs from the host's buffer", - client.username, - buffer_id, - path - ); - - let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned()); - let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned()); - match (host_file, guest_file) { - (Some(host_file), Some(guest_file)) => { - assert_eq!(guest_file.path(), host_file.path()); - assert_eq!(guest_file.is_deleted(), host_file.is_deleted()); - assert_eq!( - guest_file.mtime(), - host_file.mtime(), - "guest {} mtime does not match host {} for path {:?} in project {}", - guest_user_id, - host_user_id, - guest_file.path(), - project_id, - ); - } - (None, None) => {} - (None, _) => panic!("host's file is None, guest's isn't"), - (_, None) => panic!("guest's file is None, hosts's isn't"), - } - - let host_diff_base = - host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string)); - let guest_diff_base = guest_buffer - .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string)); - assert_eq!( - guest_diff_base, host_diff_base, - "guest {} diff base does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_saved_version = - host_buffer.read_with(host_cx, |b, _| b.saved_version().clone()); - let guest_saved_version = - guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone()); - assert_eq!( - guest_saved_version, host_saved_version, - "guest {} saved version does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_saved_version_fingerprint = - host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint()); - let guest_saved_version_fingerprint = - guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint()); - assert_eq!( - guest_saved_version_fingerprint, host_saved_version_fingerprint, - "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime()); - let guest_saved_mtime = guest_buffer.read_with(client_cx, |b, _| b.saved_mtime()); - assert_eq!( - guest_saved_mtime, host_saved_mtime, - "guest {} saved mtime does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty()); - let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty()); - assert_eq!(guest_is_dirty, host_is_dirty, - "guest {} dirty status does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict()); - let guest_has_conflict = guest_buffer.read_with(client_cx, |b, _| b.has_conflict()); - assert_eq!(guest_has_conflict, host_has_conflict, - "guest {} conflict status does not match host's for path {path:?} in project {project_id}", - client.username - ); - } - } - } -} - -struct TestPlan { - rng: StdRng, - replay: bool, - stored_operations: Vec<(StoredOperation, Arc)>, - max_operations: usize, - operation_ix: usize, - users: Vec, - next_batch_id: usize, - allow_server_restarts: bool, - allow_client_reconnection: bool, - allow_client_disconnection: bool, -} - -struct UserTestPlan { - user_id: UserId, - username: String, - next_root_id: usize, - operation_ix: usize, - online: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(untagged)] -enum StoredOperation { - Server(Operation), - Client { - user_id: UserId, - batch_id: usize, - operation: ClientOperation, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum Operation { - AddConnection { - user_id: UserId, - }, - RemoveConnection { - user_id: UserId, - }, - BounceConnection { - user_id: UserId, - }, - RestartServer, - MutateClients { - batch_id: usize, - #[serde(skip_serializing)] - #[serde(skip_deserializing)] - user_ids: Vec, - quiesce: bool, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum ClientOperation { - AcceptIncomingCall, - RejectIncomingCall, - LeaveCall, - InviteContactToCall { - user_id: UserId, - }, - OpenLocalProject { - first_root_name: String, - }, - OpenRemoteProject { - host_id: UserId, - first_root_name: String, - }, - AddWorktreeToProject { - project_root_name: String, - new_root_path: PathBuf, - }, - CloseRemoteProject { - project_root_name: String, - }, - OpenBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - }, - SearchProject { - project_root_name: String, - is_local: bool, - query: String, - detach: bool, - }, - EditBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - edits: Vec<(Range, Arc)>, - }, - CloseBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - }, - SaveBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - detach: bool, - }, - RequestLspDataInBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - offset: usize, - kind: LspRequestKind, - detach: bool, - }, - CreateWorktreeEntry { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - is_dir: bool, - }, - WriteFsEntry { - path: PathBuf, - is_dir: bool, - content: String, - }, - GitOperation { - operation: GitOperation, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum GitOperation { - WriteGitIndex { - repo_path: PathBuf, - contents: Vec<(PathBuf, String)>, - }, - WriteGitBranch { - repo_path: PathBuf, - new_branch: Option, - }, - WriteGitStatuses { - repo_path: PathBuf, - statuses: Vec<(PathBuf, GitFileStatus)>, - git_operation: bool, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum LspRequestKind { - Rename, - Completion, - CodeAction, - Definition, - Highlights, -} - -enum TestError { - Inapplicable, - Other(anyhow::Error), -} - -impl From for TestError { - fn from(value: anyhow::Error) -> Self { - Self::Other(value) - } -} - -impl TestPlan { - fn new(mut rng: StdRng, users: Vec, max_operations: usize) -> Self { - Self { - replay: false, - allow_server_restarts: rng.gen_bool(0.7), - allow_client_reconnection: rng.gen_bool(0.7), - allow_client_disconnection: rng.gen_bool(0.1), - stored_operations: Vec::new(), - operation_ix: 0, - next_batch_id: 0, - max_operations, - users, - rng, - } - } - - fn deserialize(&mut self, json: Vec) { - let stored_operations: Vec = serde_json::from_slice(&json).unwrap(); - self.replay = true; - self.stored_operations = stored_operations - .iter() - .cloned() - .enumerate() - .map(|(i, mut operation)| { - if let StoredOperation::Server(Operation::MutateClients { - batch_id: current_batch_id, - user_ids, - .. - }) = &mut operation - { - assert!(user_ids.is_empty()); - user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| { - if let StoredOperation::Client { - user_id, batch_id, .. - } = operation - { - if batch_id == current_batch_id { - return Some(user_id); - } - } - None - })); - user_ids.sort_unstable(); - } - (operation, Arc::new(AtomicBool::new(false))) - }) - .collect() - } - - fn serialize(&mut self) -> Vec { - // Format each operation as one line - let mut json = Vec::new(); - json.push(b'['); - for (operation, applied) in &self.stored_operations { - if !applied.load(SeqCst) { - continue; - } - if json.len() > 1 { - json.push(b','); - } - json.extend_from_slice(b"\n "); - serde_json::to_writer(&mut json, operation).unwrap(); - } - json.extend_from_slice(b"\n]\n"); - json - } - - fn next_server_operation( - &mut self, - clients: &[(Rc, TestAppContext)], - ) -> Option<(Operation, Arc)> { - if self.replay { - while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) { - self.operation_ix += 1; - if let (StoredOperation::Server(operation), applied) = stored_operation { - return Some((operation.clone(), applied.clone())); - } - } - None - } else { - let operation = self.generate_server_operation(clients)?; - let applied = Arc::new(AtomicBool::new(false)); - self.stored_operations - .push((StoredOperation::Server(operation.clone()), applied.clone())); - Some((operation, applied)) - } - } - - fn next_client_operation( - &mut self, - client: &TestClient, - current_batch_id: usize, - cx: &TestAppContext, - ) -> Option<(ClientOperation, Arc)> { - let current_user_id = client.current_user_id(cx); - let user_ix = self - .users - .iter() - .position(|user| user.user_id == current_user_id) - .unwrap(); - let user_plan = &mut self.users[user_ix]; - - if self.replay { - while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) { - user_plan.operation_ix += 1; - if let ( - StoredOperation::Client { - user_id, operation, .. - }, - applied, - ) = stored_operation - { - if user_id == ¤t_user_id { - return Some((operation.clone(), applied.clone())); - } - } - } - None - } else { - let operation = self.generate_client_operation(current_user_id, client, cx)?; - let applied = Arc::new(AtomicBool::new(false)); - self.stored_operations.push(( - StoredOperation::Client { - user_id: current_user_id, - batch_id: current_batch_id, - operation: operation.clone(), - }, - applied.clone(), - )); - Some((operation, applied)) - } - } - - fn generate_server_operation( - &mut self, - clients: &[(Rc, TestAppContext)], - ) -> Option { - if self.operation_ix == self.max_operations { - return None; - } - - Some(loop { - break match self.rng.gen_range(0..100) { - 0..=29 if clients.len() < self.users.len() => { - let user = self - .users - .iter() - .filter(|u| !u.online) - .choose(&mut self.rng) - .unwrap(); - self.operation_ix += 1; - Operation::AddConnection { - user_id: user.user_id, - } - } - 30..=34 if clients.len() > 1 && self.allow_client_disconnection => { - let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; - let user_id = client.current_user_id(cx); - self.operation_ix += 1; - Operation::RemoveConnection { user_id } - } - 35..=39 if clients.len() > 1 && self.allow_client_reconnection => { - let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; - let user_id = client.current_user_id(cx); - self.operation_ix += 1; - Operation::BounceConnection { user_id } - } - 40..=44 if self.allow_server_restarts && clients.len() > 1 => { - self.operation_ix += 1; - Operation::RestartServer - } - _ if !clients.is_empty() => { - let count = self - .rng - .gen_range(1..10) - .min(self.max_operations - self.operation_ix); - let batch_id = util::post_inc(&mut self.next_batch_id); - let mut user_ids = (0..count) - .map(|_| { - let ix = self.rng.gen_range(0..clients.len()); - let (client, cx) = &clients[ix]; - client.current_user_id(cx) - }) - .collect::>(); - user_ids.sort_unstable(); - Operation::MutateClients { - user_ids, - batch_id, - quiesce: self.rng.gen_bool(0.7), - } - } - _ => continue, - }; - }) - } - - fn generate_client_operation( - &mut self, - user_id: UserId, - client: &TestClient, - cx: &TestAppContext, - ) -> Option { - if self.operation_ix == self.max_operations { - return None; - } - - self.operation_ix += 1; - let call = cx.read(ActiveCall::global); - Some(loop { - match self.rng.gen_range(0..100_u32) { - // Mutate the call - 0..=29 => { - // Respond to an incoming call - if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) { - break if self.rng.gen_bool(0.7) { - ClientOperation::AcceptIncomingCall - } else { - ClientOperation::RejectIncomingCall - }; - } - - match self.rng.gen_range(0..100_u32) { - // Invite a contact to the current call - 0..=70 => { - let available_contacts = - client.user_store().read_with(cx, |user_store, _| { - user_store - .contacts() - .iter() - .filter(|contact| contact.online && !contact.busy) - .cloned() - .collect::>() - }); - if !available_contacts.is_empty() { - let contact = available_contacts.choose(&mut self.rng).unwrap(); - break ClientOperation::InviteContactToCall { - user_id: UserId(contact.user.id as i32), - }; - } - } - - // Leave the current call - 71.. => { - if self.allow_client_disconnection - && call.read_with(cx, |call, _| call.room().is_some()) - { - break ClientOperation::LeaveCall; - } - } - } - } - - // Mutate projects - 30..=59 => match self.rng.gen_range(0..100_u32) { - // Open a new project - 0..=70 => { - // Open a remote project - if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) { - let existing_remote_project_ids = cx.read(|cx| { - client - .remote_projects() - .iter() - .map(|p| p.read(cx).remote_id().unwrap()) - .collect::>() - }); - let new_remote_projects = room.read_with(cx, |room, _| { - room.remote_participants() - .values() - .flat_map(|participant| { - participant.projects.iter().filter_map(|project| { - if existing_remote_project_ids.contains(&project.id) { - None - } else { - Some(( - UserId::from_proto(participant.user.id), - project.worktree_root_names[0].clone(), - )) - } - }) - }) - .collect::>() - }); - if !new_remote_projects.is_empty() { - let (host_id, first_root_name) = - new_remote_projects.choose(&mut self.rng).unwrap().clone(); - break ClientOperation::OpenRemoteProject { - host_id, - first_root_name, - }; - } - } - // Open a local project - else { - let first_root_name = self.next_root_dir_name(user_id); - break ClientOperation::OpenLocalProject { first_root_name }; - } - } - - // Close a remote project - 71..=80 => { - if !client.remote_projects().is_empty() { - let project = client - .remote_projects() - .choose(&mut self.rng) - .unwrap() - .clone(); - let first_root_name = root_name_for_project(&project, cx); - break ClientOperation::CloseRemoteProject { - project_root_name: first_root_name, - }; - } - } - - // Mutate project worktrees - 81.. => match self.rng.gen_range(0..100_u32) { - // Add a worktree to a local project - 0..=50 => { - let Some(project) = - client.local_projects().choose(&mut self.rng).cloned() - else { - continue; - }; - let project_root_name = root_name_for_project(&project, cx); - let mut paths = client.fs().paths(false); - paths.remove(0); - let new_root_path = if paths.is_empty() || self.rng.gen() { - Path::new("/").join(&self.next_root_dir_name(user_id)) - } else { - paths.choose(&mut self.rng).unwrap().clone() - }; - break ClientOperation::AddWorktreeToProject { - project_root_name, - new_root_path, - }; - } - - // Add an entry to a worktree - _ => { - let Some(project) = choose_random_project(client, &mut self.rng) else { - continue; - }; - let project_root_name = root_name_for_project(&project, cx); - let is_local = project.read_with(cx, |project, _| project.is_local()); - let worktree = project.read_with(cx, |project, cx| { - project - .worktrees(cx) - .filter(|worktree| { - let worktree = worktree.read(cx); - worktree.is_visible() - && worktree.entries(false).any(|e| e.is_file()) - && worktree.root_entry().map_or(false, |e| e.is_dir()) - }) - .choose(&mut self.rng) - }); - let Some(worktree) = worktree else { continue }; - let is_dir = self.rng.gen::(); - let mut full_path = - worktree.read_with(cx, |w, _| PathBuf::from(w.root_name())); - full_path.push(gen_file_name(&mut self.rng)); - if !is_dir { - full_path.set_extension("rs"); - } - break ClientOperation::CreateWorktreeEntry { - project_root_name, - is_local, - full_path, - is_dir, - }; - } - }, - }, - - // Query and mutate buffers - 60..=90 => { - let Some(project) = choose_random_project(client, &mut self.rng) else { - continue; - }; - let project_root_name = root_name_for_project(&project, cx); - let is_local = project.read_with(cx, |project, _| project.is_local()); - - match self.rng.gen_range(0..100_u32) { - // Manipulate an existing buffer - 0..=70 => { - let Some(buffer) = client - .buffers_for_project(&project) - .iter() - .choose(&mut self.rng) - .cloned() - else { - continue; - }; - - let full_path = buffer - .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); - - match self.rng.gen_range(0..100_u32) { - // Close the buffer - 0..=15 => { - break ClientOperation::CloseBuffer { - project_root_name, - is_local, - full_path, - }; - } - // Save the buffer - 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => { - let detach = self.rng.gen_bool(0.3); - break ClientOperation::SaveBuffer { - project_root_name, - is_local, - full_path, - detach, - }; - } - // Edit the buffer - 30..=69 => { - let edits = buffer.read_with(cx, |buffer, _| { - buffer.get_random_edits(&mut self.rng, 3) - }); - break ClientOperation::EditBuffer { - project_root_name, - is_local, - full_path, - edits, - }; - } - // Make an LSP request - _ => { - let offset = buffer.read_with(cx, |buffer, _| { - buffer.clip_offset( - self.rng.gen_range(0..=buffer.len()), - language::Bias::Left, - ) - }); - let detach = self.rng.gen(); - break ClientOperation::RequestLspDataInBuffer { - project_root_name, - full_path, - offset, - is_local, - kind: match self.rng.gen_range(0..5_u32) { - 0 => LspRequestKind::Rename, - 1 => LspRequestKind::Highlights, - 2 => LspRequestKind::Definition, - 3 => LspRequestKind::CodeAction, - 4.. => LspRequestKind::Completion, - }, - detach, - }; - } - } - } - - 71..=80 => { - let query = self.rng.gen_range('a'..='z').to_string(); - let detach = self.rng.gen_bool(0.3); - break ClientOperation::SearchProject { - project_root_name, - is_local, - query, - detach, - }; - } - - // Open a buffer - 81.. => { - let worktree = project.read_with(cx, |project, cx| { - project - .worktrees(cx) - .filter(|worktree| { - let worktree = worktree.read(cx); - worktree.is_visible() - && worktree.entries(false).any(|e| e.is_file()) - }) - .choose(&mut self.rng) - }); - let Some(worktree) = worktree else { continue }; - let full_path = worktree.read_with(cx, |worktree, _| { - let entry = worktree - .entries(false) - .filter(|e| e.is_file()) - .choose(&mut self.rng) - .unwrap(); - if entry.path.as_ref() == Path::new("") { - Path::new(worktree.root_name()).into() - } else { - Path::new(worktree.root_name()).join(&entry.path) - } - }); - break ClientOperation::OpenBuffer { - project_root_name, - is_local, - full_path, - }; - } - } - } - - // Update a git related action - 91..=95 => { - break ClientOperation::GitOperation { - operation: self.generate_git_operation(client), - }; - } - - // Create or update a file or directory - 96.. => { - let is_dir = self.rng.gen::(); - let content; - let mut path; - let dir_paths = client.fs().directories(false); - - if is_dir { - content = String::new(); - path = dir_paths.choose(&mut self.rng).unwrap().clone(); - path.push(gen_file_name(&mut self.rng)); - } else { - content = Alphanumeric.sample_string(&mut self.rng, 16); - - // Create a new file or overwrite an existing file - let file_paths = client.fs().files(); - if file_paths.is_empty() || self.rng.gen_bool(0.5) { - path = dir_paths.choose(&mut self.rng).unwrap().clone(); - path.push(gen_file_name(&mut self.rng)); - path.set_extension("rs"); - } else { - path = file_paths.choose(&mut self.rng).unwrap().clone() - }; - } - break ClientOperation::WriteFsEntry { - path, - is_dir, - content, - }; - } - } - }) - } - - fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation { - fn generate_file_paths( - repo_path: &Path, - rng: &mut StdRng, - client: &TestClient, - ) -> Vec { - let mut paths = client - .fs() - .files() - .into_iter() - .filter(|path| path.starts_with(repo_path)) - .collect::>(); - - let count = rng.gen_range(0..=paths.len()); - paths.shuffle(rng); - paths.truncate(count); - - paths - .iter() - .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf()) - .collect::>() - } - - let repo_path = client - .fs() - .directories(false) - .choose(&mut self.rng) - .unwrap() - .clone(); - - match self.rng.gen_range(0..100_u32) { - 0..=25 => { - let file_paths = generate_file_paths(&repo_path, &mut self.rng, client); - - let contents = file_paths - .into_iter() - .map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16))) - .collect(); - - GitOperation::WriteGitIndex { - repo_path, - contents, - } - } - 26..=63 => { - let new_branch = (self.rng.gen_range(0..10) > 3) - .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); - - GitOperation::WriteGitBranch { - repo_path, - new_branch, - } - } - 64..=100 => { - let file_paths = generate_file_paths(&repo_path, &mut self.rng, client); - - let statuses = file_paths - .into_iter() - .map(|paths| { - ( - paths, - match self.rng.gen_range(0..3_u32) { - 0 => GitFileStatus::Added, - 1 => GitFileStatus::Modified, - 2 => GitFileStatus::Conflict, - _ => unreachable!(), - }, - ) - }) - .collect::>(); - - let git_operation = self.rng.gen::(); - - GitOperation::WriteGitStatuses { - repo_path, - statuses, - git_operation, - } - } - _ => unreachable!(), - } - } - - fn next_root_dir_name(&mut self, user_id: UserId) -> String { - let user_ix = self - .users - .iter() - .position(|user| user.user_id == user_id) - .unwrap(); - let root_id = util::post_inc(&mut self.users[user_ix].next_root_id); - format!("dir-{user_id}-{root_id}") - } - - fn user(&mut self, user_id: UserId) -> &mut UserTestPlan { - let ix = self - .users - .iter() - .position(|user| user.user_id == user_id) - .unwrap(); - &mut self.users[ix] - } -} - -async fn simulate_client( - client: Rc, - mut operation_rx: futures::channel::mpsc::UnboundedReceiver, - plan: Arc>, - mut cx: TestAppContext, -) { - // Setup language server - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - None, - ); - let _fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-fake-language-server", - capabilities: lsp::LanguageServer::full_capabilities(), - initializer: Some(Box::new({ - let fs = client.app_state.fs.clone(); - move |fake_server: &mut FakeLanguageServer| { - fake_server.handle_request::( - |_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "the-new-text".to_string(), - })), - ..Default::default() - }, - ]))) - }, - ); - - fake_server.handle_request::( - |_, _| async move { - Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), - ..Default::default() - }, - )])) - }, - ); - - fake_server.handle_request::( - |params, _| async move { - Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( - params.position, - params.position, - )))) - }, - ); - - fake_server.handle_request::({ - let fs = fs.clone(); - move |_, cx| { - let background = cx.background(); - let mut rng = background.rng(); - let count = rng.gen_range::(1..3); - let files = fs.as_fake().files(); - let files = (0..count) - .map(|_| files.choose(&mut *rng).unwrap().clone()) - .collect::>(); - async move { - log::info!("LSP: Returning definitions in files {:?}", &files); - Ok(Some(lsp::GotoDefinitionResponse::Array( - files - .into_iter() - .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), - range: Default::default(), - }) - .collect(), - ))) - } - } - }); - - fake_server.handle_request::( - move |_, cx| { - let mut highlights = Vec::new(); - let background = cx.background(); - let mut rng = background.rng(); - - let highlight_count = rng.gen_range(1..=5); - for _ in 0..highlight_count { - let start_row = rng.gen_range(0..100); - let start_column = rng.gen_range(0..100); - let end_row = rng.gen_range(0..100); - let end_column = rng.gen_range(0..100); - let start = PointUtf16::new(start_row, start_column); - let end = PointUtf16::new(end_row, end_column); - let range = if start > end { end..start } else { start..end }; - highlights.push(lsp::DocumentHighlight { - range: range_to_lsp(range.clone()), - kind: Some(lsp::DocumentHighlightKind::READ), - }); - } - highlights.sort_unstable_by_key(|highlight| { - (highlight.range.start, highlight.range.end) - }); - async move { Ok(Some(highlights)) } - }, - ); - } - })), - ..Default::default() - })) - .await; - client.app_state.languages.add(Arc::new(language)); - - while let Some(batch_id) = operation_rx.next().await { - let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) - else { - break; - }; - applied.store(true, SeqCst); - match apply_client_operation(&client, operation, &mut cx).await { - Ok(()) => {} - Err(TestError::Inapplicable) => { - applied.store(false, SeqCst); - log::info!("skipped operation"); - } - Err(TestError::Other(error)) => { - log::error!("{} error: {}", client.username, error); - } - } - cx.background().simulate_random_delay().await; - } - log::info!("{}: done", client.username); -} - -fn buffer_for_full_path( - client: &TestClient, - project: &ModelHandle, - full_path: &PathBuf, - cx: &TestAppContext, -) -> Option> { - client - .buffers_for_project(project) - .iter() - .find(|buffer| { - buffer.read_with(cx, |buffer, cx| { - buffer.file().unwrap().full_path(cx) == *full_path - }) - }) - .cloned() -} - -fn project_for_root_name( - client: &TestClient, - root_name: &str, - cx: &TestAppContext, -) -> Option> { - if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) { - return Some(client.local_projects()[ix].clone()); - } - if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) { - return Some(client.remote_projects()[ix].clone()); - } - None -} - -fn project_ix_for_root_name( - projects: &[ModelHandle], - root_name: &str, - cx: &TestAppContext, -) -> Option { - projects.iter().position(|project| { - project.read_with(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next().unwrap(); - worktree.read(cx).root_name() == root_name - }) - }) -} - -fn root_name_for_project(project: &ModelHandle, cx: &TestAppContext) -> String { - project.read_with(cx, |project, cx| { - project - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .root_name() - .to_string() - }) -} - -fn project_path_for_full_path( - project: &ModelHandle, - full_path: &Path, - cx: &TestAppContext, -) -> Option { - let mut components = full_path.components(); - let root_name = components.next().unwrap().as_os_str().to_str().unwrap(); - let path = components.as_path().into(); - let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees(cx).find_map(|worktree| { - let worktree = worktree.read(cx); - if worktree.root_name() == root_name { - Some(worktree.id()) - } else { - None - } - }) - })?; - Some(ProjectPath { worktree_id, path }) -} - -async fn ensure_project_shared( - project: &ModelHandle, - client: &TestClient, - cx: &mut TestAppContext, -) { - let first_root_name = root_name_for_project(project, cx); - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.room().is_some()) - && project.read_with(cx, |project, _| project.is_local() && !project.is_shared()) - { - match active_call - .update(cx, |call, cx| call.share_project(project.clone(), cx)) - .await - { - Ok(project_id) => { - log::info!( - "{}: shared project {} with id {}", - client.username, - first_root_name, - project_id - ); - } - Err(error) => { - log::error!( - "{}: error sharing project {}: {:?}", - client.username, - first_root_name, - error - ); - } - } - } -} - -fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option> { - client - .local_projects() - .iter() - .chain(client.remote_projects().iter()) - .choose(rng) - .cloned() -} - -fn gen_file_name(rng: &mut StdRng) -> String { - let mut name = String::new(); - for _ in 0..10 { - let letter = rng.gen_range('a'..='z'); - name.push(letter); - } - name -} - -fn path_env_var(name: &str) -> Option { - let value = env::var(name).ok()?; - let mut path = PathBuf::from(value); - if path.is_relative() { - let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - abs_path.pop(); - abs_path.pop(); - abs_path.push(path); - path = abs_path - } - Some(path) -} diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs new file mode 100644 index 0000000000..dc102b75c6 --- /dev/null +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -0,0 +1,694 @@ +use crate::{ + db::{self, Database, NewUserParams, UserId}, + rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, + tests::{TestClient, TestServer}, +}; +use async_trait::async_trait; +use futures::StreamExt; +use gpui::{executor::Deterministic, Task, TestAppContext}; +use parking_lot::Mutex; +use rand::prelude::*; +use rpc::RECEIVE_TIMEOUT; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use settings::SettingsStore; +use std::{ + env, + path::PathBuf, + rc::Rc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, +}; + +lazy_static::lazy_static! { + static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); + static ref PLAN_SAVE_PATH: Option = path_env_var("SAVE_PLAN"); + static ref MAX_PEERS: usize = env::var("MAX_PEERS") + .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) + .unwrap_or(3); + static ref MAX_OPERATIONS: usize = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + +} + +static LOADED_PLAN_JSON: Mutex>> = Mutex::new(None); +static LAST_PLAN: Mutex Vec>>> = Mutex::new(None); + +struct TestPlan { + rng: StdRng, + replay: bool, + stored_operations: Vec<(StoredOperation, Arc)>, + max_operations: usize, + operation_ix: usize, + users: Vec, + next_batch_id: usize, + allow_server_restarts: bool, + allow_client_reconnection: bool, + allow_client_disconnection: bool, +} + +pub struct UserTestPlan { + pub user_id: UserId, + pub username: String, + pub allow_client_reconnection: bool, + pub allow_client_disconnection: bool, + next_root_id: usize, + operation_ix: usize, + online: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +enum StoredOperation { + Server(ServerOperation), + Client { + user_id: UserId, + batch_id: usize, + operation: T, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum ServerOperation { + AddConnection { + user_id: UserId, + }, + RemoveConnection { + user_id: UserId, + }, + BounceConnection { + user_id: UserId, + }, + RestartServer, + MutateClients { + batch_id: usize, + #[serde(skip_serializing)] + #[serde(skip_deserializing)] + user_ids: Vec, + quiesce: bool, + }, +} + +pub enum TestError { + Inapplicable, + Other(anyhow::Error), +} + +#[async_trait(?Send)] +pub trait RandomizedTest: 'static + Sized { + type Operation: Send + Clone + Serialize + DeserializeOwned; + + fn generate_operation( + client: &TestClient, + rng: &mut StdRng, + plan: &mut UserTestPlan, + cx: &TestAppContext, + ) -> Self::Operation; + + async fn on_client_added(client: &Rc); + + fn on_clients_quiesced(client: &[(Rc, TestAppContext)]); + + async fn apply_operation( + client: &TestClient, + operation: Self::Operation, + cx: &mut TestAppContext, + ) -> Result<(), TestError>; +} + +pub async fn run_randomized_test( + cx: &mut TestAppContext, + deterministic: Arc, + rng: StdRng, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let plan = TestPlan::::new(server.app_state.db.clone(), rng).await; + + LAST_PLAN.lock().replace({ + let plan = plan.clone(); + Box::new(move || plan.lock().serialize()) + }); + + let mut clients = Vec::new(); + let mut client_tasks = Vec::new(); + let mut operation_channels = Vec::new(); + loop { + let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { + break; + }; + applied.store(true, SeqCst); + let did_apply = TestPlan::apply_server_operation( + plan.clone(), + deterministic.clone(), + &mut server, + &mut clients, + &mut client_tasks, + &mut operation_channels, + next_operation, + cx, + ) + .await; + if !did_apply { + applied.store(false, SeqCst); + } + } + + drop(operation_channels); + deterministic.start_waiting(); + futures::future::join_all(client_tasks).await; + deterministic.finish_waiting(); + + deterministic.run_until_parked(); + T::on_clients_quiesced(&clients); + + for (client, mut cx) in clients { + cx.update(|cx| { + let store = cx.remove_global::(); + cx.clear_globals(); + cx.set_global(store); + drop(client); + }); + } + deterministic.run_until_parked(); + + if let Some(path) = &*PLAN_SAVE_PATH { + eprintln!("saved test plan to path {:?}", path); + std::fs::write(path, plan.lock().serialize()).unwrap(); + } +} + +pub fn save_randomized_test_plan() { + if let Some(serialize_plan) = LAST_PLAN.lock().take() { + if let Some(path) = &*PLAN_SAVE_PATH { + eprintln!("saved test plan to path {:?}", path); + std::fs::write(path, serialize_plan()).unwrap(); + } + } +} + +impl TestPlan { + pub async fn new(db: Arc, mut rng: StdRng) -> Arc> { + let allow_server_restarts = rng.gen_bool(0.7); + let allow_client_reconnection = rng.gen_bool(0.7); + let allow_client_disconnection = rng.gen_bool(0.1); + + let mut users = Vec::new(); + for ix in 0..*MAX_PEERS { + let username = format!("user-{}", ix + 1); + let user_id = db + .create_user( + &format!("{username}@example.com"), + false, + NewUserParams { + github_login: username.clone(), + github_user_id: (ix + 1) as i32, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + users.push(UserTestPlan { + user_id, + username, + online: false, + next_root_id: 0, + operation_ix: 0, + allow_client_disconnection, + allow_client_reconnection, + }); + } + + for (ix, user_a) in users.iter().enumerate() { + for user_b in &users[ix + 1..] { + db.send_contact_request(user_a.user_id, user_b.user_id) + .await + .unwrap(); + db.respond_to_contact_request(user_b.user_id, user_a.user_id, true) + .await + .unwrap(); + } + } + + let plan = Arc::new(Mutex::new(Self { + replay: false, + allow_server_restarts, + allow_client_reconnection, + allow_client_disconnection, + stored_operations: Vec::new(), + operation_ix: 0, + next_batch_id: 0, + max_operations: *MAX_OPERATIONS, + users, + rng, + })); + + if let Some(path) = &*PLAN_LOAD_PATH { + let json = LOADED_PLAN_JSON + .lock() + .get_or_insert_with(|| { + eprintln!("loaded test plan from path {:?}", path); + std::fs::read(path).unwrap() + }) + .clone(); + plan.lock().deserialize(json); + } + + plan + } + + fn deserialize(&mut self, json: Vec) { + let stored_operations: Vec> = + serde_json::from_slice(&json).unwrap(); + self.replay = true; + self.stored_operations = stored_operations + .iter() + .cloned() + .enumerate() + .map(|(i, mut operation)| { + let did_apply = Arc::new(AtomicBool::new(false)); + if let StoredOperation::Server(ServerOperation::MutateClients { + batch_id: current_batch_id, + user_ids, + .. + }) = &mut operation + { + assert!(user_ids.is_empty()); + user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| { + if let StoredOperation::Client { + user_id, batch_id, .. + } = operation + { + if batch_id == current_batch_id { + return Some(user_id); + } + } + None + })); + user_ids.sort_unstable(); + } + (operation, did_apply) + }) + .collect() + } + + fn serialize(&mut self) -> Vec { + // Format each operation as one line + let mut json = Vec::new(); + json.push(b'['); + for (operation, applied) in &self.stored_operations { + if !applied.load(SeqCst) { + continue; + } + if json.len() > 1 { + json.push(b','); + } + json.extend_from_slice(b"\n "); + serde_json::to_writer(&mut json, operation).unwrap(); + } + json.extend_from_slice(b"\n]\n"); + json + } + + fn next_server_operation( + &mut self, + clients: &[(Rc, TestAppContext)], + ) -> Option<(ServerOperation, Arc)> { + if self.replay { + while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) { + self.operation_ix += 1; + if let (StoredOperation::Server(operation), applied) = stored_operation { + return Some((operation.clone(), applied.clone())); + } + } + None + } else { + let operation = self.generate_server_operation(clients)?; + let applied = Arc::new(AtomicBool::new(false)); + self.stored_operations + .push((StoredOperation::Server(operation.clone()), applied.clone())); + Some((operation, applied)) + } + } + + fn next_client_operation( + &mut self, + client: &TestClient, + current_batch_id: usize, + cx: &TestAppContext, + ) -> Option<(T::Operation, Arc)> { + let current_user_id = client.current_user_id(cx); + let user_ix = self + .users + .iter() + .position(|user| user.user_id == current_user_id) + .unwrap(); + let user_plan = &mut self.users[user_ix]; + + if self.replay { + while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) { + user_plan.operation_ix += 1; + if let ( + StoredOperation::Client { + user_id, operation, .. + }, + applied, + ) = stored_operation + { + if user_id == ¤t_user_id { + return Some((operation.clone(), applied.clone())); + } + } + } + None + } else { + if self.operation_ix == self.max_operations { + return None; + } + self.operation_ix += 1; + let operation = T::generate_operation( + client, + &mut self.rng, + self.users + .iter_mut() + .find(|user| user.user_id == current_user_id) + .unwrap(), + cx, + ); + let applied = Arc::new(AtomicBool::new(false)); + self.stored_operations.push(( + StoredOperation::Client { + user_id: current_user_id, + batch_id: current_batch_id, + operation: operation.clone(), + }, + applied.clone(), + )); + Some((operation, applied)) + } + } + + fn generate_server_operation( + &mut self, + clients: &[(Rc, TestAppContext)], + ) -> Option { + if self.operation_ix == self.max_operations { + return None; + } + + Some(loop { + break match self.rng.gen_range(0..100) { + 0..=29 if clients.len() < self.users.len() => { + let user = self + .users + .iter() + .filter(|u| !u.online) + .choose(&mut self.rng) + .unwrap(); + self.operation_ix += 1; + ServerOperation::AddConnection { + user_id: user.user_id, + } + } + 30..=34 if clients.len() > 1 && self.allow_client_disconnection => { + let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; + let user_id = client.current_user_id(cx); + self.operation_ix += 1; + ServerOperation::RemoveConnection { user_id } + } + 35..=39 if clients.len() > 1 && self.allow_client_reconnection => { + let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; + let user_id = client.current_user_id(cx); + self.operation_ix += 1; + ServerOperation::BounceConnection { user_id } + } + 40..=44 if self.allow_server_restarts && clients.len() > 1 => { + self.operation_ix += 1; + ServerOperation::RestartServer + } + _ if !clients.is_empty() => { + let count = self + .rng + .gen_range(1..10) + .min(self.max_operations - self.operation_ix); + let batch_id = util::post_inc(&mut self.next_batch_id); + let mut user_ids = (0..count) + .map(|_| { + let ix = self.rng.gen_range(0..clients.len()); + let (client, cx) = &clients[ix]; + client.current_user_id(cx) + }) + .collect::>(); + user_ids.sort_unstable(); + ServerOperation::MutateClients { + user_ids, + batch_id, + quiesce: self.rng.gen_bool(0.7), + } + } + _ => continue, + }; + }) + } + + async fn apply_server_operation( + plan: Arc>, + deterministic: Arc, + server: &mut TestServer, + clients: &mut Vec<(Rc, TestAppContext)>, + client_tasks: &mut Vec>, + operation_channels: &mut Vec>, + operation: ServerOperation, + cx: &mut TestAppContext, + ) -> bool { + match operation { + ServerOperation::AddConnection { user_id } => { + let username; + { + let mut plan = plan.lock(); + let user = plan.user(user_id); + if user.online { + return false; + } + user.online = true; + username = user.username.clone(); + }; + log::info!("adding new connection for {}", username); + let next_entity_id = (user_id.0 * 10_000) as usize; + let mut client_cx = TestAppContext::new( + cx.foreground_platform(), + cx.platform(), + deterministic.build_foreground(user_id.0 as usize), + deterministic.build_background(), + cx.font_cache(), + cx.leak_detector(), + next_entity_id, + cx.function_name.clone(), + ); + + let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded(); + let client = Rc::new(server.create_client(&mut client_cx, &username).await); + operation_channels.push(operation_tx); + clients.push((client.clone(), client_cx.clone())); + client_tasks.push(client_cx.foreground().spawn(Self::simulate_client( + plan.clone(), + client, + operation_rx, + client_cx, + ))); + + log::info!("added connection for {}", username); + } + + ServerOperation::RemoveConnection { + user_id: removed_user_id, + } => { + log::info!("simulating full disconnection of user {}", removed_user_id); + let client_ix = clients + .iter() + .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); + let Some(client_ix) = client_ix else { + return false; + }; + let user_connection_ids = server + .connection_pool + .lock() + .user_connection_ids(removed_user_id) + .collect::>(); + assert_eq!(user_connection_ids.len(), 1); + let removed_peer_id = user_connection_ids[0].into(); + let (client, mut client_cx) = clients.remove(client_ix); + let client_task = client_tasks.remove(client_ix); + operation_channels.remove(client_ix); + server.forbid_connections(); + server.disconnect_client(removed_peer_id); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + deterministic.start_waiting(); + log::info!("waiting for user {} to exit...", removed_user_id); + client_task.await; + deterministic.finish_waiting(); + server.allow_connections(); + + for project in client.remote_projects().iter() { + project.read_with(&client_cx, |project, _| { + assert!( + project.is_read_only(), + "project {:?} should be read only", + project.remote_id() + ) + }); + } + + for (client, cx) in clients { + let contacts = server + .app_state + .db + .get_contacts(client.current_user_id(cx)) + .await + .unwrap(); + let pool = server.connection_pool.lock(); + for contact in contacts { + if let db::Contact::Accepted { user_id, busy, .. } = contact { + if user_id == removed_user_id { + assert!(!pool.is_user_online(user_id)); + assert!(!busy); + } + } + } + } + + log::info!("{} removed", client.username); + plan.lock().user(removed_user_id).online = false; + client_cx.update(|cx| { + cx.clear_globals(); + drop(client); + }); + } + + ServerOperation::BounceConnection { user_id } => { + log::info!("simulating temporary disconnection of user {}", user_id); + let user_connection_ids = server + .connection_pool + .lock() + .user_connection_ids(user_id) + .collect::>(); + if user_connection_ids.is_empty() { + return false; + } + assert_eq!(user_connection_ids.len(), 1); + let peer_id = user_connection_ids[0].into(); + server.disconnect_client(peer_id); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + } + + ServerOperation::RestartServer => { + log::info!("simulating server restart"); + server.reset().await; + deterministic.advance_clock(RECEIVE_TIMEOUT); + server.start().await.unwrap(); + deterministic.advance_clock(CLEANUP_TIMEOUT); + let environment = &server.app_state.config.zed_environment; + let (stale_room_ids, _) = server + .app_state + .db + .stale_server_resource_ids(environment, server.id()) + .await + .unwrap(); + assert_eq!(stale_room_ids, vec![]); + } + + ServerOperation::MutateClients { + user_ids, + batch_id, + quiesce, + } => { + let mut applied = false; + for user_id in user_ids { + let client_ix = clients + .iter() + .position(|(client, cx)| client.current_user_id(cx) == user_id); + let Some(client_ix) = client_ix else { continue }; + applied = true; + if let Err(err) = operation_channels[client_ix].unbounded_send(batch_id) { + log::error!("error signaling user {user_id}: {err}"); + } + } + + if quiesce && applied { + deterministic.run_until_parked(); + T::on_clients_quiesced(&clients); + } + + return applied; + } + } + true + } + + async fn simulate_client( + plan: Arc>, + client: Rc, + mut operation_rx: futures::channel::mpsc::UnboundedReceiver, + mut cx: TestAppContext, + ) { + T::on_client_added(&client).await; + + while let Some(batch_id) = operation_rx.next().await { + let Some((operation, applied)) = + plan.lock().next_client_operation(&client, batch_id, &cx) + else { + break; + }; + applied.store(true, SeqCst); + match T::apply_operation(&client, operation, &mut cx).await { + Ok(()) => {} + Err(TestError::Inapplicable) => { + applied.store(false, SeqCst); + log::info!("skipped operation"); + } + Err(TestError::Other(error)) => { + log::error!("{} error: {}", client.username, error); + } + } + cx.background().simulate_random_delay().await; + } + log::info!("{}: done", client.username); + } + + fn user(&mut self, user_id: UserId) -> &mut UserTestPlan { + self.users + .iter_mut() + .find(|user| user.user_id == user_id) + .unwrap() + } +} + +impl UserTestPlan { + pub fn next_root_dir_name(&mut self) -> String { + let user_id = self.user_id; + let root_id = util::post_inc(&mut self.next_root_id); + format!("dir-{user_id}-{root_id}") + } +} + +impl From for TestError { + fn from(value: anyhow::Error) -> Self { + Self::Other(value) + } +} + +fn path_env_var(name: &str) -> Option { + let value = env::var(name).ok()?; + let mut path = PathBuf::from(value); + if path.is_relative() { + let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + abs_path.pop(); + abs_path.pop(); + abs_path.push(path); + path = abs_path + } + Some(path) +} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs new file mode 100644 index 0000000000..44f6ac1450 --- /dev/null +++ b/crates/collab/src/tests/test_server.rs @@ -0,0 +1,551 @@ +use crate::{ + db::{tests::TestDb, NewUserParams, UserId}, + executor::Executor, + rpc::{Server, CLEANUP_TIMEOUT}, + AppState, +}; +use anyhow::anyhow; +use call::ActiveCall; +use channel::ChannelStore; +use client::{ + self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, +}; +use collections::{HashMap, HashSet}; +use fs::FakeFs; +use futures::{channel::oneshot, StreamExt as _}; +use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; +use language::LanguageRegistry; +use parking_lot::Mutex; +use project::{Project, WorktreeId}; +use settings::SettingsStore; +use std::{ + cell::{Ref, RefCell, RefMut}, + env, + ops::{Deref, DerefMut}, + path::Path, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, + Arc, + }, +}; +use util::http::FakeHttpClient; +use workspace::Workspace; + +pub struct TestServer { + pub app_state: Arc, + pub test_live_kit_server: Arc, + server: Arc, + connection_killers: Arc>>>, + forbid_connections: Arc, + _test_db: TestDb, +} + +pub struct TestClient { + pub username: String, + pub app_state: Arc, + state: RefCell, +} + +#[derive(Default)] +struct TestClientState { + local_projects: Vec>, + remote_projects: Vec>, + buffers: HashMap, HashSet>>, +} + +pub struct ContactsSummary { + pub current: Vec, + pub outgoing_requests: Vec, + pub incoming_requests: Vec, +} + +impl TestServer { + pub async fn start(deterministic: &Arc) -> Self { + static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0); + + let use_postgres = env::var("USE_POSTGRES").ok(); + let use_postgres = use_postgres.as_deref(); + let test_db = if use_postgres == Some("true") || use_postgres == Some("1") { + TestDb::postgres(deterministic.build_background()) + } else { + TestDb::sqlite(deterministic.build_background()) + }; + let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst); + let live_kit_server = live_kit_client::TestServer::create( + format!("http://livekit.{}.test", live_kit_server_id), + format!("devkey-{}", live_kit_server_id), + format!("secret-{}", live_kit_server_id), + deterministic.build_background(), + ) + .unwrap(); + let app_state = Self::build_app_state(&test_db, &live_kit_server).await; + let epoch = app_state + .db + .create_server(&app_state.config.zed_environment) + .await + .unwrap(); + let server = Server::new( + epoch, + app_state.clone(), + Executor::Deterministic(deterministic.build_background()), + ); + server.start().await.unwrap(); + // Advance clock to ensure the server's cleanup task is finished. + deterministic.advance_clock(CLEANUP_TIMEOUT); + Self { + app_state, + server, + connection_killers: Default::default(), + forbid_connections: Default::default(), + _test_db: test_db, + test_live_kit_server: live_kit_server, + } + } + + pub async fn reset(&self) { + self.app_state.db.reset(); + let epoch = self + .app_state + .db + .create_server(&self.app_state.config.zed_environment) + .await + .unwrap(); + self.server.reset(epoch); + } + + pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { + cx.update(|cx| { + if cx.has_global::() { + panic!("Same cx used to create two test clients") + } + cx.set_global(SettingsStore::test(cx)); + }); + + let http = FakeHttpClient::with_404_response(); + let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await + { + user.id + } else { + self.app_state + .db + .create_user( + &format!("{name}@example.com"), + false, + NewUserParams { + github_login: name.into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .expect("creating user failed") + .user_id + }; + let client_name = name.to_string(); + let mut client = cx.read(|cx| Client::new(http.clone(), cx)); + let server = self.server.clone(); + let db = self.app_state.db.clone(); + let connection_killers = self.connection_killers.clone(); + let forbid_connections = self.forbid_connections.clone(); + + Arc::get_mut(&mut client) + .unwrap() + .set_id(user_id.0 as usize) + .override_authenticate(move |cx| { + cx.spawn(|_| async move { + let access_token = "the-token".to_string(); + Ok(Credentials { + user_id: user_id.0 as u64, + access_token, + }) + }) + }) + .override_establish_connection(move |credentials, cx| { + assert_eq!(credentials.user_id, user_id.0 as u64); + assert_eq!(credentials.access_token, "the-token"); + + let server = server.clone(); + let db = db.clone(); + let connection_killers = connection_killers.clone(); + let forbid_connections = forbid_connections.clone(); + let client_name = client_name.clone(); + cx.spawn(move |cx| async move { + if forbid_connections.load(SeqCst) { + Err(EstablishConnectionError::other(anyhow!( + "server is forbidding connections" + ))) + } else { + let (client_conn, server_conn, killed) = + Connection::in_memory(cx.background()); + let (connection_id_tx, connection_id_rx) = oneshot::channel(); + let user = db + .get_user_by_id(user_id) + .await + .expect("retrieving user failed") + .unwrap(); + cx.background() + .spawn(server.handle_connection( + server_conn, + client_name, + user, + Some(connection_id_tx), + Executor::Deterministic(cx.background()), + )) + .detach(); + let connection_id = connection_id_rx.await.unwrap(); + connection_killers + .lock() + .insert(connection_id.into(), killed); + Ok(client_conn) + } + }) + }); + + let fs = FakeFs::new(cx.background()); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + let channel_store = + cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + let app_state = Arc::new(workspace::AppState { + client: client.clone(), + user_store: user_store.clone(), + channel_store: channel_store.clone(), + languages: Arc::new(LanguageRegistry::test()), + fs: fs.clone(), + build_window_options: |_, _, _| Default::default(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), + background_actions: || &[], + }); + + cx.update(|cx| { + theme::init((), cx); + Project::init(&client, cx); + client::init(&client, cx); + language::init(cx); + editor::init_settings(cx); + workspace::init(app_state.clone(), cx); + audio::init((), cx); + call::init(client.clone(), user_store.clone(), cx); + channel::init(&client); + }); + + client + .authenticate_and_connect(false, &cx.to_async()) + .await + .unwrap(); + + let client = TestClient { + app_state, + username: name.to_string(), + state: Default::default(), + }; + client.wait_for_current_user(cx).await; + client + } + + pub fn disconnect_client(&self, peer_id: PeerId) { + self.connection_killers + .lock() + .remove(&peer_id) + .unwrap() + .store(true, SeqCst); + } + + pub fn forbid_connections(&self) { + self.forbid_connections.store(true, SeqCst); + } + + pub fn allow_connections(&self) { + self.forbid_connections.store(false, SeqCst); + } + + pub async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { + for ix in 1..clients.len() { + let (left, right) = clients.split_at_mut(ix); + let (client_a, cx_a) = left.last_mut().unwrap(); + for (client_b, cx_b) in right { + client_a + .app_state + .user_store + .update(*cx_a, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + client_b + .app_state + .user_store + .update(*cx_b, |store, cx| { + store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) + }) + .await + .unwrap(); + } + } + } + + pub async fn make_channel( + &self, + channel: &str, + admin: (&TestClient, &mut TestAppContext), + members: &mut [(&TestClient, &mut TestAppContext)], + ) -> u64 { + let (admin_client, admin_cx) = admin; + let channel_id = admin_client + .app_state + .channel_store + .update(admin_cx, |channel_store, cx| { + channel_store.create_channel(channel, None, cx) + }) + .await + .unwrap(); + + for (member_client, member_cx) in members { + admin_client + .app_state + .channel_store + .update(admin_cx, |channel_store, cx| { + channel_store.invite_member( + channel_id, + member_client.user_id().unwrap(), + false, + cx, + ) + }) + .await + .unwrap(); + + admin_cx.foreground().run_until_parked(); + + member_client + .app_state + .channel_store + .update(*member_cx, |channels, _| { + channels.respond_to_channel_invite(channel_id, true) + }) + .await + .unwrap(); + } + + channel_id + } + + pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { + self.make_contacts(clients).await; + + let (left, right) = clients.split_at_mut(1); + let (_client_a, cx_a) = &mut left[0]; + let active_call_a = cx_a.read(ActiveCall::global); + + for (client_b, cx_b) in right { + let user_id_b = client_b.current_user_id(*cx_b).to_proto(); + active_call_a + .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx)) + .await + .unwrap(); + + cx_b.foreground().run_until_parked(); + let active_call_b = cx_b.read(ActiveCall::global); + active_call_b + .update(*cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + } + } + + pub async fn build_app_state( + test_db: &TestDb, + fake_server: &live_kit_client::TestServer, + ) -> Arc { + Arc::new(AppState { + db: test_db.db().clone(), + live_kit_client: Some(Arc::new(fake_server.create_api_client())), + config: Default::default(), + }) + } +} + +impl Deref for TestServer { + type Target = Server; + + fn deref(&self) -> &Self::Target { + &self.server + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + self.server.teardown(); + self.test_live_kit_server.teardown().unwrap(); + } +} + +impl Deref for TestClient { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.app_state.client + } +} + +impl TestClient { + pub fn fs(&self) -> &FakeFs { + self.app_state.fs.as_fake() + } + + pub fn channel_store(&self) -> &ModelHandle { + &self.app_state.channel_store + } + + pub fn user_store(&self) -> &ModelHandle { + &self.app_state.user_store + } + + pub fn language_registry(&self) -> &Arc { + &self.app_state.languages + } + + pub fn client(&self) -> &Arc { + &self.app_state.client + } + + pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { + UserId::from_proto( + self.app_state + .user_store + .read_with(cx, |user_store, _| user_store.current_user().unwrap().id), + ) + } + + pub async fn wait_for_current_user(&self, cx: &TestAppContext) { + let mut authed_user = self + .app_state + .user_store + .read_with(cx, |user_store, _| user_store.watch_current_user()); + while authed_user.next().await.unwrap().is_none() {} + } + + pub async fn clear_contacts(&self, cx: &mut TestAppContext) { + self.app_state + .user_store + .update(cx, |store, _| store.clear_contacts()) + .await; + } + + pub fn local_projects<'a>(&'a self) -> impl Deref>> + 'a { + Ref::map(self.state.borrow(), |state| &state.local_projects) + } + + pub fn remote_projects<'a>(&'a self) -> impl Deref>> + 'a { + Ref::map(self.state.borrow(), |state| &state.remote_projects) + } + + pub fn local_projects_mut<'a>( + &'a self, + ) -> impl DerefMut>> + 'a { + RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects) + } + + pub fn remote_projects_mut<'a>( + &'a self, + ) -> impl DerefMut>> + 'a { + RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects) + } + + pub fn buffers_for_project<'a>( + &'a self, + project: &ModelHandle, + ) -> impl DerefMut>> + 'a { + RefMut::map(self.state.borrow_mut(), |state| { + state.buffers.entry(project.clone()).or_default() + }) + } + + pub fn buffers<'a>( + &'a self, + ) -> impl DerefMut, HashSet>>> + 'a + { + RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers) + } + + pub fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { + self.app_state + .user_store + .read_with(cx, |store, _| ContactsSummary { + current: store + .contacts() + .iter() + .map(|contact| contact.user.github_login.clone()) + .collect(), + outgoing_requests: store + .outgoing_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), + incoming_requests: store + .incoming_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), + }) + } + + pub async fn build_local_project( + &self, + root_path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, WorktreeId) { + let project = cx.update(|cx| { + Project::local( + self.client().clone(), + self.app_state.user_store.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), + cx, + ) + }); + let (worktree, _) = project + .update(cx, |p, cx| { + p.find_or_create_local_worktree(root_path, true, cx) + }) + .await + .unwrap(); + worktree + .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + (project, worktree.read_with(cx, |tree, _| tree.id())) + } + + pub async fn build_remote_project( + &self, + host_project_id: u64, + guest_cx: &mut TestAppContext, + ) -> ModelHandle { + let active_call = guest_cx.read(ActiveCall::global); + let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone()); + room.update(guest_cx, |room, cx| { + room.join_project( + host_project_id, + self.app_state.languages.clone(), + self.app_state.fs.clone(), + cx, + ) + }) + .await + .unwrap() + } + + pub fn build_workspace( + &self, + project: &ModelHandle, + cx: &mut TestAppContext, + ) -> WindowHandle { + cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) + } +} + +impl Drop for TestClient { + fn drop(&mut self) { + self.app_state.client.teardown(); + } +} diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index 7f70bc6a91..16f293afbe 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -37,8 +37,14 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { Some("seed") => starting_seed = parse_int(&meta.lit)?, Some("on_failure") => { if let Lit::Str(name) = meta.lit { - let ident = Ident::new(&name.value(), name.span()); - on_failure_fn_name = quote!(Some(#ident)); + let mut path = syn::Path { + leading_colon: None, + segments: Default::default(), + }; + for part in name.value().split("::") { + path.segments.push(Ident::new(part, name.span()).into()); + } + on_failure_fn_name = quote!(Some(#path)); } else { return Err(TokenStream::from( syn::Error::new( From e779adfe468fca4a6fd81435eec5f717846aeb52 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Sep 2023 14:09:36 -0700 Subject: [PATCH 152/156] Add basic randomized integration test for channel notes --- crates/channel/src/channel_store.rs | 11 +- crates/collab/src/db/queries/channels.rs | 14 ++ .../src/tests/random_channel_buffer_tests.rs | 237 +++++++++++++++++- .../random_project_collaboration_tests.rs | 26 +- .../src/tests/randomized_test_helpers.rs | 37 ++- crates/collab/src/tests/test_server.rs | 9 +- 6 files changed, 295 insertions(+), 39 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 3d2f61d61f..a4c8da6df4 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; -use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use std::{mem, sync::Arc, time::Duration}; use util::ResultExt; @@ -152,6 +152,15 @@ impl ChannelStore { self.channels_by_id.get(&channel_id) } + pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool { + if let Some(buffer) = self.opened_buffers.get(&channel_id) { + if let OpenedChannelBuffer::Open(buffer) = buffer { + return buffer.upgrade(cx).is_some(); + } + } + false + } + pub fn open_channel_buffer( &mut self, channel_id: ChannelId, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index e3d3643a61..5da4dd1464 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,6 +1,20 @@ use super::*; impl Database { + #[cfg(test)] + pub async fn all_channels(&self) -> Result> { + self.transaction(move |tx| async move { + let mut channels = Vec::new(); + let mut rows = channel::Entity::find().stream(&*tx).await?; + while let Some(row) = rows.next().await { + let row = row?; + channels.push((row.id, row.name)); + } + Ok(channels) + }) + .await + } + pub async fn create_root_channel( &self, name: &str, diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 929e567977..933683eaa6 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -1,12 +1,16 @@ -use crate::tests::{run_randomized_test, RandomizedTest, TestClient, TestError, UserTestPlan}; +use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; use anyhow::Result; use async_trait::async_trait; use gpui::{executor::Deterministic, TestAppContext}; -use rand::rngs::StdRng; +use rand::prelude::*; use serde_derive::{Deserialize, Serialize}; -use std::{rc::Rc, sync::Arc}; +use std::{ops::Range, rc::Rc, sync::Arc}; +use text::Bias; -#[gpui::test] +#[gpui::test( + iterations = 100, + on_failure = "crate::tests::save_randomized_test_plan" +)] async fn test_random_channel_buffers( cx: &mut TestAppContext, deterministic: Arc, @@ -19,20 +23,105 @@ struct RandomChannelBufferTest; #[derive(Clone, Serialize, Deserialize)] enum ChannelBufferOperation { - Join, + JoinChannelNotes { + channel_name: String, + }, + LeaveChannelNotes { + channel_name: String, + }, + EditChannelNotes { + channel_name: String, + edits: Vec<(Range, Arc)>, + }, + Noop, } +const CHANNEL_COUNT: usize = 3; + #[async_trait(?Send)] impl RandomizedTest for RandomChannelBufferTest { type Operation = ChannelBufferOperation; + async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) { + let db = &server.app_state.db; + for ix in 0..CHANNEL_COUNT { + let id = db + .create_channel( + &format!("channel-{ix}"), + None, + &format!("livekit-room-{ix}"), + users[0].user_id, + ) + .await + .unwrap(); + for user in &users[1..] { + db.invite_channel_member(id, user.user_id, users[0].user_id, false) + .await + .unwrap(); + db.respond_to_channel_invite(id, user.user_id, true) + .await + .unwrap(); + } + } + } + fn generate_operation( client: &TestClient, rng: &mut StdRng, - plan: &mut UserTestPlan, + _: &mut UserTestPlan, cx: &TestAppContext, ) -> ChannelBufferOperation { - ChannelBufferOperation::Join + let channel_store = client.channel_store().clone(); + let channel_buffers = client.channel_buffers(); + + // When signed out, we can't do anything unless a channel buffer is + // already open. + if channel_buffers.is_empty() + && channel_store.read_with(cx, |store, _| store.channel_count() == 0) + { + return ChannelBufferOperation::Noop; + } + + loop { + match rng.gen_range(0..100_u32) { + 0..=29 => { + let channel_name = client.channel_store().read_with(cx, |store, cx| { + store.channels().find_map(|(_, channel)| { + if store.has_open_channel_buffer(channel.id, cx) { + None + } else { + Some(channel.name.clone()) + } + }) + }); + if let Some(channel_name) = channel_name { + break ChannelBufferOperation::JoinChannelNotes { channel_name }; + } + } + + 30..=40 => { + if let Some(buffer) = channel_buffers.iter().choose(rng) { + let channel_name = buffer.read_with(cx, |b, _| b.channel().name.clone()); + break ChannelBufferOperation::LeaveChannelNotes { channel_name }; + } + } + + _ => { + if let Some(buffer) = channel_buffers.iter().choose(rng) { + break buffer.read_with(cx, |b, _| { + let channel_name = b.channel().name.clone(); + let edits = b + .buffer() + .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); + ChannelBufferOperation::EditChannelNotes { + channel_name, + edits, + } + }); + } + } + } + } } async fn apply_operation( @@ -40,10 +129,140 @@ impl RandomizedTest for RandomChannelBufferTest { operation: ChannelBufferOperation, cx: &mut TestAppContext, ) -> Result<(), TestError> { + match operation { + ChannelBufferOperation::JoinChannelNotes { channel_name } => { + let buffer = client.channel_store().update(cx, |store, cx| { + let channel_id = store + .channels() + .find(|(_, c)| c.name == channel_name) + .unwrap() + .1 + .id; + if store.has_open_channel_buffer(channel_id, cx) { + Err(TestError::Inapplicable) + } else { + Ok(store.open_channel_buffer(channel_id, cx)) + } + })?; + + log::info!( + "{}: opening notes for channel {channel_name}", + client.username + ); + client.channel_buffers().insert(buffer.await?); + } + + ChannelBufferOperation::LeaveChannelNotes { channel_name } => { + let buffer = cx.update(|cx| { + let mut left_buffer = Err(TestError::Inapplicable); + client.channel_buffers().retain(|buffer| { + if buffer.read(cx).channel().name == channel_name { + left_buffer = Ok(buffer.clone()); + false + } else { + true + } + }); + left_buffer + })?; + + log::info!( + "{}: closing notes for channel {channel_name}", + client.username + ); + cx.update(|_| drop(buffer)); + } + + ChannelBufferOperation::EditChannelNotes { + channel_name, + edits, + } => { + let channel_buffer = cx + .read(|cx| { + client + .channel_buffers() + .iter() + .find(|buffer| buffer.read(cx).channel().name == channel_name) + .cloned() + }) + .ok_or_else(|| TestError::Inapplicable)?; + + log::info!( + "{}: editing notes for channel {channel_name} with {:?}", + client.username, + edits + ); + + channel_buffer.update(cx, |buffer, cx| { + let buffer = buffer.buffer(); + buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(); + buffer.edit( + edits.into_iter().map(|(range, text)| { + let start = snapshot.clip_offset(range.start, Bias::Left); + let end = snapshot.clip_offset(range.end, Bias::Right); + (start..end, text) + }), + None, + cx, + ); + }); + }); + } + + ChannelBufferOperation::Noop => Err(TestError::Inapplicable)?, + } Ok(()) } - async fn on_client_added(client: &Rc) {} + async fn on_client_added(client: &Rc, cx: &mut TestAppContext) { + let channel_store = client.channel_store(); + while channel_store.read_with(cx, |store, _| store.channel_count() == 0) { + channel_store.next_notification(cx).await; + } + } - fn on_clients_quiesced(clients: &[(Rc, TestAppContext)]) {} + async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { + let channels = server.app_state.db.all_channels().await.unwrap(); + + for (channel_id, channel_name) in channels { + let mut collaborator_user_ids = server + .app_state + .db + .get_channel_buffer_collaborators(channel_id) + .await + .unwrap() + .into_iter() + .map(|id| id.to_proto()) + .collect::>(); + collaborator_user_ids.sort(); + + for (client, client_cx) in clients.iter_mut() { + client_cx.update(|cx| { + client + .channel_buffers() + .retain(|b| b.read(cx).is_connected()); + + if let Some(channel_buffer) = client + .channel_buffers() + .iter() + .find(|b| b.read(cx).channel().id == channel_id.to_proto()) + { + let channel_buffer = channel_buffer.read(cx); + let collaborators = channel_buffer.collaborators(); + let mut user_ids = + collaborators.iter().map(|c| c.user_id).collect::>(); + user_ids.sort(); + assert_eq!( + user_ids, + collaborator_user_ids, + "client {} has different user ids for channel {} than the server", + client.user_id().unwrap(), + channel_name + ); + } + }); + } + } + } } diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 242cfbc162..7570768249 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1,7 +1,5 @@ -use crate::{ - db::UserId, - tests::{run_randomized_test, RandomizedTest, TestClient, TestError, UserTestPlan}, -}; +use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; +use crate::db::UserId; use anyhow::{anyhow, Result}; use async_trait::async_trait; use call::ActiveCall; @@ -145,6 +143,20 @@ struct ProjectCollaborationTest; impl RandomizedTest for ProjectCollaborationTest { type Operation = ClientOperation; + async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) { + let db = &server.app_state.db; + for (ix, user_a) in users.iter().enumerate() { + for user_b in &users[ix + 1..] { + db.send_contact_request(user_a.user_id, user_b.user_id) + .await + .unwrap(); + db.respond_to_contact_request(user_b.user_id, user_a.user_id, true) + .await + .unwrap(); + } + } + } + fn generate_operation( client: &TestClient, rng: &mut StdRng, @@ -1005,7 +1017,7 @@ impl RandomizedTest for ProjectCollaborationTest { Ok(()) } - async fn on_client_added(client: &Rc) { + async fn on_client_added(client: &Rc, _: &mut TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), @@ -1119,8 +1131,8 @@ impl RandomizedTest for ProjectCollaborationTest { client.app_state.languages.add(Arc::new(language)); } - fn on_clients_quiesced(clients: &[(Rc, TestAppContext)]) { - for (client, client_cx) in clients { + async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { + for (client, client_cx) in clients.iter() { for guest_project in client.remote_projects().iter() { guest_project.read_with(client_cx, |guest_project, cx| { let host_project = clients.iter().find_map(|(client, cx)| { diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index dc102b75c6..39598bdaf9 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -1,5 +1,5 @@ use crate::{ - db::{self, Database, NewUserParams, UserId}, + db::{self, NewUserParams, UserId}, rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, tests::{TestClient, TestServer}, }; @@ -107,15 +107,17 @@ pub trait RandomizedTest: 'static + Sized { cx: &TestAppContext, ) -> Self::Operation; - async fn on_client_added(client: &Rc); - - fn on_clients_quiesced(client: &[(Rc, TestAppContext)]); - async fn apply_operation( client: &TestClient, operation: Self::Operation, cx: &mut TestAppContext, ) -> Result<(), TestError>; + + async fn initialize(server: &mut TestServer, users: &[UserTestPlan]); + + async fn on_client_added(client: &Rc, cx: &mut TestAppContext); + + async fn on_quiesce(server: &mut TestServer, client: &mut [(Rc, TestAppContext)]); } pub async fn run_randomized_test( @@ -125,7 +127,7 @@ pub async fn run_randomized_test( ) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; - let plan = TestPlan::::new(server.app_state.db.clone(), rng).await; + let plan = TestPlan::::new(&mut server, rng).await; LAST_PLAN.lock().replace({ let plan = plan.clone(); @@ -162,7 +164,7 @@ pub async fn run_randomized_test( deterministic.finish_waiting(); deterministic.run_until_parked(); - T::on_clients_quiesced(&clients); + T::on_quiesce(&mut server, &mut clients).await; for (client, mut cx) in clients { cx.update(|cx| { @@ -190,7 +192,7 @@ pub fn save_randomized_test_plan() { } impl TestPlan { - pub async fn new(db: Arc, mut rng: StdRng) -> Arc> { + pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc> { let allow_server_restarts = rng.gen_bool(0.7); let allow_client_reconnection = rng.gen_bool(0.7); let allow_client_disconnection = rng.gen_bool(0.1); @@ -198,7 +200,9 @@ impl TestPlan { let mut users = Vec::new(); for ix in 0..*MAX_PEERS { let username = format!("user-{}", ix + 1); - let user_id = db + let user_id = server + .app_state + .db .create_user( &format!("{username}@example.com"), false, @@ -222,16 +226,7 @@ impl TestPlan { }); } - for (ix, user_a) in users.iter().enumerate() { - for user_b in &users[ix + 1..] { - db.send_contact_request(user_a.user_id, user_b.user_id) - .await - .unwrap(); - db.respond_to_contact_request(user_b.user_id, user_a.user_id, true) - .await - .unwrap(); - } - } + T::initialize(server, &users).await; let plan = Arc::new(Mutex::new(Self { replay: false, @@ -619,7 +614,7 @@ impl TestPlan { if quiesce && applied { deterministic.run_until_parked(); - T::on_clients_quiesced(&clients); + T::on_quiesce(server, clients).await; } return applied; @@ -634,7 +629,7 @@ impl TestPlan { mut operation_rx: futures::channel::mpsc::UnboundedReceiver, mut cx: TestAppContext, ) { - T::on_client_added(&client).await; + T::on_client_added(&client, &mut cx).await; while let Some(batch_id) = operation_rx.next().await { let Some((operation, applied)) = diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 44f6ac1450..eef1dde967 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::anyhow; use call::ActiveCall; -use channel::ChannelStore; +use channel::{channel_buffer::ChannelBuffer, ChannelStore}; use client::{ self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; @@ -51,6 +51,7 @@ struct TestClientState { local_projects: Vec>, remote_projects: Vec>, buffers: HashMap, HashSet>>, + channel_buffers: HashSet>, } pub struct ContactsSummary { @@ -468,6 +469,12 @@ impl TestClient { RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers) } + pub fn channel_buffers<'a>( + &'a self, + ) -> impl DerefMut>> + 'a { + RefMut::map(self.state.borrow_mut(), |state| &mut state.channel_buffers) + } + pub fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { self.app_state .user_store From b75e69d31b5874d2fe3fb0b69008a0d40349b41d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Sep 2023 14:25:07 -0700 Subject: [PATCH 153/156] Check that channel notes text converges in randomized test --- .../src/tests/random_channel_buffer_tests.rs | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 933683eaa6..a60d3d7d7d 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -225,7 +225,17 @@ impl RandomizedTest for RandomChannelBufferTest { async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { let channels = server.app_state.db.all_channels().await.unwrap(); + for (client, client_cx) in clients.iter_mut() { + client_cx.update(|cx| { + client + .channel_buffers() + .retain(|b| b.read(cx).is_connected()); + }); + } + for (channel_id, channel_name) in channels { + let mut prev_text: Option<(u64, String)> = None; + let mut collaborator_user_ids = server .app_state .db @@ -237,18 +247,30 @@ impl RandomizedTest for RandomChannelBufferTest { .collect::>(); collaborator_user_ids.sort(); - for (client, client_cx) in clients.iter_mut() { - client_cx.update(|cx| { - client - .channel_buffers() - .retain(|b| b.read(cx).is_connected()); - + for (client, client_cx) in clients.iter() { + let user_id = client.user_id().unwrap(); + client_cx.read(|cx| { if let Some(channel_buffer) = client .channel_buffers() .iter() .find(|b| b.read(cx).channel().id == channel_id.to_proto()) { let channel_buffer = channel_buffer.read(cx); + + // Assert that channel buffer's text matches other clients' copies. + let text = channel_buffer.buffer().read(cx).text(); + if let Some((prev_user_id, prev_text)) = &prev_text { + assert_eq!( + &text, + prev_text, + "client {user_id} has different text than client {prev_user_id} for channel {channel_name}", + ); + } else { + prev_text = Some((user_id, text.clone())); + } + + // Assert that all clients and the server agree about who is present in the + // channel buffer. let collaborators = channel_buffer.collaborators(); let mut user_ids = collaborators.iter().map(|c| c.user_id).collect::>(); @@ -256,9 +278,7 @@ impl RandomizedTest for RandomChannelBufferTest { assert_eq!( user_ids, collaborator_user_ids, - "client {} has different user ids for channel {} than the server", - client.user_id().unwrap(), - channel_name + "client {user_id} has different user ids for channel {channel_name} than the server", ); } }); From ed2aed4f93c7549252cba5ac533b15bd03e00766 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Sep 2023 14:29:11 -0700 Subject: [PATCH 154/156] Update test name in randomized-test-minimize script --- script/randomized-test-minimize | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/script/randomized-test-minimize b/script/randomized-test-minimize index ce0b7203b4..df003cbf3e 100755 --- a/script/randomized-test-minimize +++ b/script/randomized-test-minimize @@ -9,7 +9,6 @@ const CARGO_TEST_ARGS = [ '--release', '--lib', '--package', 'collab', - 'random_collaboration', ] if (require.main === module) { @@ -99,7 +98,7 @@ function buildTests() { } function runTests(env) { - const {status, stdout} = spawnSync('cargo', ['test', ...CARGO_TEST_ARGS], { + const {status, stdout} = spawnSync('cargo', ['test', ...CARGO_TEST_ARGS, 'random_project_collaboration'], { stdio: 'pipe', encoding: 'utf8', env: { From 58f58a629b86d5659bdf5ce6dc1b96c08104b6d6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Sep 2023 14:58:25 -0700 Subject: [PATCH 155/156] Tolerate channel buffer operations being re-sent --- crates/collab/src/db/queries/buffers.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 8236eb9c3b..00de201403 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -380,6 +380,16 @@ impl Database { .collect::>(); if !operations.is_empty() { buffer_operation::Entity::insert_many(operations) + .on_conflict( + OnConflict::columns([ + buffer_operation::Column::BufferId, + buffer_operation::Column::Epoch, + buffer_operation::Column::LamportTimestamp, + buffer_operation::Column::ReplicaId, + ]) + .do_nothing() + .to_owned(), + ) .exec(&*tx) .await?; } From 39e13b667554691f7685c5b0e07dc8e0a479e6ef Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Sep 2023 22:53:05 -0400 Subject: [PATCH 156/156] Allow call events to be logged without a room id --- crates/call/src/call.rs | 30 ++++++++++++++++------------ crates/client/src/telemetry.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 18 +++++++++++++---- crates/collab_ui/src/collab_ui.rs | 8 ++++---- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 5886462ccf..4db298fe98 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -273,7 +273,13 @@ impl ActiveCall { .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - Self::report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); + Self::report_call_event_for_room( + "decline incoming", + Some(call.room_id), + None, + &self.client, + cx, + ); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -403,22 +409,20 @@ impl ActiveCall { &self.pending_invites } - pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { - if let Some(room) = self.room() { - let room = room.read(cx); - Self::report_call_event_for_room( - operation, - room.id(), - room.channel_id(), - &self.client, - cx, - ) - } + fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + let (room_id, channel_id) = match self.room() { + Some(room) => { + let room = room.read(cx); + (Some(room.id()), room.channel_id()) + } + None => (None, None), + }; + Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx) } pub fn report_call_event_for_room( operation: &'static str, - room_id: u64, + room_id: Option, channel_id: Option, client: &Arc, cx: &AppContext, diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 9cc5d13af0..f8642dd7fa 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -73,7 +73,7 @@ pub enum ClickhouseEvent { }, Call { operation: &'static str, - room_id: u64, + room_id: Option, channel_id: Option, }, } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d27cdc8acf..fba10c61ba 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2240,7 +2240,8 @@ impl CollabPanel { fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { let pane = workspace.read(cx).active_pane().clone(); - let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx); + let channel_id = action.channel_id; + let channel_view = ChannelView::open(channel_id, pane.clone(), workspace, cx); cx.spawn(|_, mut cx| async move { let channel_view = channel_view.await?; pane.update(&mut cx, |pane, cx| { @@ -2249,9 +2250,18 @@ impl CollabPanel { anyhow::Ok(()) }) .detach(); - ActiveCall::global(cx).update(cx, |call, cx| { - call.report_call_event("open channel notes", cx) - }); + let room_id = ActiveCall::global(cx) + .read(cx) + .room() + .map(|room| room.read(cx).id()); + + ActiveCall::report_call_event_for_room( + "open channel notes", + room_id, + Some(channel_id), + &self.client, + cx, + ); } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 04644b62d9..ee34f600fa 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -49,7 +49,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { if room.is_screen_sharing() { ActiveCall::report_call_event_for_room( "disable screen share", - room.id(), + Some(room.id()), room.channel_id(), &client, cx, @@ -58,7 +58,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { } else { ActiveCall::report_call_event_for_room( "enable screen share", - room.id(), + Some(room.id()), room.channel_id(), &client, cx, @@ -78,7 +78,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { if room.is_muted(cx) { ActiveCall::report_call_event_for_room( "enable microphone", - room.id(), + Some(room.id()), room.channel_id(), &client, cx, @@ -86,7 +86,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { } else { ActiveCall::report_call_event_for_room( "disable microphone", - room.id(), + Some(room.id()), room.channel_id(), &client, cx,