diff --git a/Cargo.lock b/Cargo.lock index 3db82b3cb335344dd32092480f7d057031104e21..c4a7112abd41bc7ee3621f0f4d8fa88b026e852f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -727,6 +727,7 @@ dependencies = [ "editor", "gpui", "language", + "project", "search", "theme", "workspace", @@ -5428,6 +5429,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-typescript" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "ttf-parser" version = "0.9.0" @@ -5970,7 +5981,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "zed" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "async-compression", @@ -6039,6 +6050,7 @@ dependencies = [ "tree-sitter-json", "tree-sitter-markdown", "tree-sitter-rust", + "tree-sitter-typescript", "unindent", "url", "util", diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 2e74fd2090dedaf165c15e950bfe113e8d9c392b..7dbafdb3be560805d18df4fe47a4c0b7c605b95a 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -12,6 +12,7 @@ collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } +project = { path = "../project" } search = { path = "../search" } theme = { path = "../theme" } workspace = { path = "../workspace" } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index ce32fb22723833fdf86c5ec2c49c155f34368b2a..59c8b08b685270576967492f3f0c87d659a5799b 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -1,10 +1,11 @@ use editor::{Anchor, Editor}; use gpui::{ - elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle, + elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext, + ViewHandle, }; -use language::{BufferSnapshot, OutlineItem}; +use language::{Buffer, OutlineItem}; +use project::Project; use search::ProjectSearchView; -use std::borrow::Cow; use theme::SyntaxTheme; use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView}; @@ -13,14 +14,16 @@ pub enum Event { } pub struct Breadcrumbs { + project: ModelHandle, editor: Option>, project_search: Option>, subscriptions: Vec, } impl Breadcrumbs { - pub fn new() -> Self { + pub fn new(project: ModelHandle) -> Self { Self { + project, editor: Default::default(), subscriptions: Default::default(), project_search: Default::default(), @@ -31,14 +34,14 @@ impl Breadcrumbs { &self, theme: &SyntaxTheme, cx: &AppContext, - ) -> Option<(BufferSnapshot, Vec>)> { + ) -> Option<(ModelHandle, Vec>)> { let editor = self.editor.as_ref()?.read(cx); let cursor = editor.newest_anchor_selection().head(); - let (buffer, symbols) = editor - .buffer() - .read(cx) + let multibuffer = &editor.buffer().read(cx); + let (buffer_id, symbols) = multibuffer .read(cx) .symbols_containing(cursor, Some(theme))?; + let buffer = multibuffer.buffer(buffer_id)?; Some((buffer, symbols)) } } @@ -60,15 +63,21 @@ impl View for Breadcrumbs { } else { return Empty::new().boxed(); }; - - let filename = if let Some(path) = buffer.path() { - path.to_string_lossy() + let buffer = buffer.read(cx); + let filename = if let Some(file) = buffer.file() { + if file.path().file_name().is_none() + || self.project.read(cx).visible_worktrees(cx).count() > 1 + { + file.full_path(cx).to_string_lossy().to_string() + } else { + file.path().to_string_lossy().to_string() + } } else { - Cow::Borrowed("untitled") + "untitled".to_string() }; Flex::row() - .with_child(Label::new(filename.to_string(), theme.breadcrumbs.text.clone()).boxed()) + .with_child(Label::new(filename, theme.breadcrumbs.text.clone()).boxed()) .with_children(symbols.into_iter().flat_map(|symbol| { [ Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed(), @@ -99,7 +108,10 @@ impl ToolbarItemView for Breadcrumbs { if let Some(editor) = item.act_as::(cx) { self.subscriptions .push(cx.subscribe(&editor, |_, _, event, cx| match event { - editor::Event::BufferEdited => cx.notify(), + editor::Event::BufferEdited + | editor::Event::TitleChanged + | editor::Event::Saved + | editor::Event::Reparsed => cx.notify(), editor::Event::SelectionsChanged { local } if *local => cx.notify(), _ => {} })); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 56de434cf49e73c82ac19eefd7be36bdd3f5c71e..da50e99f1e5175d04900c4d0a4839479e1dd82c7 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -478,6 +478,14 @@ impl workspace::Item for ProjectDiagnosticsEditor { self.editor.save(project, cx) } + fn reload( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.editor.reload(project, cx) + } + fn can_save_as(&self, _: &AppContext) -> bool { false } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 89eb2863fc65b2ba0e1828f7db30011b04fcf66e..486840c3d2e950fd28e71782ec359a1def7f42e1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1388,6 +1388,24 @@ impl Editor { } } + pub fn replace_selections_with( + &mut self, + cx: &mut ViewContext, + find_replacement: impl Fn(&DisplaySnapshot) -> DisplayPoint, + ) { + let display_map = self.snapshot(cx); + let cursor = find_replacement(&display_map); + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + .map(|display_point| display_point.to_point(&display_map)); + self.update_selections(vec![selection], None, cx); + } + pub fn move_selections( &mut self, cx: &mut ViewContext, @@ -1398,21 +1416,9 @@ impl Editor { .local_selections::(cx) .into_iter() .map(|selection| { - let mut selection = Selection { - id: selection.id, - start: selection.start.to_display_point(&display_map), - end: selection.end.to_display_point(&display_map), - reversed: selection.reversed, - goal: selection.goal, - }; + let mut selection = selection.map(|point| point.to_display_point(&display_map)); move_selection(&display_map, &mut selection); - Selection { - id: selection.id, - start: selection.start.to_point(&display_map), - end: selection.end.to_point(&display_map), - reversed: selection.reversed, - goal: selection.goal, - } + selection.map(|display_point| display_point.to_point(&display_map)) }) .collect(); self.update_selections(selections, Some(Autoscroll::Fit), cx); @@ -2593,6 +2599,8 @@ impl Editor { } } } + } else { + return Ok(()); } let mut ranges_to_highlight = Vec::new(); @@ -5858,6 +5866,7 @@ impl Editor { self.refresh_code_actions(cx); cx.emit(Event::BufferEdited); } + language::Event::Reparsed => cx.emit(Event::Reparsed), language::Event::Dirtied => cx.emit(Event::Dirtied), language::Event::Saved => cx.emit(Event::Saved), language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), @@ -5985,6 +5994,7 @@ pub enum Event { Activate, BufferEdited, Edited, + Reparsed, Blurred, Dirtied, Saved, @@ -6451,13 +6461,12 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { - use super::*; use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowOptions}, }; - use language::{LanguageConfig, LanguageServerConfig}; + use language::{FakeLspAdapter, LanguageConfig}; use lsp::FakeLanguageServer; use project::FakeFs; use smol::stream::StreamExt; @@ -8893,26 +8902,27 @@ mod tests { cx.foreground().forbid_parking(); cx.update(populate_settings); - let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }); let fs = FakeFs::new(cx.background().clone()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages().add(language)); + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -8926,7 +8936,9 @@ mod tests { .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); @@ -8940,13 +8952,14 @@ mod tests { params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); - Some(vec![lsp::TextEdit::new( + Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), - )]) + )])) }) .next() .await; + cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), @@ -8968,6 +8981,7 @@ mod tests { }); let save = cx.update(|cx| editor.save(project.clone(), cx)); cx.foreground().advance_clock(items::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), @@ -8980,23 +8994,24 @@ mod tests { async fn test_completion(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); - let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); let text = " one @@ -9009,7 +9024,7 @@ mod tests { fs.insert_file("/file.rs", text).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages().add(language)); + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -9168,7 +9183,7 @@ mod tests { params.text_document_position.position, lsp::Position::new(position.row, position.column) ); - Some(lsp::CompletionResponse::Array( + Ok(Some(lsp::CompletionResponse::Array( completions .iter() .map(|(range, new_text)| lsp::CompletionItem { @@ -9183,7 +9198,7 @@ mod tests { ..Default::default() }) .collect(), - )) + ))) } }) .next() @@ -9197,7 +9212,7 @@ mod tests { fake.handle_request::(move |_, _| { let edit = edit.clone(); async move { - lsp::CompletionItem { + Ok(lsp::CompletionItem { additional_text_edits: edit.map(|(range, new_text)| { vec![lsp::TextEdit::new( lsp::Range::new( @@ -9208,7 +9223,7 @@ mod tests { )] }), ..Default::default() - } + }) } }) .next() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 79b25f8f60fa2ebe9e9832aac99b68c91b775f6b..67d5aee7731d9e23934849a8ee638de034bc82f6 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -371,6 +371,31 @@ impl Item for Editor { }) } + fn reload( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + let buffer = self.buffer().clone(); + let buffers = self.buffer.read(cx).all_buffers(); + let reload_buffers = + project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx)); + cx.spawn(|this, mut cx| async move { + let transaction = reload_buffers.log_err().await; + this.update(&mut cx, |editor, cx| { + editor.request_autoscroll(Autoscroll::Fit, cx) + }); + buffer.update(&mut cx, |buffer, _| { + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0); + } + } + }); + Ok(()) + }) + } + fn should_activate_item_on_event(event: &Event) -> bool { matches!(event, Event::Activate) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index ad3fc3202d94cfeef9f6ace31ccac8068b0c9b10..c07df895c995eda51d44da0221a30bf5118b5e39 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1001,6 +1001,13 @@ impl MultiBuffer { .collect() } + pub fn buffer(&self, buffer_id: usize) -> Option> { + self.buffers + .borrow() + .get(&buffer_id) + .map(|state| state.buffer.clone()) + } + pub fn save(&mut self, cx: &mut ModelContext) -> Task> { let mut save_tasks = Vec::new(); for BufferState { buffer, .. } in self.buffers.borrow().values() { @@ -2268,12 +2275,12 @@ impl MultiBufferSnapshot { &self, offset: T, theme: Option<&SyntaxTheme>, - ) -> Option<(BufferSnapshot, Vec>)> { + ) -> Option<(usize, Vec>)> { let anchor = self.anchor_before(offset); let excerpt_id = anchor.excerpt_id(); let excerpt = self.excerpt(excerpt_id)?; Some(( - excerpt.buffer.clone(), + excerpt.buffer_id, excerpt .buffer .symbols_containing(anchor.text_anchor, theme) diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 33f9f22e116b0b2005a0e79947de345dc69d0dcf..7170bd2fd598d81301a3d10342f93c24040301d9 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -30,6 +30,7 @@ impl Event { let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); let unmodified_chars = CStr::from_ptr( native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char, @@ -80,7 +81,7 @@ impl Event { NSF12FunctionKey => "f12", _ => { - if !cmd && !ctrl { + if !cmd && !ctrl && !function { input = Some( CStr::from_ptr( native_event.characters().UTF8String() as *mut c_char diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0c1ce7c2285588a50d7bef75357e5ab26f37b990..eb68b8bbddb1af10cd7538398430600d361a2e15 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,8 +1,7 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, - proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, - PLAIN_TEXT, + proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, @@ -276,6 +275,16 @@ pub enum CharKind { Word, } +impl CharKind { + pub fn coerce_punctuation(self, treat_punctuation_as_word: bool) -> Self { + if treat_punctuation_as_word && self == CharKind::Punctuation { + CharKind::Word + } else { + self + } + } +} + impl Buffer { pub fn new>>( replica_id: ReplicaId, @@ -498,6 +507,30 @@ impl Buffer { cx.notify(); } + pub fn reload(&mut self, cx: &mut ModelContext) -> Task>> { + cx.spawn(|this, mut cx| async move { + if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| { + let file = this.file.as_ref()?.as_local()?; + Some((file.mtime(), file.load(cx))) + }) { + let new_text = new_text.await?; + let diff = this + .read_with(&cx, |this, cx| this.diff(new_text.into(), cx)) + .await; + this.update(&mut cx, |this, cx| { + if let Some(transaction) = this.apply_diff(diff, cx).cloned() { + this.did_reload(this.version(), new_mtime, cx); + Ok(Some(transaction)) + } else { + Ok(None) + } + }) + } else { + Ok(None) + } + }) + } + pub fn did_reload( &mut self, version: clock::Global, @@ -543,29 +576,8 @@ impl Buffer { file_changed = true; if !self.is_dirty() { - task = cx.spawn(|this, mut cx| { - async move { - let new_text = this.read_with(&cx, |this, cx| { - this.file - .as_ref() - .and_then(|file| file.as_local().map(|f| f.load(cx))) - }); - if let Some(new_text) = new_text { - let new_text = new_text.await?; - let diff = this - .read_with(&cx, |this, cx| this.diff(new_text.into(), cx)) - .await; - this.update(&mut cx, |this, cx| { - if this.apply_diff(diff, cx) { - this.did_reload(this.version(), new_mtime, cx); - } - }); - } - Ok(()) - } - .log_err() - .map(drop) - }); + let reload = self.reload(cx).log_err().map(drop); + task = cx.foreground().spawn(reload); } } } @@ -902,8 +914,13 @@ impl Buffer { }) } - pub(crate) fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext) -> bool { + pub(crate) fn apply_diff( + &mut self, + diff: Diff, + cx: &mut ModelContext, + ) -> Option<&Transaction> { if self.version == diff.base_version { + self.finalize_last_transaction(); self.start_transaction(); let mut offset = diff.start_offset; for (tag, len) in diff.changes { @@ -924,10 +941,13 @@ impl Buffer { } } } - self.end_transaction(cx); - true + if self.end_transaction(cx).is_some() { + self.finalize_last_transaction() + } else { + None + } } else { - false + None } } diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 490789a8c80c3abe320d54bc12b88ec3529832cc..51c921e61c90081e516376ad25d729315d13c678 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -34,6 +34,23 @@ pub struct Summary { count: usize, } +impl DiagnosticEntry { + // Used to provide diagnostic context to lsp codeAction request + pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic { + let code = self + .diagnostic + .code + .clone() + .map(lsp::NumberOrString::String); + + lsp::Diagnostic { + code, + severity: Some(self.diagnostic.severity), + ..Default::default() + } + } +} + impl DiagnosticSet { pub fn from_sorted_entries(iter: I, buffer: &text::BufferSnapshot) -> Self where diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8e5698a614f1bb34088ec62dcea39df365cd7dd8..322fd19b9e8de659202ff2773907932109917bdc 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -7,8 +7,8 @@ pub mod proto; mod tests; use anyhow::{anyhow, Context, Result}; -use client::http::{self, HttpClient}; -use collections::HashSet; +use client::http::HttpClient; +use collections::HashMap; use futures::{ future::{BoxFuture, Shared}, FutureExt, TryFutureExt, @@ -20,6 +20,7 @@ use parking_lot::{Mutex, RwLock}; use serde::Deserialize; use serde_json::Value; use std::{ + any::Any, cell::RefCell, ops::Range, path::{Path, PathBuf}, @@ -51,7 +52,6 @@ lazy_static! { brackets: Default::default(), autoclose_before: Default::default(), line_comment: None, - language_server: None, }, None, )); @@ -61,20 +61,18 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub struct LspBinaryVersion { - pub name: String, - pub url: Option, -} +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct LanguageServerName(pub Arc); pub trait LspAdapter: 'static + Send + Sync { - fn name(&self) -> &'static str; + fn name(&self) -> LanguageServerName; fn fetch_latest_server_version( &self, http: Arc, - ) -> BoxFuture<'static, Result>; + ) -> BoxFuture<'static, Result>>; fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, http: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result>; @@ -96,6 +94,14 @@ pub trait LspAdapter: 'static + Send + Sync { fn initialization_options(&self) -> Option { None } + + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + Default::default() + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + None + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -113,7 +119,6 @@ pub struct LanguageConfig { #[serde(default)] pub autoclose_before: String, pub line_comment: Option, - pub language_server: Option, } impl Default for LanguageConfig { @@ -124,25 +129,17 @@ impl Default for LanguageConfig { brackets: Default::default(), autoclose_before: Default::default(), line_comment: Default::default(), - language_server: Default::default(), } } } -#[derive(Default, Deserialize)] -pub struct LanguageServerConfig { - pub disk_based_diagnostic_sources: HashSet, - pub disk_based_diagnostics_progress_token: Option, - #[cfg(any(test, feature = "test-support"))] - #[serde(skip)] - fake_config: Option, -} - #[cfg(any(test, feature = "test-support"))] -struct FakeLanguageServerConfig { - servers_tx: mpsc::UnboundedSender, - capabilities: lsp::ServerCapabilities, - initializer: Option>, +pub struct FakeLspAdapter { + pub name: &'static str, + pub capabilities: lsp::ServerCapabilities, + pub initializer: Option>, + pub disk_based_diagnostics_progress_token: Option<&'static str>, + pub disk_based_diagnostics_sources: &'static [&'static str], } #[derive(Clone, Debug, Deserialize)] @@ -157,7 +154,12 @@ pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, pub(crate) adapter: Option>, - lsp_binary_path: Mutex>>>>>, + + #[cfg(any(test, feature = "test-support"))] + fake_adapter: Option<( + mpsc::UnboundedSender, + Arc, + )>, } pub struct Grammar { @@ -184,6 +186,12 @@ pub struct LanguageRegistry { lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, login_shell_env_loaded: Shared>, + lsp_binary_paths: Mutex< + HashMap< + LanguageServerName, + Shared>>>, + >, + >, } impl LanguageRegistry { @@ -195,6 +203,7 @@ impl LanguageRegistry { lsp_binary_statuses_tx, lsp_binary_statuses_rx, login_shell_env_loaded: login_shell_env_loaded.shared(), + lsp_binary_paths: Default::default(), } } @@ -244,7 +253,7 @@ impl LanguageRegistry { } pub fn start_language_server( - &self, + self: &Arc, server_id: usize, language: Arc, root_path: Arc, @@ -252,34 +261,20 @@ impl LanguageRegistry { cx: &mut MutableAppContext, ) -> Option>> { #[cfg(any(test, feature = "test-support"))] - if language - .config - .language_server - .as_ref() - .and_then(|config| config.fake_config.as_ref()) - .is_some() - { + if language.fake_adapter.is_some() { let language = language.clone(); - return Some(cx.spawn(|mut cx| async move { - let fake_config = language - .config - .language_server - .as_ref() - .unwrap() - .fake_config - .as_ref() - .unwrap(); - let (server, mut fake_server) = cx.update(|cx| { - lsp::LanguageServer::fake_with_capabilities( - fake_config.capabilities.clone(), - cx, - ) - }); - if let Some(initializer) = &fake_config.initializer { + return Some(cx.spawn(|cx| async move { + let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); + let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities( + fake_adapter.capabilities.clone(), + cx.clone(), + ); + + if let Some(initializer) = &fake_adapter.initializer { initializer(&mut fake_server); } - let servers_tx = fake_config.servers_tx.clone(); + let servers_tx = servers_tx.clone(); cx.background() .spawn(async move { fake_server @@ -298,16 +293,17 @@ impl LanguageRegistry { .ok_or_else(|| anyhow!("language server download directory has not been assigned")) .log_err()?; + let this = self.clone(); let adapter = language.adapter.clone()?; - let background = cx.background().clone(); let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); - Some(cx.background().spawn(async move { + Some(cx.spawn(|cx| async move { login_shell_env_loaded.await; - let server_binary_path = language - .lsp_binary_path + let server_binary_path = this + .lsp_binary_paths .lock() - .get_or_insert_with(|| { + .entry(adapter.name()) + .or_insert_with(|| { get_server_binary_path( adapter.clone(), language.clone(), @@ -329,8 +325,7 @@ impl LanguageRegistry { &server_binary_path, server_args, &root_path, - adapter.initialization_options(), - background, + cx, )?; Ok(server) })) @@ -350,7 +345,7 @@ async fn get_server_binary_path( download_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { - let container_dir = download_dir.join(adapter.name()); + let container_dir = download_dir.join(adapter.name().0.as_ref()); if !container_dir.exists() { smol::fs::create_dir_all(&container_dir) .await @@ -423,10 +418,16 @@ impl Language { }) }), adapter: None, - lsp_binary_path: Default::default(), + + #[cfg(any(test, feature = "test-support"))] + fake_adapter: None, } } + pub fn lsp_adapter(&self) -> Option> { + self.adapter.clone() + } + pub fn with_highlights_query(mut self, source: &str) -> Result { let grammar = self .grammar @@ -467,11 +468,23 @@ impl Language { Ok(self) } - pub fn with_lsp_adapter(mut self, lsp_adapter: impl LspAdapter) -> Self { - self.adapter = Some(Arc::new(lsp_adapter)); + pub fn with_lsp_adapter(mut self, lsp_adapter: Arc) -> Self { + self.adapter = Some(lsp_adapter); self } + #[cfg(any(test, feature = "test-support"))] + pub fn set_fake_lsp_adapter( + &mut self, + fake_lsp_adapter: FakeLspAdapter, + ) -> mpsc::UnboundedReceiver { + let (servers_tx, servers_rx) = mpsc::unbounded(); + let adapter = Arc::new(fake_lsp_adapter); + self.fake_adapter = Some((servers_tx, adapter.clone())); + self.adapter = Some(adapter); + servers_rx + } + pub fn name(&self) -> Arc { self.config.name.clone() } @@ -480,18 +493,16 @@ impl Language { self.config.line_comment.as_deref() } - pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet> { - self.config - .language_server - .as_ref() - .map(|config| &config.disk_based_diagnostic_sources) + pub fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + self.adapter.as_ref().map_or(&[] as &[_], |adapter| { + adapter.disk_based_diagnostic_sources() + }) } - pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> { - self.config - .language_server + pub fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + self.adapter .as_ref() - .and_then(|config| config.disk_based_diagnostics_progress_token.as_ref()) + .and_then(|adapter| adapter.disk_based_diagnostics_progress_token()) } pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { @@ -598,47 +609,70 @@ impl CodeLabel { } #[cfg(any(test, feature = "test-support"))] -impl LanguageServerConfig { - pub fn fake() -> (Self, mpsc::UnboundedReceiver) { - let (servers_tx, servers_rx) = mpsc::unbounded(); - ( - Self { - fake_config: Some(FakeLanguageServerConfig { - servers_tx, - capabilities: lsp::LanguageServer::full_capabilities(), - initializer: None, - }), - disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()), - ..Default::default() - }, - servers_rx, - ) +impl Default for FakeLspAdapter { + fn default() -> Self { + Self { + name: "the-fake-language-server", + capabilities: lsp::LanguageServer::full_capabilities(), + initializer: None, + disk_based_diagnostics_progress_token: None, + disk_based_diagnostics_sources: &[], + } } +} - pub fn set_fake_capabilities(&mut self, capabilities: lsp::ServerCapabilities) { - self.fake_config.as_mut().unwrap().capabilities = capabilities; +#[cfg(any(test, feature = "test-support"))] +impl LspAdapter for FakeLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName(self.name.into()) } - pub fn set_fake_initializer( - &mut self, - initializer: impl 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer), - ) { - self.fake_config.as_mut().unwrap().initializer = Some(Box::new(initializer)); + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result>> { + unreachable!(); } -} -impl ToLspPosition for PointUtf16 { - fn to_lsp_position(self) -> lsp::Position { - lsp::Position::new(self.row, self.column) + fn fetch_server_binary( + &self, + _: Box, + _: Arc, + _: PathBuf, + ) -> BoxFuture<'static, Result> { + unreachable!(); } + + fn cached_server_binary(&self, _: PathBuf) -> BoxFuture<'static, Option> { + unreachable!(); + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + self.disk_based_diagnostics_sources + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + self.disk_based_diagnostics_progress_token + } +} + +pub fn point_to_lsp(point: PointUtf16) -> lsp::Position { + lsp::Position::new(point.row, point.column) } pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 { PointUtf16::new(point.line, point.character) } +pub fn range_to_lsp(range: Range) -> lsp::Range { + lsp::Range { + start: point_to_lsp(range.start), + end: point_to_lsp(range.end), + } +} + pub fn range_from_lsp(range: lsp::Range) -> Range { - let start = PointUtf16::new(range.start.line, range.start.character); - let end = PointUtf16::new(range.end.line, range.end.character); - start..end + point_from_lsp(range.start)..point_from_lsp(range.end) } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 3eb87cefb62928573aa9087a39be8533f727bcc9..0acd4d7d2ffc5a5d9e8db4957da1d04c7430b02c 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -136,12 +136,16 @@ async fn test_apply_diff(cx: &mut gpui::TestAppContext) { let text = "a\nccc\ndddd\nffffff\n"; let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(cx, |b, cx| b.apply_diff(diff, cx)); + buffer.update(cx, |buffer, cx| { + buffer.apply_diff(diff, cx).unwrap(); + }); cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(cx, |b, cx| b.apply_diff(diff, cx)); + buffer.update(cx, |buffer, cx| { + buffer.apply_diff(diff, cx).unwrap(); + }); cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); } @@ -927,7 +931,6 @@ fn rust_lang() -> Language { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: None, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index de47381c4666b7980b1c484d06fdae770d032767..f5fc98640d7400160b104ec6654d0260c3ffd25c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,15 +1,17 @@ +pub use lsp_types::*; + use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; -use gpui::{executor, Task}; -use parking_lot::{Mutex, RwLock}; +use gpui::{executor, AsyncAppContext, Task}; +use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, - process::Command, + process, }; use std::{ future::Future, @@ -22,15 +24,12 @@ use std::{ }, }; use std::{path::Path, process::Stdio}; -use util::TryFutureExt; - -pub use lsp_types::*; +use util::{ResultExt, TryFutureExt}; const JSON_RPC_VERSION: &'static str = "2.0"; const CONTENT_LEN_HEADER: &'static str = "Content-Length: "; -type NotificationHandler = - Box, &str, &mut channel::Sender>) -> Result<()>>; +type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; pub struct LanguageServer { @@ -39,18 +38,17 @@ pub struct LanguageServer { outbound_tx: channel::Sender>, name: String, capabilities: ServerCapabilities, - notification_handlers: Arc>>, + notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, root_path: PathBuf, - options: Option, } pub struct Subscription { method: &'static str, - notification_handlers: Arc>>, + notification_handlers: Arc>>, } #[derive(Serialize, Deserialize)] @@ -61,18 +59,6 @@ struct Request<'a, T> { params: T, } -#[cfg(any(test, feature = "test-support"))] -#[derive(Deserialize)] -struct AnyRequest<'a> { - id: usize, - #[serde(borrow)] - jsonrpc: &'a str, - #[serde(borrow)] - method: &'a str, - #[serde(borrow)] - params: &'a RawValue, -} - #[derive(Serialize, Deserialize)] struct AnyResponse<'a> { id: usize, @@ -85,7 +71,8 @@ struct AnyResponse<'a> { #[derive(Serialize)] struct Response { id: usize, - result: T, + result: Option, + error: Option, } #[derive(Serialize, Deserialize)] @@ -118,15 +105,14 @@ impl LanguageServer { binary_path: &Path, args: &[&str], root_path: &Path, - options: Option, - background: Arc, + cx: AsyncAppContext, ) -> Result { let working_dir = if root_path.is_dir() { root_path } else { root_path.parent().unwrap_or(Path::new("/")) }; - let mut server = Command::new(binary_path) + let mut server = process::Command::new(binary_path) .current_dir(working_dir) .args(args) .stdin(Stdio::piped()) @@ -136,99 +122,97 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); let mut server = - Self::new_internal(server_id, stdin, stdout, root_path, options, background); + Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| { + log::info!( + "unhandled notification {}:\n{}", + notification.method, + serde_json::to_string_pretty( + &Value::from_str(notification.params.get()).unwrap() + ) + .unwrap() + ); + }); if let Some(name) = binary_path.file_name() { server.name = name.to_string_lossy().to_string(); } Ok(server) } - fn new_internal( + fn new_internal( server_id: usize, stdin: Stdin, stdout: Stdout, root_path: &Path, - options: Option, - executor: Arc, + cx: AsyncAppContext, + mut on_unhandled_notification: F, ) -> Self where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send, { let mut stdin = BufWriter::new(stdin); let mut stdout = BufReader::new(stdout); let (outbound_tx, outbound_rx) = channel::unbounded::>(); let notification_handlers = - Arc::new(RwLock::new(HashMap::<_, NotificationHandler>::default())); + Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default())); let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::default())); - let input_task = executor.spawn( - { - let notification_handlers = notification_handlers.clone(); - let response_handlers = response_handlers.clone(); - let mut outbound_tx = outbound_tx.clone(); - async move { - let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone()); - let mut buffer = Vec::new(); - loop { - buffer.clear(); - stdout.read_until(b'\n', &mut buffer).await?; - stdout.read_until(b'\n', &mut buffer).await?; - let message_len: usize = std::str::from_utf8(&buffer)? - .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid header"))? - .trim_end() - .parse()?; - - buffer.resize(message_len, 0); - stdout.read_exact(&mut buffer).await?; - - if let Ok(AnyNotification { id, method, params }) = - serde_json::from_slice(&buffer) - { - if let Some(handler) = notification_handlers.write().get_mut(method) { - if let Err(e) = handler(id, params.get(), &mut outbound_tx) { - log::error!("error handling {} message: {:?}", method, e); - } + let input_task = cx.spawn(|cx| { + let notification_handlers = notification_handlers.clone(); + let response_handlers = response_handlers.clone(); + async move { + let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone()); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stdout.read_until(b'\n', &mut buffer).await?; + stdout.read_until(b'\n', &mut buffer).await?; + let message_len: usize = std::str::from_utf8(&buffer)? + .strip_prefix(CONTENT_LEN_HEADER) + .ok_or_else(|| anyhow!("invalid header"))? + .trim_end() + .parse()?; + + buffer.resize(message_len, 0); + stdout.read_exact(&mut buffer).await?; + log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer)); + + if let Ok(msg) = serde_json::from_slice::(&buffer) { + if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { + handler(msg.id, msg.params.get(), cx.clone()); + } else { + on_unhandled_notification(msg); + } + } else if let Ok(AnyResponse { id, error, result }) = + serde_json::from_slice(&buffer) + { + if let Some(handler) = response_handlers.lock().remove(&id) { + if let Some(error) = error { + handler(Err(error)); + } else if let Some(result) = result { + handler(Ok(result.get())); } else { - log::info!( - "unhandled notification {}:\n{}", - method, - serde_json::to_string_pretty( - &Value::from_str(params.get()).unwrap() - ) - .unwrap() - ); - } - } else if let Ok(AnyResponse { id, error, result }) = - serde_json::from_slice(&buffer) - { - if let Some(handler) = response_handlers.lock().remove(&id) { - if let Some(error) = error { - handler(Err(error)); - } else if let Some(result) = result { - handler(Ok(result.get())); - } else { - handler(Ok("null")); - } + handler(Ok("null")); } - } else { - return Err(anyhow!( - "failed to deserialize message:\n{}", - std::str::from_utf8(&buffer)? - )); } + } else { + return Err(anyhow!( + "failed to deserialize message:\n{}", + std::str::from_utf8(&buffer)? + )); } } } - .log_err(), - ); + .log_err() + }); let (output_done_tx, output_done_rx) = barrier::channel(); - let output_task = executor.spawn({ + let output_task = cx.background().spawn({ let response_handlers = response_handlers.clone(); async move { let _clear_response_handlers = ClearResponseHandlers(response_handlers); let mut content_len_buffer = Vec::new(); while let Ok(message) = outbound_rx.recv().await { + log::trace!("outgoing message:{}", String::from_utf8_lossy(&message)); content_len_buffer.clear(); write!(content_len_buffer, "{}", message.len()).unwrap(); stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; @@ -251,18 +235,15 @@ impl LanguageServer { capabilities: Default::default(), next_id: Default::default(), outbound_tx, - executor: executor.clone(), + executor: cx.background().clone(), io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), - options, } } - pub async fn initialize(mut self) -> Result> { - let options = self.options.take(); - let mut this = Arc::new(self); - let root_uri = Url::from_file_path(&this.root_path).unwrap(); + pub async fn initialize(mut self, options: Option) -> Result> { + let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { process_id: Default::default(), @@ -288,12 +269,13 @@ impl LanguageServer { value_set: vec![ CodeActionKind::REFACTOR.as_str().into(), CodeActionKind::QUICKFIX.as_str().into(), + CodeActionKind::SOURCE.as_str().into(), ], }, }), data_support: Some(true), resolve_support: Some(CodeActionCapabilityResolveSupport { - properties: vec!["edit".to_string()], + properties: vec!["edit".to_string(), "command".to_string()], }), ..Default::default() }), @@ -324,16 +306,14 @@ impl LanguageServer { locale: Default::default(), }; - let response = this.request::(params).await?; - { - let this = Arc::get_mut(&mut this).unwrap(); - if let Some(info) = response.server_info { - this.name = info.name; - } - this.capabilities = response.capabilities; + let response = self.request::(params).await?; + if let Some(info) = response.server_info { + self.name = info.name; } - this.notify::(InitializedParams {})?; - Ok(this) + self.capabilities = response.capabilities; + + self.notify::(InitializedParams {})?; + Ok(Arc::new(self)) } pub fn shutdown(&self) -> Option>> { @@ -368,37 +348,42 @@ impl LanguageServer { } } - pub fn on_notification(&mut self, f: F) -> Subscription + #[must_use] + pub fn on_notification(&self, f: F) -> Subscription where T: notification::Notification, - F: 'static + Send + Sync + FnMut(T::Params), + F: 'static + Send + FnMut(T::Params, AsyncAppContext), { self.on_custom_notification(T::METHOD, f) } - pub fn on_request(&mut self, f: F) -> Subscription + #[must_use] + pub fn on_request(&self, f: F) -> Subscription where T: request::Request, - F: 'static + Send + Sync + FnMut(T::Params) -> Result, + T::Params: 'static + Send, + F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut, + Fut: 'static + Future>, { self.on_custom_request(T::METHOD, f) } - pub fn on_custom_notification( - &mut self, - method: &'static str, - mut f: F, - ) -> Subscription + pub fn remove_request_handler(&self) { + self.notification_handlers.lock().remove(T::METHOD); + } + + #[must_use] + pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription where - F: 'static + Send + Sync + FnMut(Params), + F: 'static + Send + FnMut(Params, AsyncAppContext), Params: DeserializeOwned, { - let prev_handler = self.notification_handlers.write().insert( + let prev_handler = self.notification_handlers.lock().insert( method, - Box::new(move |_, params, _| { - let params = serde_json::from_str(params)?; - f(params); - Ok(()) + Box::new(move |_, params, cx| { + if let Some(params) = serde_json::from_str(params).log_err() { + f(params, cx); + } }), ); assert!( @@ -411,26 +396,52 @@ impl LanguageServer { } } - pub fn on_custom_request( - &mut self, + #[must_use] + pub fn on_custom_request( + &self, method: &'static str, mut f: F, ) -> Subscription where - F: 'static + Send + Sync + FnMut(Params) -> Result, - Params: DeserializeOwned, + F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut, + Fut: 'static + Future>, + Params: DeserializeOwned + Send + 'static, Res: Serialize, { - let prev_handler = self.notification_handlers.write().insert( + let outbound_tx = self.outbound_tx.clone(); + let prev_handler = self.notification_handlers.lock().insert( method, - Box::new(move |id, params, tx| { + Box::new(move |id, params, cx| { if let Some(id) = id { - let params = serde_json::from_str(params)?; - let result = f(params)?; - let response = serde_json::to_vec(&Response { id, result })?; - tx.try_send(response)?; + if let Some(params) = serde_json::from_str(params).log_err() { + let response = f(params, cx.clone()); + cx.foreground() + .spawn({ + let outbound_tx = outbound_tx.clone(); + async move { + let response = match response.await { + Ok(result) => Response { + id, + result: Some(result), + error: None, + }, + Err(error) => Response { + id, + result: None, + error: Some(Error { + message: error.to_string(), + }), + }, + }; + if let Some(response) = serde_json::to_vec(&response).log_err() + { + outbound_tx.try_send(response).ok(); + } + } + }) + .detach(); + } } - Ok(()) }), ); assert!( @@ -456,7 +467,7 @@ impl LanguageServer { } pub fn request( - self: &Arc, + &self, params: T::Params, ) -> impl Future> where @@ -547,36 +558,17 @@ impl Subscription { impl Drop for Subscription { fn drop(&mut self) { - self.notification_handlers.write().remove(self.method); + self.notification_handlers.lock().remove(self.method); } } #[cfg(any(test, feature = "test-support"))] +#[derive(Clone)] pub struct FakeLanguageServer { - handlers: FakeLanguageServerHandlers, - outgoing_tx: futures::channel::mpsc::UnboundedSender>, - incoming_rx: futures::channel::mpsc::UnboundedReceiver>, - _input_task: Task>, - _output_task: Task>, + pub server: Arc, + notifications_rx: channel::Receiver<(String, String)>, } -#[cfg(any(test, feature = "test-support"))] -type FakeLanguageServerHandlers = Arc< - Mutex< - HashMap< - &'static str, - Box< - dyn Send - + FnMut( - usize, - &[u8], - gpui::AsyncAppContext, - ) -> futures::future::BoxFuture<'static, Vec>, - >, - >, - >, ->; - #[cfg(any(test, feature = "test-support"))] impl LanguageServer { pub fn full_capabilities() -> ServerCapabilities { @@ -589,177 +581,101 @@ impl LanguageServer { } } - pub fn fake(cx: &mut gpui::MutableAppContext) -> (Self, FakeLanguageServer) { + pub fn fake(cx: AsyncAppContext) -> (Self, FakeLanguageServer) { Self::fake_with_capabilities(Self::full_capabilities(), cx) } pub fn fake_with_capabilities( capabilities: ServerCapabilities, - cx: &mut gpui::MutableAppContext, + cx: AsyncAppContext, ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); + let (notifications_tx, notifications_rx) = channel::unbounded(); - let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx); + let server = Self::new_internal( + 0, + stdin_writer, + stdout_reader, + Path::new("/"), + cx.clone(), + |_| {}, + ); + let fake = FakeLanguageServer { + server: Arc::new(Self::new_internal( + 0, + stdout_writer, + stdin_reader, + Path::new("/"), + cx.clone(), + move |msg| { + notifications_tx + .try_send((msg.method.to_string(), msg.params.get().to_string())) + .ok(); + }, + )), + notifications_rx, + }; fake.handle_request::({ let capabilities = capabilities.clone(); move |_, _| { let capabilities = capabilities.clone(); async move { - InitializeResult { + Ok(InitializeResult { capabilities, ..Default::default() - } + }) } } }); - let executor = cx.background().clone(); - let server = Self::new_internal( - 0, - stdin_writer, - stdout_reader, - Path::new("/"), - None, - executor, - ); (server, fake) } } #[cfg(any(test, feature = "test-support"))] impl FakeLanguageServer { - fn new( - stdin: async_pipe::PipeReader, - stdout: async_pipe::PipeWriter, - cx: &mut gpui::MutableAppContext, - ) -> Self { - use futures::StreamExt as _; - - let (incoming_tx, incoming_rx) = futures::channel::mpsc::unbounded(); - let (outgoing_tx, mut outgoing_rx) = futures::channel::mpsc::unbounded(); - let handlers = FakeLanguageServerHandlers::default(); - - let input_task = cx.spawn(|cx| { - let handlers = handlers.clone(); - let outgoing_tx = outgoing_tx.clone(); - async move { - let mut buffer = Vec::new(); - let mut stdin = smol::io::BufReader::new(stdin); - while Self::receive(&mut stdin, &mut buffer).await.is_ok() { - cx.background().simulate_random_delay().await; - - if let Ok(request) = serde_json::from_slice::(&buffer) { - assert_eq!(request.jsonrpc, JSON_RPC_VERSION); - - let response; - if let Some(handler) = handlers.lock().get_mut(request.method) { - response = - handler(request.id, request.params.get().as_bytes(), cx.clone()) - .await; - log::debug!("handled lsp request. method:{}", request.method); - } else { - response = serde_json::to_vec(&AnyResponse { - id: request.id, - error: Some(Error { - message: "no handler".to_string(), - }), - result: None, - }) - .unwrap(); - log::debug!("unhandled lsp request. method:{}", request.method); - } - outgoing_tx.unbounded_send(response)?; - } else { - incoming_tx.unbounded_send(buffer.clone())?; - } - } - Ok::<_, anyhow::Error>(()) - } - }); - - let output_task = cx.background().spawn(async move { - let mut stdout = smol::io::BufWriter::new(stdout); - while let Some(message) = outgoing_rx.next().await { - stdout.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; - stdout - .write_all((format!("{}", message.len())).as_bytes()) - .await?; - stdout.write_all("\r\n\r\n".as_bytes()).await?; - stdout.write_all(&message).await?; - stdout.flush().await?; - } - Ok(()) - }); - - Self { - outgoing_tx, - incoming_rx, - handlers, - _input_task: input_task, - _output_task: output_task, - } - } - - pub fn notify(&mut self, params: T::Params) { - let message = serde_json::to_vec(&Notification { - jsonrpc: JSON_RPC_VERSION, - method: T::METHOD, - params, - }) - .unwrap(); - self.outgoing_tx.unbounded_send(message).unwrap(); + pub fn notify(&self, params: T::Params) { + self.server.notify::(params).ok(); } pub async fn receive_notification(&mut self) -> T::Params { use futures::StreamExt as _; loop { - let bytes = self.incoming_rx.next().await.unwrap(); - if let Ok(notification) = serde_json::from_slice::>(&bytes) { - assert_eq!(notification.method, T::METHOD); - return notification.params; + let (method, params) = self.notifications_rx.next().await.unwrap(); + if &method == T::METHOD { + return serde_json::from_str::(¶ms).unwrap(); } else { - log::info!( - "skipping message in fake language server {:?}", - std::str::from_utf8(&bytes) - ); + log::info!("skipping message in fake language server {:?}", params); } } } pub fn handle_request( - &mut self, + &self, mut handler: F, ) -> futures::channel::mpsc::UnboundedReceiver<()> where T: 'static + request::Request, + T::Params: 'static + Send, F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut, - Fut: 'static + Send + Future, + Fut: 'static + Send + Future>, { - use futures::FutureExt as _; - let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); - self.handlers.lock().insert( - T::METHOD, - Box::new(move |id, params, cx| { - let result = handler(serde_json::from_slice::(params).unwrap(), cx); + self.server.remove_request_handler::(); + self.server + .on_request::(move |params, cx| { + let result = handler(params, cx.clone()); let responded_tx = responded_tx.clone(); async move { + cx.background().simulate_random_delay().await; let result = result.await; - let result = serde_json::to_string(&result).unwrap(); - let result = serde_json::from_str::<&RawValue>(&result).unwrap(); - let response = AnyResponse { - id, - error: None, - result: Some(result), - }; responded_tx.unbounded_send(()).ok(); - serde_json::to_vec(&response).unwrap() + result } - .boxed() - }), - ); + }) + .detach(); responded_rx } @@ -767,7 +683,7 @@ impl FakeLanguageServer { where T: 'static + request::Request, { - self.handlers.lock().remove(T::METHOD); + self.server.remove_request_handler::(); } pub async fn start_progress(&mut self, token: impl Into) { @@ -783,25 +699,6 @@ impl FakeLanguageServer { value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())), }); } - - async fn receive( - stdin: &mut smol::io::BufReader, - buffer: &mut Vec, - ) -> Result<()> { - buffer.clear(); - stdin.read_until(b'\n', buffer).await?; - stdin.read_until(b'\n', buffer).await?; - let message_len: usize = std::str::from_utf8(buffer) - .unwrap() - .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid content length header"))? - .trim_end() - .parse() - .unwrap(); - buffer.resize(message_len, 0); - stdin.read_exact(buffer).await?; - Ok(()) - } } struct ClearResponseHandlers(Arc>>); @@ -826,22 +723,22 @@ mod tests { #[gpui::test] async fn test_fake(cx: &mut TestAppContext) { - let (mut server, mut fake) = cx.update(LanguageServer::fake); + let (server, mut fake) = LanguageServer::fake(cx.to_async()); let (message_tx, message_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); server - .on_notification::(move |params| { + .on_notification::(move |params, _| { message_tx.try_send(params).unwrap() }) .detach(); server - .on_notification::(move |params| { + .on_notification::(move |params, _| { diagnostics_tx.try_send(params).unwrap() }) .detach(); - let server = server.initialize().await.unwrap(); + let server = server.initialize(None).await.unwrap(); server .notify::(DidOpenTextDocumentParams { text_document: TextDocumentItem::new( @@ -876,7 +773,7 @@ mod tests { "file://b/c" ); - fake.handle_request::(|_, _| async move {}); + fake.handle_request::(|_, _| async move { Ok(()) }); drop(server); fake.receive_notification::().await; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4867ada7cb0f86b124cc7c11f5a568df83236eb7..71ad489d07320af8a3e45ece6e7df6d4b163f551 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -4,9 +4,9 @@ use async_trait::async_trait; use client::{proto, PeerId}; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ - point_from_lsp, + point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16, + range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16, }; use lsp::{DocumentHighlightKind, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path}; @@ -91,7 +91,7 @@ impl LspCommand for PrepareRename { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), } } @@ -208,7 +208,7 @@ impl LspCommand for PerformRename { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, new_name: self.new_name.clone(), work_done_progress_params: Default::default(), @@ -223,22 +223,19 @@ impl LspCommand for PerformRename { mut cx: AsyncAppContext, ) -> Result { if let Some(edit) = message { - let language_server = project + let (lsp_adapter, lsp_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; Project::deserialize_workspace_edit( project, edit, self.push_to_history, - language.name(), - language_server, + lsp_adapter, + lsp_server, &mut cx, ) .await @@ -328,7 +325,7 @@ impl LspCommand for GetDefinition { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), @@ -343,16 +340,13 @@ impl LspCommand for GetDefinition { mut cx: AsyncAppContext, ) -> Result> { let mut definitions = Vec::new(); - let language_server = project + let (lsp_adapter, language_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(message) = message { let mut unresolved_locations = Vec::new(); @@ -377,7 +371,7 @@ impl LspCommand for GetDefinition { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( target_uri, - language.name(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -503,7 +497,7 @@ impl LspCommand for GetReferences { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), @@ -521,16 +515,13 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let language_server = project + let (lsp_adapter, language_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(locations) = locations { for lsp_location in locations { @@ -538,7 +529,7 @@ impl LspCommand for GetReferences { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( lsp_location.uri, - language.name(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -668,7 +659,7 @@ impl LspCommand for GetDocumentHighlights { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4ec856f19964166d42efd489520e242328ce7685..c9683f39d978de3c8d10de0f4abccebcb801b768 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -15,11 +15,12 @@ use gpui::{ MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ + point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, - LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition, - ToOffset, ToPointUtf16, Transaction, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, + Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, + LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch, + PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -57,10 +58,13 @@ pub struct Project { worktrees: Vec, active_entry: Option, languages: Arc, - language_servers: HashMap<(WorktreeId, Arc), Arc>, - started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, + language_servers: + HashMap<(WorktreeId, LanguageServerName), (Arc, Arc)>, + started_language_servers: + HashMap<(WorktreeId, LanguageServerName), Task>>>, language_server_statuses: BTreeMap, language_server_settings: Arc>, + last_workspace_edits_by_language_server: HashMap, next_language_server_id: usize, client: Arc, next_entry_id: Arc, @@ -128,20 +132,6 @@ pub enum Event { CollaboratorLeft(PeerId), } -enum LanguageServerEvent { - WorkStart { - token: String, - }, - WorkProgress { - token: String, - progress: LanguageServerProgress, - }, - WorkEnd { - token: String, - }, - DiagnosticsUpdate(lsp::PublishDiagnosticsParams), -} - pub struct LanguageServerStatus { pub name: String, pub pending_work: BTreeMap, @@ -185,7 +175,7 @@ pub struct DocumentHighlight { pub struct Symbol { pub source_worktree_id: WorktreeId, pub worktree_id: WorktreeId, - pub language_name: String, + pub language_server_name: LanguageServerName, pub path: PathBuf, pub label: CodeLabel, pub name: String, @@ -270,6 +260,7 @@ impl Project { client.add_model_message_handler(Self::handle_update_worktree); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); + client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_format_buffers); client.add_model_request_handler(Self::handle_get_code_actions); client.add_model_request_handler(Self::handle_get_completions); @@ -342,6 +333,7 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), language_server_statuses: Default::default(), + last_workspace_edits_by_language_server: Default::default(), language_server_settings: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), @@ -429,6 +421,7 @@ impl Project { ) }) .collect(), + last_workspace_edits_by_language_server: Default::default(), next_language_server_id: 0, opened_buffers: Default::default(), buffer_snapshots: Default::default(), @@ -957,8 +950,8 @@ impl Project { fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, - lang_name: Arc, - lang_server: Arc, + lsp_adapter: Arc, + lsp_server: Arc, cx: &mut ModelContext, ) -> Task>> { cx.spawn(|this, mut cx| async move { @@ -976,8 +969,10 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.language_servers - .insert((worktree.read(cx).id(), lang_name), lang_server); + this.language_servers.insert( + (worktree.read(cx).id(), lsp_adapter.name()), + (lsp_adapter, lsp_server), + ); }); (worktree, PathBuf::new()) }; @@ -1120,7 +1115,7 @@ impl Project { } } - if let Some(server) = language_server { + if let Some((_, server)) = language_server { server .notify::( lsp::DidOpenTextDocumentParams { @@ -1153,7 +1148,7 @@ impl Project { if let Some(file) = File::from_dyn(buffer.file()) { if file.is_local() { let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - if let Some(server) = this.language_server_for_buffer(buffer, cx) { + if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) { server .notify::( lsp::DidCloseTextDocumentParams { @@ -1189,7 +1184,7 @@ impl Project { cx.background().spawn(request).detach_and_log_err(cx); } BufferEvent::Edited { .. } => { - let language_server = self + let (_, language_server) = self .language_server_for_buffer(buffer.read(cx), cx)? .clone(); let buffer = buffer.read(cx); @@ -1211,8 +1206,8 @@ impl Project { .collect(); lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range::new( - edit_start.to_lsp_position(), - edit_end.to_lsp_position(), + point_to_lsp(edit_start), + point_to_lsp(edit_end), )), range_length: None, text: new_text, @@ -1262,11 +1257,11 @@ impl Project { fn language_servers_for_worktree( &self, worktree_id: WorktreeId, - ) -> impl Iterator)> { + ) -> impl Iterator, Arc)> { self.language_servers.iter().filter_map( - move |((language_server_worktree_id, language_name), server)| { + move |((language_server_worktree_id, _), server)| { if *language_server_worktree_id == worktree_id { - Some((language_name.as_ref(), server)) + Some(server) } else { None } @@ -1302,7 +1297,12 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - let key = (worktree_id, language.name()); + let adapter = if let Some(adapter) = language.lsp_adapter() { + adapter + } else { + return; + }; + let key = (worktree_id, adapter.name()); self.started_language_servers .entry(key.clone()) .or_insert_with(|| { @@ -1315,109 +1315,100 @@ impl Project { cx, ); cx.spawn_weak(|this, mut cx| async move { - let mut language_server = language_server?.await.log_err()?; + let language_server = language_server?.await.log_err()?; + let language_server = language_server + .initialize(adapter.initialization_options()) + .await + .log_err()?; let this = this.upgrade(&cx)?; - let (language_server_events_tx, language_server_events_rx) = - smol::channel::unbounded(); + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token(); language_server .on_notification::({ - let language_server_events_tx = language_server_events_tx.clone(); - move |params| { - language_server_events_tx - .try_send(LanguageServerEvent::DiagnosticsUpdate(params)) - .ok(); + let this = this.downgrade(); + let adapter = adapter.clone(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_diagnostics_published( + server_id, + params, + &adapter, + disk_based_diagnostics_progress_token, + cx, + ); + }); + } } }) .detach(); language_server - .on_request::({ + .on_request::({ let settings = this .read_with(&cx, |this, _| this.language_server_settings.clone()); - move |params| { - let settings = settings.lock(); - Ok(params - .items - .into_iter() - .map(|item| { - if let Some(section) = &item.section { - settings - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null) - } else { - settings.clone() - } - }) - .collect()) + move |params, _| { + let settings = settings.lock().clone(); + async move { + Ok(params + .items + .into_iter() + .map(|item| { + if let Some(section) = &item.section { + settings + .get(section) + .cloned() + .unwrap_or(serde_json::Value::Null) + } else { + settings.clone() + } + }) + .collect()) + } } }) .detach(); language_server - .on_notification::(move |params| { - let token = match params.token { - lsp::NumberOrString::String(token) => token, - lsp::NumberOrString::Number(token) => { - log::info!("skipping numeric progress token {}", token); - return; - } - }; - - match params.value { - lsp::ProgressParamsValue::WorkDone(progress) => match progress { - lsp::WorkDoneProgress::Begin(_) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkStart { token }) - .ok(); - } - lsp::WorkDoneProgress::Report(report) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkProgress { - token, - progress: LanguageServerProgress { - message: report.message, - percentage: report - .percentage - .map(|p| p as usize), - last_update_at: Instant::now(), - }, - }) - .ok(); - } - lsp::WorkDoneProgress::End(_) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkEnd { token }) - .ok(); - } - }, + .on_request::({ + let this = this.downgrade(); + let adapter = adapter.clone(); + let language_server = language_server.clone(); + move |params, cx| { + Self::on_lsp_workspace_edit( + this, + params, + server_id, + adapter.clone(), + language_server.clone(), + cx, + ) } }) .detach(); - // Process all the LSP events. - cx.spawn(|mut cx| { - let this = this.downgrade(); - async move { - while let Ok(event) = language_server_events_rx.recv().await { - let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { - this.on_lsp_event(server_id, event, &language, cx) - }); - - // Don't starve the main thread when lots of events arrive all at once. - smol::future::yield_now().await; + language_server + .on_notification::({ + let this = this.downgrade(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_progress( + params, + server_id, + disk_based_diagnostics_progress_token, + cx, + ); + }); + } } - Some(()) - } - }) - .detach(); + }) + .detach(); - let language_server = language_server.initialize().await.log_err()?; this.update(&mut cx, |this, cx| { this.language_servers - .insert(key.clone(), language_server.clone()); + .insert(key.clone(), (adapter, language_server.clone())); this.language_server_statuses.insert( server_id, LanguageServerStatus { @@ -1460,7 +1451,10 @@ impl Project { } else { continue; }; - if (file.worktree.read(cx).id(), language.name()) != key { + if file.worktree.read(cx).id() != key.0 + || language.lsp_adapter().map(|a| a.name()) + != Some(key.1.clone()) + { continue; } @@ -1539,15 +1533,20 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - let key = (worktree_id, language.name()); + let adapter = if let Some(adapter) = language.lsp_adapter() { + adapter + } else { + return; + }; + let key = (worktree_id, adapter.name()); let server_to_shutdown = self.language_servers.remove(&key); self.started_language_servers.remove(&key); server_to_shutdown .as_ref() - .map(|server| self.language_server_statuses.remove(&server.server_id())); + .map(|(_, server)| self.language_server_statuses.remove(&server.server_id())); cx.spawn_weak(|this, mut cx| async move { if let Some(this) = this.upgrade(&cx) { - if let Some(server_to_shutdown) = server_to_shutdown { + if let Some((_, server_to_shutdown)) = server_to_shutdown { if let Some(shutdown_task) = server_to_shutdown.shutdown() { shutdown_task.await; } @@ -1561,116 +1560,138 @@ impl Project { .detach(); } - fn on_lsp_event( + fn on_lsp_diagnostics_published( &mut self, - language_server_id: usize, - event: LanguageServerEvent, - language: &Arc, + server_id: usize, + mut params: lsp::PublishDiagnosticsParams, + adapter: &Arc, + disk_based_diagnostics_progress_token: Option<&str>, cx: &mut ModelContext, ) { - let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); - let language_server_status = - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status - } else { + adapter.process_diagnostics(&mut params); + if disk_based_diagnostics_progress_token.is_none() { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + self.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx) + .log_err(); + if disk_based_diagnostics_progress_token.is_none() { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } + + fn on_lsp_progress( + &mut self, + progress: lsp::ProgressParams, + server_id: usize, + disk_based_diagnostics_progress_token: Option<&str>, + cx: &mut ModelContext, + ) { + let token = match progress.token { + lsp::NumberOrString::String(token) => token, + lsp::NumberOrString::Number(token) => { + log::info!("skipping numeric progress token {}", token); return; - }; + } + }; - match event { - LanguageServerEvent::WorkStart { token } => { - if Some(&token) == disk_diagnostics_token { - language_server_status.pending_diagnostic_updates += 1; - if language_server_status.pending_diagnostic_updates == 1 { - self.disk_based_diagnostics_started(cx); + match progress.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + let language_server_status = + if let Some(status) = self.language_server_statuses.get_mut(&server_id) { + status + } else { + return; + }; + + if Some(token.as_str()) == disk_based_diagnostics_progress_token { + language_server_status.pending_diagnostic_updates += 1; + if language_server_status.pending_diagnostic_updates == 1 { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + } else { + self.on_lsp_work_start(server_id, token.clone(), cx); self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, + server_id, + proto::update_language_server::Variant::WorkStart( + proto::LspWorkStart { token }, ), ); } - } else { - self.on_lsp_work_start(language_server_id, token.clone(), cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { - token, - }), - ); } - } - LanguageServerEvent::WorkProgress { token, progress } => { - if Some(&token) != disk_diagnostics_token { - self.on_lsp_work_progress( - language_server_id, - token.clone(), - progress.clone(), - cx, - ); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkProgress( - proto::LspWorkProgress { - token, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), + lsp::WorkDoneProgress::Report(report) => { + if Some(token.as_str()) != disk_based_diagnostics_progress_token { + self.on_lsp_work_progress( + server_id, + token.clone(), + LanguageServerProgress { + message: report.message.clone(), + percentage: report.percentage.map(|p| p as usize), + last_update_at: Instant::now(), }, - ), - ); - } - } - LanguageServerEvent::WorkEnd { token } => { - if Some(&token) == disk_diagnostics_token { - language_server_status.pending_diagnostic_updates -= 1; - if language_server_status.pending_diagnostic_updates == 0 { - self.disk_based_diagnostics_finished(cx); + cx, + ); self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, + server_id, + proto::update_language_server::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message: report.message, + percentage: report.percentage.map(|p| p as u32), + }, ), ); } - } else { - self.on_lsp_work_end(language_server_id, token.clone(), cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { - token, - }), - ); } - } - LanguageServerEvent::DiagnosticsUpdate(mut params) => { - language.process_diagnostics(&mut params); + lsp::WorkDoneProgress::End(_) => { + if Some(token.as_str()) == disk_based_diagnostics_progress_token { + let language_server_status = if let Some(status) = + self.language_server_statuses.get_mut(&server_id) + { + status + } else { + return; + }; - if disk_diagnostics_token.is_none() { - self.disk_based_diagnostics_started(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - ), - ); - } - self.update_diagnostics( - params, - language - .disk_based_diagnostic_sources() - .unwrap_or(&Default::default()), - cx, - ) - .log_err(); - if disk_diagnostics_token.is_none() { - self.disk_based_diagnostics_finished(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ); + language_server_status.pending_diagnostic_updates -= 1; + if language_server_status.pending_diagnostic_updates == 0 { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } else { + self.on_lsp_work_end(server_id, token.clone(), cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { + token, + }), + ); + } } - } + }, } } @@ -1718,6 +1739,40 @@ impl Project { } } + async fn on_lsp_workspace_edit( + this: WeakModelHandle, + params: lsp::ApplyWorkspaceEditParams, + server_id: usize, + adapter: Arc, + language_server: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project project closed"))?; + let transaction = Self::deserialize_workspace_edit( + this.clone(), + params.edit, + true, + adapter.clone(), + language_server.clone(), + &mut cx, + ) + .await + .log_err(); + this.update(&mut cx, |this, _| { + if let Some(transaction) = transaction { + this.last_workspace_edits_by_language_server + .insert(server_id, transaction); + } + }); + Ok(lsp::ApplyWorkspaceEditResponse { + applied: true, + failed_change: None, + failure_reason: None, + }) + } + fn broadcast_language_server_update( &self, language_server_id: usize, @@ -1735,7 +1790,7 @@ impl Project { } pub fn set_language_server_settings(&mut self, settings: serde_json::Value) { - for server in self.language_servers.values() { + for (_, server) in self.language_servers.values() { server .notify::( lsp::DidChangeConfigurationParams { @@ -1756,7 +1811,7 @@ impl Project { pub fn update_diagnostics( &mut self, params: lsp::PublishDiagnosticsParams, - disk_based_sources: &HashSet, + disk_based_sources: &[&str], cx: &mut ModelContext, ) -> Result<()> { let abs_path = params @@ -1799,8 +1854,9 @@ impl Project { ); } else { let group_id = post_inc(&mut next_group_id); - let is_disk_based = - source.map_or(false, |source| disk_based_sources.contains(source)); + let is_disk_based = source.map_or(false, |source| { + disk_based_sources.contains(&source.as_str()) + }); sources_by_group_id.insert(group_id, source); primary_diagnostic_group_ids @@ -1973,6 +2029,70 @@ impl Project { Ok(()) } + pub fn reload_buffers( + &self, + buffers: HashSet>, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + let mut local_buffers = Vec::new(); + let mut remote_buffers = None; + for buffer_handle in buffers { + let buffer = buffer_handle.read(cx); + if buffer.is_dirty() { + if let Some(file) = File::from_dyn(buffer.file()) { + if file.is_local() { + local_buffers.push(buffer_handle); + } else { + remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); + } + } + } + } + + let remote_buffers = self.remote_id().zip(remote_buffers); + let client = self.client.clone(); + + cx.spawn(|this, mut cx| async move { + let mut project_transaction = ProjectTransaction::default(); + + if let Some((project_id, remote_buffers)) = remote_buffers { + let response = client + .request(proto::ReloadBuffers { + project_id, + buffer_ids: remote_buffers + .iter() + .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) + .collect(), + }) + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project_transaction = this + .update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + }) + .await?; + } + + for buffer in local_buffers { + let transaction = buffer + .update(&mut cx, |buffer, cx| buffer.reload(cx)) + .await?; + buffer.update(&mut cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + project_transaction.0.insert(cx.handle(), transaction); + } + }); + } + + Ok(project_transaction) + }) + } + pub fn format( &self, buffers: HashSet>, @@ -1985,7 +2105,7 @@ impl Project { let buffer = buffer_handle.read(cx); if let Some(file) = File::from_dyn(buffer.file()) { if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { - if let Some(server) = self.language_server_for_buffer(buffer, cx) { + if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { local_buffers.push((buffer_handle, buffer_abs_path, server.clone())); } } else { @@ -2034,7 +2154,12 @@ impl Project { language_server .request::(lsp::DocumentFormattingParams { text_document, - options: Default::default(), + options: lsp::FormattingOptions { + tab_size: 4, + insert_spaces: true, + insert_final_newline: Some(true), + ..Default::default() + }, work_done_progress_params: Default::default(), }) .await? @@ -2044,15 +2169,19 @@ impl Project { .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) { let buffer_start = lsp::Position::new(0, 0); - let buffer_end = buffer - .read_with(&cx, |buffer, _| buffer.max_point_utf16()) - .to_lsp_position(); + let buffer_end = + buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16())); language_server .request::( lsp::DocumentRangeFormattingParams { text_document, range: lsp::Range::new(buffer_start, buffer_end), - options: Default::default(), + options: lsp::FormattingOptions { + tab_size: 4, + insert_spaces: true, + insert_final_newline: Some(true), + ..Default::default() + }, work_done_progress_params: Default::default(), }, ) @@ -2122,25 +2251,24 @@ impl Project { pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { if self.is_local() { let mut language_servers = HashMap::default(); - for ((worktree_id, language_name), language_server) in self.language_servers.iter() { - if let Some((worktree, language)) = self + for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() { + if let Some(worktree) = self .worktree_for_id(*worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) - .zip(self.languages.get_language(language_name)) { language_servers .entry(Arc::as_ptr(language_server)) .or_insert(( + lsp_adapter.clone(), language_server.clone(), *worktree_id, worktree.abs_path().clone(), - language.clone(), )); } } let mut requests = Vec::new(); - for (language_server, _, _, _) in language_servers.values() { + for (_, language_server, _, _) in language_servers.values() { requests.push(language_server.request::( lsp::WorkspaceSymbolParams { query: query.to_string(), @@ -2155,7 +2283,7 @@ impl Project { let mut symbols = Vec::new(); if let Some(this) = this.upgrade(&cx) { this.read_with(&cx, |this, cx| { - for ((_, source_worktree_id, worktree_abs_path, language), lsp_symbols) in + for ((adapter, _, source_worktree_id, worktree_abs_path), lsp_symbols) in language_servers.into_values().zip(responses) { symbols.extend(lsp_symbols.into_iter().flatten().filter_map( @@ -2172,8 +2300,13 @@ impl Project { path = relativize_path(&worktree_abs_path, &abs_path); } - let label = language - .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) + let label = this + .languages + .select_language(&path) + .and_then(|language| { + language + .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) + }) .unwrap_or_else(|| { CodeLabel::plain(lsp_symbol.name.clone(), None) }); @@ -2182,7 +2315,7 @@ impl Project { Some(Symbol { source_worktree_id, worktree_id, - language_name: language.name().to_string(), + language_server_name: adapter.name(), name: lsp_symbol.name, kind: lsp_symbol.kind, label, @@ -2229,9 +2362,9 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let language_server = if let Some(server) = self.language_servers.get(&( + let (lsp_adapter, language_server) = if let Some(server) = self.language_servers.get(&( symbol.source_worktree_id, - Arc::from(symbol.language_name.as_str()), + symbol.language_server_name.clone(), )) { server.clone() } else { @@ -2256,12 +2389,7 @@ impl Project { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp( - symbol_uri, - Arc::from(symbol.language_name.as_str()), - language_server, - cx, - ) + self.open_local_buffer_via_lsp(symbol_uri, lsp_adapter, language_server, cx) } else if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::OpenBufferForSymbol { project_id, @@ -2302,7 +2430,7 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(source_buffer, cx) { server.clone() } else { @@ -2316,7 +2444,7 @@ impl Project { lsp::TextDocumentIdentifier::new( lsp::Url::from_file_path(buffer_abs_path).unwrap(), ), - position.to_lsp_position(), + point_to_lsp(position), ), context: Default::default(), work_done_progress_params: Default::default(), @@ -2338,11 +2466,26 @@ impl Project { Ok(completions .into_iter() .filter_map(|lsp_completion| { - let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? { - lsp::CompletionTextEdit::Edit(edit) => { + let (old_range, new_text) = match lsp_completion.text_edit.as_ref() { + Some(lsp::CompletionTextEdit::Edit(edit)) => { (range_from_lsp(edit.range), edit.new_text.clone()) } - lsp::CompletionTextEdit::InsertAndReplace(_) => { + None => { + let clipped_position = + this.clip_point_utf16(position, Bias::Left); + if position != clipped_position { + log::info!("completion out of expected range"); + return None; + } + ( + this.common_prefix_at( + clipped_position, + &lsp_completion.label, + ), + lsp_completion.label.clone(), + ) + } + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; } @@ -2367,6 +2510,7 @@ impl Project { lsp_completion, }) } else { + log::info!("completion out of expected range"); None } }) @@ -2414,7 +2558,8 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + { server.clone() } else { return Task::ready(Ok(Default::default())); @@ -2484,7 +2629,7 @@ impl Project { } } - pub fn code_actions( + pub fn code_actions( &self, buffer_handle: &ModelHandle, range: Range, @@ -2492,6 +2637,11 @@ impl Project { ) -> Task>> { let buffer_handle = buffer_handle.clone(); let buffer = buffer_handle.read(cx); + let snapshot = buffer.snapshot(); + let relevant_diagnostics = snapshot + .diagnostics_in_range::(range.to_offset(&snapshot), false) + .map(|entry| entry.to_lsp_diagnostic_stub()) + .collect(); let buffer_id = buffer.remote_id(); let worktree; let buffer_abs_path; @@ -2505,16 +2655,14 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + { server.clone() } else { return Task::ready(Ok(Default::default())); }; - let lsp_range = lsp::Range::new( - range.start.to_point_utf16(buffer).to_lsp_position(), - range.end.to_point_utf16(buffer).to_lsp_position(), - ); + let lsp_range = range_to_lsp(range.to_point_utf16(buffer)); cx.foreground().spawn(async move { if !lang_server.capabilities().code_action_provider.is_some() { return Ok(Default::default()); @@ -2529,11 +2677,12 @@ impl Project { work_done_progress_params: Default::default(), partial_result_params: Default::default(), context: lsp::CodeActionContext { - diagnostics: Default::default(), + diagnostics: relevant_diagnostics, only: Some(vec![ lsp::CodeActionKind::QUICKFIX, lsp::CodeActionKind::REFACTOR, lsp::CodeActionKind::REFACTOR_EXTRACT, + lsp::CodeActionKind::SOURCE, ]), }, }) @@ -2592,16 +2741,12 @@ impl Project { ) -> Task> { if self.is_local() { let buffer = buffer_handle.read(cx); - let lang_name = if let Some(lang) = buffer.language() { - lang.name() - } else { - return Task::ready(Ok(Default::default())); - }; - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { - server.clone() - } else { - return Task::ready(Ok(Default::default())); - }; + let (lsp_adapter, lang_server) = + if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; let range = action.range.to_point_utf16(buffer); cx.spawn(|this, mut cx| async move { @@ -2612,11 +2757,7 @@ impl Project { .and_then(|d| d.get_mut("codeActionParams")) .and_then(|d| d.get_mut("range")) { - *lsp_range = serde_json::to_value(&lsp::Range::new( - range.start.to_lsp_position(), - range.end.to_lsp_position(), - )) - .unwrap(); + *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap(); action.lsp_action = lang_server .request::(action.lsp_action) .await?; @@ -2638,11 +2779,28 @@ impl Project { this, edit, push_to_history, - lang_name, + lsp_adapter, lang_server, &mut cx, ) .await + } else if let Some(command) = action.lsp_action.command { + this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()); + }); + lang_server + .request::(lsp::ExecuteCommandParams { + command: command.command, + arguments: command.arguments.unwrap_or_default(), + ..Default::default() + }) + .await?; + Ok(this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()) + .unwrap_or_default() + })) } else { Ok(ProjectTransaction::default()) } @@ -2674,7 +2832,7 @@ impl Project { this: ModelHandle, edit: lsp::WorkspaceEdit, push_to_history: bool, - language_name: Arc, + lsp_adapter: Arc, language_server: Arc, cx: &mut AsyncAppContext, ) -> Result { @@ -2751,7 +2909,7 @@ impl Project { .update(cx, |this, cx| { this.open_local_buffer_via_lsp( op.text_document.uri, - language_name.clone(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -3046,7 +3204,7 @@ impl Project { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, language_server)) = + if let Some((file, (_, language_server))) = file.zip(self.language_server_for_buffer(buffer, cx).cloned()) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); @@ -3667,6 +3825,35 @@ impl Project { }) } + async fn handle_reload_buffers( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let reload = this.update(&mut cx, |this, cx| { + let mut buffers = HashSet::default(); + for buffer_id in &envelope.payload.buffer_ids { + buffers.insert( + this.opened_buffers + .get(buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, + ); + } + Ok::<_, anyhow::Error>(this.reload_buffers(buffers, false, cx)) + })?; + + let project_transaction = reload.await?; + let project_transaction = this.update(&mut cx, |this, cx| { + this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx) + }); + Ok(proto::ReloadBuffersResponse { + transaction: Some(project_transaction), + }) + } + async fn handle_format_buffers( this: ModelHandle, envelope: TypedEnvelope, @@ -4144,9 +4331,8 @@ impl Project { } fn deserialize_symbol(&self, serialized_symbol: proto::Symbol) -> Result { - let language = self - .languages - .get_language(&serialized_symbol.language_name); + let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id); + let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id); let start = serialized_symbol .start .ok_or_else(|| anyhow!("invalid start"))?; @@ -4154,15 +4340,17 @@ impl Project { .end .ok_or_else(|| anyhow!("invalid end"))?; let kind = unsafe { mem::transmute(serialized_symbol.kind) }; + let path = PathBuf::from(serialized_symbol.path); + let language = self.languages.select_language(&path); Ok(Symbol { - source_worktree_id: WorktreeId::from_proto(serialized_symbol.source_worktree_id), - worktree_id: WorktreeId::from_proto(serialized_symbol.worktree_id), - language_name: serialized_symbol.language_name.clone(), + source_worktree_id, + worktree_id, + language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), label: language .and_then(|language| language.label_for_symbol(&serialized_symbol.name, kind)) .unwrap_or_else(|| CodeLabel::plain(serialized_symbol.name.clone(), None)), name: serialized_symbol.name, - path: PathBuf::from(serialized_symbol.path), + path, range: PointUtf16::new(start.row, start.column)..PointUtf16::new(end.row, end.column), kind, signature: serialized_symbol @@ -4407,10 +4595,11 @@ impl Project { &self, buffer: &Buffer, cx: &AppContext, - ) -> Option<&Arc> { + ) -> Option<&(Arc, Arc)> { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); - self.language_servers.get(&(worktree_id, language.name())) + self.language_servers + .get(&(worktree_id, language.lsp_adapter()?.name())) } else { None } @@ -4524,7 +4713,7 @@ impl Entity for Project { let shutdown_futures = self .language_servers .drain() - .filter_map(|(_, server)| server.shutdown()) + .filter_map(|(_, (_, server))| server.shutdown()) .collect::>(); Some( async move { @@ -4595,7 +4784,7 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { source_worktree_id: symbol.source_worktree_id.to_proto(), worktree_id: symbol.worktree_id.to_proto(), - language_name: symbol.language_name.clone(), + language_server_name: symbol.language_server_name.0.to_string(), name: symbol.name.clone(), kind: unsafe { mem::transmute(symbol.kind) }, path: symbol.path.to_string_lossy().to_string(), @@ -4653,7 +4842,7 @@ mod tests { use futures::{future, StreamExt}; use gpui::test::subscribe; use language::{ - tree_sitter_rust, Diagnostic, LanguageConfig, LanguageServerConfig, OffsetRangeExt, Point, + tree_sitter_rust, Diagnostic, FakeLspAdapter, LanguageConfig, OffsetRangeExt, Point, ToPoint, }; use lsp::Url; @@ -4730,41 +4919,44 @@ mod tests { async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (mut rust_lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake(); - let (mut json_lsp_config, mut fake_json_servers) = LanguageServerConfig::fake(); - rust_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), "::".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - json_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - - let rust_language = Arc::new(Language::new( + let mut rust_language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(rust_lsp_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); - let json_language = Arc::new(Language::new( + ); + let mut json_language = Language::new( LanguageConfig { name: "JSON".into(), path_suffixes: vec!["json".to_string()], - language_server: Some(json_lsp_config), ..Default::default() }, None, - )); + ); + let mut fake_rust_servers = rust_language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-rust-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), "::".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); + let mut fake_json_servers = json_language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-json-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4780,8 +4972,8 @@ mod tests { let project = Project::test(fs, cx); project.update(cx, |project, _| { - project.languages.add(rust_language); - project.languages.add(json_language); + project.languages.add(Arc::new(rust_language)); + project.languages.add(Arc::new(json_language)); }); let worktree_id = project @@ -4939,9 +5131,9 @@ mod tests { }); let mut rust_shutdown_requests = fake_rust_server - .handle_request::(|_, _| future::ready(())); + .handle_request::(|_, _| future::ready(Ok(()))); let mut json_shutdown_requests = fake_json_server - .handle_request::(|_, _| future::ready(())); + .handle_request::(|_, _| future::ready(Ok(()))); futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next()); let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); @@ -5008,21 +5200,20 @@ mod tests { async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let progress_token = language_server_config - .disk_based_diagnostics_progress_token - .clone() - .unwrap(); - - let language = Arc::new(Language::new( + let progress_token = "the-progress-token"; + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + disk_based_diagnostics_progress_token: Some(progress_token), + disk_based_diagnostics_sources: &["disk"], + ..Default::default() + }); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -5035,7 +5226,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -5059,15 +5250,15 @@ mod tests { let mut events = subscribe(&project, cx); let mut fake_server = fake_servers.next().await.unwrap(); - fake_server.start_progress(&progress_token).await; + fake_server.start_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsStarted ); - fake_server.start_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; - fake_server.start_progress(&progress_token).await; + fake_server.start_progress(progress_token).await; + fake_server.end_progress(progress_token).await; + fake_server.start_progress(progress_token).await; fake_server.notify::( lsp::PublishDiagnosticsParams { @@ -5086,8 +5277,8 @@ mod tests { Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) ); - fake_server.end_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; + fake_server.end_progress(progress_token).await; + fake_server.end_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsUpdated @@ -5127,19 +5318,18 @@ mod tests { async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); - lsp_config - .disk_based_diagnostic_sources - .insert("disk".to_string()); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(lsp_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + disk_based_diagnostics_sources: &["disk"], + ..Default::default() + }); let text = " fn a() { A } @@ -5152,7 +5342,7 @@ mod tests { fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -5501,16 +5691,15 @@ mod tests { async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (lsp_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(lsp_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let text = " fn a() { @@ -5535,7 +5724,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -5851,16 +6040,15 @@ mod tests { #[gpui::test] async fn test_definition(cx: &mut gpui::TestAppContext) { - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -5873,9 +6061,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); - }); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -5888,19 +6074,11 @@ mod tests { .await; let buffer = project - .update(cx, |project, cx| { - project.open_buffer( - ProjectPath { - worktree_id, - path: Path::new("").into(), - }, - cx, - ) - }) + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + let fake_server = fake_servers.next().await.unwrap(); fake_server.handle_request::(|params, _| async move { let params = params.text_document_position_params; assert_eq!( @@ -5909,9 +6087,11 @@ mod tests { ); assert_eq!(params.position, lsp::Position::new(0, 22)); - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/dir/a.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/dir/a.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + ), ))) }); @@ -5963,6 +6143,133 @@ mod tests { } } + #[gpui::test(iterations = 10)] + async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { + let mut language = Language::new( + LanguageConfig { + name: "TypeScript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "a", + }), + ) + .await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let (tree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let buffer = project + .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx)) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + // Language server returns code actions that contain commands, and not edits. + let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); + fake_server + .handle_request::(|_, _| async move { + Ok(Some(vec![ + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "The code action".into(), + command: Some(lsp::Command { + title: "The command".into(), + command: "_the/command".into(), + arguments: Some(vec![json!("the-argument")]), + }), + ..Default::default() + }), + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "two".into(), + ..Default::default() + }), + ])) + }) + .next() + .await; + + let action = actions.await.unwrap()[0].clone(); + let apply = project.update(cx, |project, cx| { + project.apply_code_action(buffer.clone(), action, true, cx) + }); + + // Resolving the code action does not populate its edits. In absence of + // edits, we must execute the given command. + fake_server.handle_request::( + |action, _| async move { Ok(action) }, + ); + + // While executing the command, the language server sends the editor + // a `workspaceEdit` request. + fake_server + .handle_request::({ + let fake = fake_server.clone(); + move |params, _| { + assert_eq!(params.command, "_the/command"); + let fake = fake.clone(); + async move { + fake.server + .request::( + lsp::ApplyWorkspaceEditParams { + label: None, + edit: lsp::WorkspaceEdit { + changes: Some( + [( + lsp::Url::from_file_path("/dir/a.ts").unwrap(), + vec![lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "X".into(), + }], + )] + .into_iter() + .collect(), + ), + ..Default::default() + }, + }, + ) + .await + .unwrap(); + Ok(Some(json!(null))) + } + } + }) + .next() + .await; + + // Applying the code action returns a project transaction containing the edits + // sent by the language server in its `workspaceEdit` request. + let transaction = apply.await.unwrap(); + assert!(transaction.0.contains_key(&buffer)); + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "Xa"); + buffer.undo(cx); + assert_eq!(buffer.text(), "a"); + }); + } + #[gpui::test] async fn test_save_file(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.background()); @@ -6657,9 +6964,7 @@ mod tests { }; project - .update(cx, |p, cx| { - p.update_diagnostics(message, &Default::default(), cx) - }) + .update(cx, |p, cx| p.update_diagnostics(message, &[], cx)) .unwrap(); let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot()); @@ -6787,16 +7092,15 @@ mod tests { async fn test_rename(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -6809,9 +7113,7 @@ mod tests { .await; let project = Project::test(fs.clone(), cx); - project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); - }); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -6830,7 +7132,7 @@ mod tests { .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + let fake_server = fake_servers.next().await.unwrap(); let response = project.update(cx, |project, cx| { project.prepare_rename(buffer.clone(), 7, cx) @@ -6839,10 +7141,10 @@ mod tests { .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( lsp::Position::new(0, 6), lsp::Position::new(0, 9), - ))) + )))) }) .next() .await @@ -6865,7 +7167,7 @@ mod tests { lsp::Position::new(0, 7) ); assert_eq!(params.new_name, "THREE"); - Some(lsp::WorkspaceEdit { + Ok(Some(lsp::WorkspaceEdit { changes: Some( [ ( @@ -6902,7 +7204,7 @@ mod tests { .collect(), ), ..Default::default() - }) + })) }) .next() .await diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9d25e66190b14bc7d4624885ec7da3dc57acb61b..f61d6275c91971ac568bf157b43ac98d36e1fccf 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -48,43 +48,45 @@ message Envelope { SaveBuffer save_buffer = 40; BufferSaved buffer_saved = 41; BufferReloaded buffer_reloaded = 42; - FormatBuffers format_buffers = 43; - FormatBuffersResponse format_buffers_response = 44; - GetCompletions get_completions = 45; - GetCompletionsResponse get_completions_response = 46; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 47; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 48; - GetCodeActions get_code_actions = 49; - GetCodeActionsResponse get_code_actions_response = 50; - ApplyCodeAction apply_code_action = 51; - ApplyCodeActionResponse apply_code_action_response = 52; - PrepareRename prepare_rename = 53; - PrepareRenameResponse prepare_rename_response = 54; - PerformRename perform_rename = 55; - PerformRenameResponse perform_rename_response = 56; - SearchProject search_project = 57; - SearchProjectResponse search_project_response = 58; - - GetChannels get_channels = 59; - GetChannelsResponse get_channels_response = 60; - JoinChannel join_channel = 61; - JoinChannelResponse join_channel_response = 62; - LeaveChannel leave_channel = 63; - SendChannelMessage send_channel_message = 64; - SendChannelMessageResponse send_channel_message_response = 65; - ChannelMessageSent channel_message_sent = 66; - GetChannelMessages get_channel_messages = 67; - GetChannelMessagesResponse get_channel_messages_response = 68; - - UpdateContacts update_contacts = 69; - - GetUsers get_users = 70; - GetUsersResponse get_users_response = 71; - - Follow follow = 72; - FollowResponse follow_response = 73; - UpdateFollowers update_followers = 74; - Unfollow unfollow = 75; + ReloadBuffers reload_buffers = 43; + ReloadBuffersResponse reload_buffers_response = 44; + FormatBuffers format_buffers = 45; + FormatBuffersResponse format_buffers_response = 46; + GetCompletions get_completions = 47; + GetCompletionsResponse get_completions_response = 48; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50; + GetCodeActions get_code_actions = 51; + GetCodeActionsResponse get_code_actions_response = 52; + ApplyCodeAction apply_code_action = 53; + ApplyCodeActionResponse apply_code_action_response = 54; + PrepareRename prepare_rename = 55; + PrepareRenameResponse prepare_rename_response = 56; + PerformRename perform_rename = 57; + PerformRenameResponse perform_rename_response = 58; + SearchProject search_project = 59; + SearchProjectResponse search_project_response = 60; + + GetChannels get_channels = 61; + GetChannelsResponse get_channels_response = 62; + JoinChannel join_channel = 63; + JoinChannelResponse join_channel_response = 64; + LeaveChannel leave_channel = 65; + SendChannelMessage send_channel_message = 66; + SendChannelMessageResponse send_channel_message_response = 67; + ChannelMessageSent channel_message_sent = 68; + GetChannelMessages get_channel_messages = 69; + GetChannelMessagesResponse get_channel_messages_response = 70; + + UpdateContacts update_contacts = 71; + + GetUsers get_users = 72; + GetUsersResponse get_users_response = 73; + + Follow follow = 74; + FollowResponse follow_response = 75; + UpdateFollowers update_followers = 76; + Unfollow unfollow = 77; } } @@ -229,7 +231,7 @@ message GetProjectSymbolsResponse { message Symbol { uint64 source_worktree_id = 1; uint64 worktree_id = 2; - string language_name = 3; + string language_server_name = 3; string name = 4; int32 kind = 5; string path = 6; @@ -299,6 +301,15 @@ message BufferReloaded { Timestamp mtime = 4; } +message ReloadBuffers { + uint64 project_id = 1; + repeated uint64 buffer_ids = 2; +} + +message ReloadBuffersResponse { + ProjectTransaction transaction = 1; +} + message FormatBuffers { uint64 project_id = 1; repeated uint64 buffer_ids = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 59d6773451fd2feebc28b17120e0b50a58de1127..a9f6b80f8e8d7895d705657d023f2b95a7c073a9 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -190,6 +190,8 @@ messages!( (Ping, Foreground), (RegisterProject, Foreground), (RegisterWorktree, Foreground), + (ReloadBuffers, Foreground), + (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), @@ -237,6 +239,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), + (ReloadBuffers, ReloadBuffersResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), @@ -268,6 +271,7 @@ entity_messages!( OpenBufferForSymbol, PerformRename, PrepareRename, + ReloadBuffers, RemoveProjectCollaborator, SaveBuffer, SearchProject, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index cfe780d5118c764a829cf047d288bc1e7a0b590e..9ee18faae2543b93e194f0e3c2a4fbe9e6604a84 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 12; +pub const PROTOCOL_VERSION: u32 = 13; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 65bb07ae46f4d6a1c43b409b6dc45bd40de2f139..745f23154fd98a77b3f20e79f1f4d513f235ebc5 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -280,6 +280,15 @@ impl Item for ProjectSearchView { unreachable!("save_as should not have been called") } + fn reload( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.reload(project, cx)) + } + fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 374aaf6a7ec6820022c1802757cdabf3b51aa946..768432ef75af1958c90e580bc2fec189dc5763e8 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -102,6 +102,7 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) @@ -1088,10 +1089,10 @@ mod tests { }; use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle}; use language::{ - tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, - LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, + range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, + LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope, }; - use lsp; + use lsp::{self, FakeLanguageServer}; use parking_lot::Mutex; use postage::barrier; use project::{ @@ -2039,22 +2040,20 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2261,29 +2260,29 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (mut language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], ..Default::default() - }), + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, ..Default::default() }); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2344,7 +2343,7 @@ mod tests { Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) }); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); buffer_b .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty()) .await; @@ -2370,7 +2369,7 @@ mod tests { lsp::Position::new(0, 14), ); - Some(lsp::CompletionResponse::Array(vec![ + Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), @@ -2397,7 +2396,7 @@ mod tests { insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }, - ])) + ]))) }) .next() .await @@ -2427,7 +2426,7 @@ mod tests { fake_language_server.handle_request::( |params, _| async move { assert_eq!(params.label, "first_method(…)"); - lsp::CompletionItem { + Ok(lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { @@ -2443,7 +2442,7 @@ mod tests { }]), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() - } + }) }, ); @@ -2460,25 +2459,140 @@ mod tests { .await; } + #[gpui::test(iterations = 10)] + async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::test()); + let fs = FakeFs::new(cx_a.background()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.rs": "let one = 1;", + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", true, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); + project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let buffer_b = cx_b + .background() + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .await + .unwrap(); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([4..7], "six", cx); + buffer.edit([10..11], "6", cx); + assert_eq!(buffer.text(), "let six = 6;"); + assert!(buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + buffer_a + .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;") + .await; + + fs.save(Path::new("/a/a.rs"), &Rope::from("let seven = 7;")) + .await + .unwrap(); + buffer_a + .condition(cx_a, |buffer, _| buffer.has_conflict()) + .await; + buffer_b + .condition(cx_b, |buffer, _| buffer.has_conflict()) + .await; + + project_b + .update(cx_b, |project, cx| { + project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx) + }) + .await + .unwrap(); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "let seven = 7;"); + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "let seven = 7;"); + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + + buffer_a.update(cx_a, |buffer, cx| { + // Undoing on the host is a no-op when the reload was initiated by the guest. + buffer.undo(cx); + assert_eq!(buffer.text(), "let seven = 7;"); + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + buffer_b.update(cx_b, |buffer, cx| { + // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared. + buffer.undo(cx); + assert_eq!(buffer.text(), "let six = 6;"); + assert!(buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + } + #[gpui::test(iterations = 10)] async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2534,9 +2648,9 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| async move { - Some(vec![ + Ok(Some(vec![ lsp::TextEdit { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), new_text: "h".to_string(), @@ -2545,7 +2659,7 @@ mod tests { range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)), new_text: "y".to_string(), }, - ]) + ])) }); project_b @@ -2563,7 +2677,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", @@ -2582,18 +2696,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2643,12 +2755,14 @@ mod tests { .unwrap(); // Request the definition of a symbol as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ), ))) }, ); @@ -2675,9 +2789,11 @@ mod tests { // the previous call to `definition`. fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + ), ))) }, ); @@ -2705,7 +2821,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", @@ -2725,18 +2841,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2786,14 +2900,14 @@ mod tests { .unwrap(); // Request references to a symbol as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), "file:///root-1/one.rs" ); - Some(vec![ + Ok(Some(vec![ lsp::Location { uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), range: lsp::Range::new( @@ -2815,7 +2929,7 @@ mod tests { lsp::Position::new(0, 40), ), }, - ]) + ])) }, ); @@ -2967,16 +3081,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - lang_registry.add(Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3026,7 +3140,7 @@ mod tests { .unwrap(); // Request document highlights as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| async move { assert_eq!( @@ -3041,7 +3155,7 @@ mod tests { params.text_document_position_params.position, lsp::Position::new(0, 34) ); - Some(vec![ + Ok(Some(vec![ lsp::DocumentHighlight { kind: Some(lsp::DocumentHighlightKind::WRITE), range: lsp::Range::new( @@ -3063,7 +3177,7 @@ mod tests { lsp::Position::new(0, 47), ), }, - ]) + ])) }, ); @@ -3092,7 +3206,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/code", @@ -3112,18 +3226,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3172,11 +3284,11 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { #[allow(deprecated)] - Some(vec![lsp::SymbolInformation { + Ok(Some(vec![lsp::SymbolInformation { name: "TWO".into(), location: lsp::Location { uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), @@ -3186,7 +3298,7 @@ mod tests { tags: None, container_name: None, deprecated: None, - }]) + }])) }, ); @@ -3231,7 +3343,7 @@ mod tests { mut rng: StdRng, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root", @@ -3244,19 +3356,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3305,12 +3414,14 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ), ))) }, ); @@ -3337,23 +3448,21 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3428,7 +3537,7 @@ mod tests { ); assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0)); - None + Ok(None) }) .next() .await; @@ -3448,7 +3557,7 @@ mod tests { assert_eq!(params.range.start, lsp::Position::new(1, 31)); assert_eq!(params.range.end, lsp::Position::new(1, 31)); - Some(vec![lsp::CodeActionOrCommand::CodeAction( + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( lsp::CodeAction { title: "Inline into all callers".to_string(), edit: Some(lsp::WorkspaceEdit { @@ -3490,7 +3599,7 @@ mod tests { })), ..Default::default() }, - )]) + )])) }) .next() .await; @@ -3513,7 +3622,7 @@ mod tests { .unwrap(); fake_language_server.handle_request::( |_, _| async move { - lsp::CodeAction { + Ok(lsp::CodeAction { title: "Inline into all callers".to_string(), edit: Some(lsp::WorkspaceEdit { changes: Some( @@ -3545,7 +3654,7 @@ mod tests { ..Default::default() }), ..Default::default() - } + }) }, ); @@ -3573,23 +3682,21 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3654,7 +3761,7 @@ mod tests { .unwrap() .downcast::() .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); // Move cursor to a location that can be renamed. let prepare_rename = editor_b.update(cx_b, |editor, cx| { @@ -3666,10 +3773,10 @@ mod tests { .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( lsp::Position::new(0, 6), lsp::Position::new(0, 9), - ))) + )))) }) .next() .await @@ -3703,7 +3810,7 @@ mod tests { lsp::Position::new(0, 6) ); assert_eq!(params.new_name, "THREE"); - Some(lsp::WorkspaceEdit { + Ok(Some(lsp::WorkspaceEdit { changes: Some( [ ( @@ -3740,7 +3847,7 @@ mod tests { .collect(), ), ..Default::default() - }) + })) }) .next() .await @@ -4838,7 +4945,7 @@ mod tests { let rng = Arc::new(Mutex::new(rng)); let guest_lang_registry = Arc::new(LanguageRegistry::test()); - let (language_server_config, _fake_language_servers) = LanguageServerConfig::fake(); + let host_language_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4852,6 +4959,7 @@ mod tests { let operations = Rc::new(Cell::new(0)); let mut server = TestServer::start(cx.foreground(), cx.background()).await; let mut clients = Vec::new(); + let files = Arc::new(Mutex::new(Vec::new())); let mut next_entity_id = 100000; let mut host_cx = TestAppContext::new( @@ -4868,7 +4976,7 @@ mod tests { Project::local( host.client.clone(), host.user_store.clone(), - Arc::new(LanguageRegistry::test()), + host_language_registry.clone(), fs.clone(), cx, ) @@ -4891,9 +4999,136 @@ mod tests { .await .unwrap(); + // Set up fake language servers. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-fake-language-server", + capabilities: lsp::LanguageServer::full_capabilities(), + initializer: Some(Box::new({ + let rng = rng.clone(); + let files = files.clone(); + let project = host_project.downgrade(); + 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 files = files.clone(); + let rng = rng.clone(); + move |_, _| { + let files = files.clone(); + let rng = rng.clone(); + async move { + let files = files.lock(); + let mut rng = rng.lock(); + let count = rng.gen_range::(1..3); + let files = (0..count) + .map(|_| files.choose(&mut *rng).unwrap()) + .collect::>(); + 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::({ + let rng = rng.clone(); + let project = project.clone(); + move |params, mut cx| { + let highlights = if let Some(project) = project.upgrade(&cx) { + project.update(&mut cx, |project, cx| { + let path = params + .text_document_position_params + .text_document + .uri + .to_file_path() + .unwrap(); + let (worktree, relative_path) = + project.find_local_worktree(&path, cx)?; + let project_path = + ProjectPath::from((worktree.read(cx).id(), relative_path)); + let buffer = + project.get_open_buffer(&project_path, cx)?.read(cx); + + let mut highlights = Vec::new(); + let highlight_count = rng.lock().gen_range(1..=5); + let mut prev_end = 0; + for _ in 0..highlight_count { + let range = + buffer.random_byte_range(prev_end, &mut *rng.lock()); + + highlights.push(lsp::DocumentHighlight { + range: range_to_lsp(range.to_point_utf16(buffer)), + kind: Some(lsp::DocumentHighlightKind::READ), + }); + prev_end = range.end; + } + Some(highlights) + }) + } else { + None + }; + async move { Ok(highlights) } + } + }); + } + })), + ..Default::default() + }); + host_language_registry.add(Arc::new(language)); + clients.push(cx.foreground().spawn(host.simulate_host( host_project, - language_server_config, + files, operations.clone(), max_operations, rng.clone(), @@ -5324,264 +5559,128 @@ mod tests { }) } - fn simulate_host( + async fn simulate_host( mut self, project: ModelHandle, - mut language_server_config: LanguageServerConfig, + files: Arc>>, operations: Rc>, max_operations: usize, rng: Arc>, mut cx: TestAppContext, - ) -> impl Future { - let files: Arc>> = Default::default(); - - // Set up a fake language server. - language_server_config.set_fake_initializer({ - let rng = rng.clone(); - let files = files.clone(); - let project = project.downgrade(); - move |fake_server| { - fake_server.handle_request::( - |_, _| async move { - 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 { - Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), - ..Default::default() - }, - )]) - }, - ); - - fake_server.handle_request::( - |params, _| async move { - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( - params.position, - params.position, - ))) - }, - ); + ) -> (Self, TestAppContext) { + let fs = project.read_with(&cx, |project, _| project.fs().clone()); + while operations.get() < max_operations { + operations.set(operations.get() + 1); - fake_server.handle_request::({ - let files = files.clone(); - let rng = rng.clone(); - move |_, _| { - let files = files.clone(); - let rng = rng.clone(); - async move { - let files = files.lock(); - let mut rng = rng.lock(); - let count = rng.gen_range::(1..3); - let files = (0..count) - .map(|_| files.choose(&mut *rng).unwrap()) - .collect::>(); - log::info!("LSP: Returning definitions in files {:?}", &files); - Some(lsp::GotoDefinitionResponse::Array( - files - .into_iter() - .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), - range: Default::default(), - }) - .collect(), - )) + let distribution = rng.lock().gen_range::(0..100); + match distribution { + 0..=20 if !files.lock().is_empty() => { + let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let mut path = path.as_path(); + while let Some(parent_path) = path.parent() { + path = parent_path; + if rng.lock().gen() { + break; } } - }); - fake_server.handle_request::({ - let rng = rng.clone(); - let project = project.clone(); - move |params, mut cx| { - let highlights = if let Some(project) = project.upgrade(&cx) { - project.update(&mut cx, |project, cx| { - let path = params - .text_document_position_params - .text_document - .uri - .to_file_path() - .unwrap(); - let (worktree, relative_path) = - project.find_local_worktree(&path, cx)?; - let project_path = - ProjectPath::from((worktree.read(cx).id(), relative_path)); - let buffer = - project.get_open_buffer(&project_path, cx)?.read(cx); - - let mut highlights = Vec::new(); - let highlight_count = rng.lock().gen_range(1..=5); - let mut prev_end = 0; - for _ in 0..highlight_count { - let range = - buffer.random_byte_range(prev_end, &mut *rng.lock()); - let start = buffer - .offset_to_point_utf16(range.start) - .to_lsp_position(); - let end = buffer - .offset_to_point_utf16(range.end) - .to_lsp_position(); - highlights.push(lsp::DocumentHighlight { - range: lsp::Range::new(start, end), - kind: Some(lsp::DocumentHighlightKind::READ), - }); - prev_end = range.end; - } - Some(highlights) - }) - } else { - None - }; - async move { highlights } + log::info!("Host: find/create local worktree {:?}", path); + let find_or_create_worktree = project.update(&mut cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }); + let find_or_create_worktree = async move { + find_or_create_worktree.await.unwrap(); + }; + if rng.lock().gen() { + cx.background().spawn(find_or_create_worktree).detach(); + } else { + find_or_create_worktree.await; } - }); - } - }); - - project.update(&mut cx, |project, _| { - project.languages().add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - None, - ))); - }); - - async move { - let fs = project.read_with(&cx, |project, _| project.fs().clone()); - while operations.get() < max_operations { - operations.set(operations.get() + 1); - - let distribution = rng.lock().gen_range::(0..100); - match distribution { - 0..=20 if !files.lock().is_empty() => { - let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let mut path = path.as_path(); - while let Some(parent_path) = path.parent() { - path = parent_path; - if rng.lock().gen() { - break; - } - } + } + 10..=80 if !files.lock().is_empty() => { + let buffer = if self.buffers.is_empty() || rng.lock().gen() { + let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let (worktree, path) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree(file.clone(), true, cx) + }) + .await + .unwrap(); + let project_path = + worktree.read_with(&cx, |worktree, _| (worktree.id(), path)); + log::info!( + "Host: opening path {:?}, worktree {}, relative_path {:?}", + file, + project_path.0, + project_path.1 + ); + let buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer(project_path, cx) + }) + .await + .unwrap(); + self.buffers.insert(buffer.clone()); + buffer + } else { + self.buffers + .iter() + .choose(&mut *rng.lock()) + .unwrap() + .clone() + }; - log::info!("Host: find/create local worktree {:?}", path); - let find_or_create_worktree = project.update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) + if rng.lock().gen_bool(0.1) { + cx.update(|cx| { + log::info!( + "Host: dropping buffer {:?}", + buffer.read(cx).file().unwrap().full_path(cx) + ); + self.buffers.remove(&buffer); + drop(buffer); }); - let find_or_create_worktree = async move { - find_or_create_worktree.await.unwrap(); - }; - if rng.lock().gen() { - cx.background().spawn(find_or_create_worktree).detach(); - } else { - find_or_create_worktree.await; - } - } - 10..=80 if !files.lock().is_empty() => { - let buffer = if self.buffers.is_empty() || rng.lock().gen() { - let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let (worktree, path) = project - .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree( - file.clone(), - true, - cx, - ) - }) - .await - .unwrap(); - let project_path = - worktree.read_with(&cx, |worktree, _| (worktree.id(), path)); + } else { + buffer.update(&mut cx, |buffer, cx| { log::info!( - "Host: opening path {:?}, worktree {}, relative_path {:?}", - file, - project_path.0, - project_path.1 + "Host: updating buffer {:?} ({})", + buffer.file().unwrap().full_path(cx), + buffer.remote_id() ); - let buffer = project - .update(&mut cx, |project, cx| { - project.open_buffer(project_path, cx) - }) - .await - .unwrap(); - self.buffers.insert(buffer.clone()); - buffer - } else { - self.buffers - .iter() - .choose(&mut *rng.lock()) - .unwrap() - .clone() - }; - - if rng.lock().gen_bool(0.1) { - cx.update(|cx| { - log::info!( - "Host: dropping buffer {:?}", - buffer.read(cx).file().unwrap().full_path(cx) - ); - self.buffers.remove(&buffer); - drop(buffer); - }); - } else { - buffer.update(&mut cx, |buffer, cx| { - log::info!( - "Host: updating buffer {:?} ({})", - buffer.file().unwrap().full_path(cx), - buffer.remote_id() - ); - buffer.randomly_edit(&mut *rng.lock(), 5, cx) - }); - } + buffer.randomly_edit(&mut *rng.lock(), 5, cx) + }); } - _ => loop { - let path_component_count = rng.lock().gen_range::(1..=5); - let mut path = PathBuf::new(); - path.push("/"); - for _ in 0..path_component_count { - let letter = rng.lock().gen_range(b'a'..=b'z'); - path.push(std::str::from_utf8(&[letter]).unwrap()); - } - path.set_extension("rs"); - let parent_path = path.parent().unwrap(); - - log::info!("Host: creating file {:?}", path,); - - if fs.create_dir(&parent_path).await.is_ok() - && fs.create_file(&path, Default::default()).await.is_ok() - { - files.lock().push(path); - break; - } else { - log::info!("Host: cannot create file"); - } - }, } + _ => loop { + let path_component_count = rng.lock().gen_range::(1..=5); + let mut path = PathBuf::new(); + path.push("/"); + for _ in 0..path_component_count { + let letter = rng.lock().gen_range(b'a'..=b'z'); + path.push(std::str::from_utf8(&[letter]).unwrap()); + } + path.set_extension("rs"); + let parent_path = path.parent().unwrap(); - cx.background().simulate_random_delay().await; - } + log::info!("Host: creating file {:?}", path,); - log::info!("Host done"); + if fs.create_dir(&parent_path).await.is_ok() + && fs.create_file(&path, Default::default()).await.is_ok() + { + files.lock().push(path); + break; + } else { + log::info!("Host: cannot create file"); + } + }, + } - self.project = Some(project); - (self, cx) + cx.background().simulate_random_delay().await; } + + log::info!("Host done"); + + self.project = Some(project); + (self, cx) } pub async fn simulate_guest( diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 7961dccd569c8380c3bb32e57e9057481e4371fd..9348ff0ba6a26e6510840b39587b5558d92f297d 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -164,6 +164,55 @@ fn test_line_len() { assert_eq!(buffer.line_len(5), 0); } +#[test] +fn test_common_prefix_at_positionn() { + let text = "a = str; b = δα"; + let buffer = Buffer::new(0, 0, History::new(text.into())); + + let offset1 = offset_after(text, "str"); + let offset2 = offset_after(text, "δα"); + + // the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "string"), + range_of(text, "str"), + ); + // a suffix of the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "tree"), + range_of(text, "tr"), + ); + // the preceding word is a substring of the suggestion, but not a prefix + assert_eq!( + buffer.common_prefix_at(offset1, "astro"), + empty_range_after(text, "str"), + ); + + // prefix matching is case insenstive. + assert_eq!( + buffer.common_prefix_at(offset1, "Strαngε"), + range_of(text, "str"), + ); + assert_eq!( + buffer.common_prefix_at(offset2, "ΔΑΜΝ"), + range_of(text, "δα"), + ); + + fn offset_after(text: &str, part: &str) -> usize { + text.find(part).unwrap() + part.len() + } + + fn empty_range_after(text: &str, part: &str) -> Range { + let offset = offset_after(text, part); + offset..offset + } + + fn range_of(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + part.len() + } +} + #[test] fn test_text_summary_for_range() { let buffer = Buffer::new(0, 0, History::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz".into())); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index b811d08c046c58f8c5d6c020c27c45a430258474..1c351079a7ddae872652707b106abea01c8f70fd 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1508,6 +1508,30 @@ impl BufferSnapshot { .eq(needle.bytes()) } + pub fn common_prefix_at(&self, position: T, needle: &str) -> Range + where + T: ToOffset + TextDimension, + { + let offset = position.to_offset(self); + let common_prefix_len = needle + .char_indices() + .map(|(index, _)| index) + .chain([needle.len()]) + .take_while(|&len| len <= offset) + .filter(|&len| { + let left = self + .chars_for_range(offset - len..offset) + .flat_map(|c| char::to_lowercase(c)); + let right = needle[..len].chars().flat_map(|c| char::to_lowercase(c)); + left.eq(right) + }) + .last() + .unwrap_or(0); + let start_offset = offset - common_prefix_len; + let start = self.text_summary_for_range(0..start_offset); + start..position + } + pub fn text(&self) -> String { self.visible_text.to_string() } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 7e49b473f9de17b5969eabfa4b8e014bf4fb7779..e4035197fd8036922cc4fa1cc5006f15948bb948 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -21,7 +21,7 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) { Mode::Insert } else { - Mode::Normal + Mode::normal() }; VimState::update_global(cx, |state, cx| { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index c027ff2c1f3de1e2d63f824e4c58df5362c6e108..9c1e36a90ef5c05c8aa4960a74572ee81a6fa34f 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -25,6 +25,23 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext CursorShape { match self { - Mode::Normal => CursorShape::Block, + Mode::Normal(_) => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -20,17 +20,53 @@ impl Mode { context.map.insert( "vim_mode".to_string(), match self { - Self::Normal => "normal", + Self::Normal(_) => "normal", Self::Insert => "insert", } .to_string(), ); + + match self { + Self::Normal(normal_state) => normal_state.set_context(&mut context), + _ => {} + } context } + + pub fn normal() -> Mode { + Mode::Normal(Default::default()) + } } impl Default for Mode { fn default() -> Self { - Self::Normal + Self::Normal(Default::default()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NormalState { + None, + GPrefix, +} + +impl NormalState { + pub fn set_context(&self, context: &mut Context) { + let submode = match self { + Self::GPrefix => Some("g"), + _ => None, + }; + + if let Some(submode) = submode { + context + .map + .insert("vim_submode".to_string(), submode.to_string()); + } + } +} + +impl Default for NormalState { + fn default() -> Self { + NormalState::None } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 232a76a030ae42f60ee96218a19e9b506f313392..5bf1a3c417b30fc97258dc2a44d4ee426c13af93 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,30 +1,55 @@ -use editor::{movement, Bias}; +mod g_prefix; + +use editor::{char_kind, movement, Bias}; use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; -use crate::{Mode, SwitchMode, VimState}; +use crate::{mode::NormalState, Mode, SwitchMode, VimState}; -action!(InsertBefore); +action!(GPrefix); action!(MoveLeft); action!(MoveDown); action!(MoveUp); action!(MoveRight); +action!(MoveToStartOfLine); +action!(MoveToEndOfLine); +action!(MoveToEnd); +action!(MoveToNextWordStart, bool); +action!(MoveToNextWordEnd, bool); +action!(MoveToPreviousWordStart, bool); pub fn init(cx: &mut MutableAppContext) { let context = Some("Editor && vim_mode == normal"); cx.add_bindings(vec![ Binding::new("i", SwitchMode(Mode::Insert), context), + Binding::new("g", SwitchMode(Mode::Normal(NormalState::GPrefix)), context), Binding::new("h", MoveLeft, context), Binding::new("j", MoveDown, context), Binding::new("k", MoveUp, context), Binding::new("l", MoveRight, context), + Binding::new("0", MoveToStartOfLine, context), + Binding::new("shift-$", MoveToEndOfLine, context), + Binding::new("shift-G", MoveToEnd, context), + Binding::new("w", MoveToNextWordStart(false), context), + Binding::new("shift-W", MoveToNextWordStart(true), context), + Binding::new("e", MoveToNextWordEnd(false), context), + Binding::new("shift-E", MoveToNextWordEnd(true), context), + Binding::new("b", MoveToPreviousWordStart(false), context), + Binding::new("shift-B", MoveToPreviousWordStart(true), context), ]); + g_prefix::init(cx); cx.add_action(move_left); cx.add_action(move_down); cx.add_action(move_up); cx.add_action(move_right); + cx.add_action(move_to_start_of_line); + cx.add_action(move_to_end_of_line); + cx.add_action(move_to_end); + cx.add_action(move_to_next_word_start); + cx.add_action(move_to_next_word_end); + cx.add_action(move_to_previous_word_start); } fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext) { @@ -64,3 +89,348 @@ fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext) }); }); } + +fn move_to_start_of_line( + _: &mut Workspace, + _: &MoveToStartOfLine, + cx: &mut ViewContext, +) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, cursor, _| { + ( + movement::line_beginning(map, cursor, false), + SelectionGoal::None, + ) + }); + }); + }); +} + +fn move_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, cursor, _| { + ( + map.clip_point(movement::line_end(map, cursor, false), Bias::Left), + SelectionGoal::None, + ) + }); + }); + }); +} + +fn move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.replace_selections_with(cx, |map| map.clip_point(map.max_point(), Bias::Left)); + }); + }); +} + +fn move_to_next_word_start( + _: &mut Workspace, + &MoveToNextWordStart(treat_punctuation_as_word): &MoveToNextWordStart, + cx: &mut ViewContext, +) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + let mut crossed_newline = false; + cursor = movement::find_boundary(map, cursor, |left, right| { + let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word); + let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word); + let at_newline = right == '\n'; + + let found = (left_kind != right_kind && !right.is_whitespace()) + || (at_newline && crossed_newline) + || (at_newline && left == '\n'); // Prevents skipping repeated empty lines + + if at_newline { + crossed_newline = true; + } + found + }); + (cursor, SelectionGoal::None) + }); + }); + }); +} + +fn move_to_next_word_end( + _: &mut Workspace, + &MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd, + cx: &mut ViewContext, +) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() += 1; + cursor = movement::find_boundary(map, cursor, |left, right| { + let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word); + let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word); + + left_kind != right_kind && !left.is_whitespace() + }); + // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know + // we have backtraced already + if !map + .chars_at(cursor) + .skip(1) + .next() + .map(|c| c == '\n') + .unwrap_or(true) + { + *cursor.column_mut() = cursor.column().saturating_sub(1); + } + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); + }); + }); +} + +fn move_to_previous_word_start( + _: &mut Workspace, + &MoveToPreviousWordStart(treat_punctuation_as_word): &MoveToPreviousWordStart, + cx: &mut ViewContext, +) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + // This works even though find_preceding_boundary is called for every character in the line containing + // cursor because the newline is checked only once. + cursor = movement::find_preceding_boundary(map, cursor, |left, right| { + let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word); + let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word); + + (left_kind != right_kind && !right.is_whitespace()) || left == '\n' + }); + (cursor, SelectionGoal::None) + }); + }); + }); +} + +#[cfg(test)] +mod test { + use indoc::indoc; + use util::test::marked_text; + + use crate::vim_test_context::VimTestContext; + + #[gpui::test] + async fn test_hjkl(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true, "Test\nTestTest\nTest").await; + cx.simulate_keystroke("l"); + cx.assert_editor_state(indoc! {" + T|est + TestTest + Test"}); + cx.simulate_keystroke("h"); + cx.assert_editor_state(indoc! {" + |Test + TestTest + Test"}); + cx.simulate_keystroke("j"); + cx.assert_editor_state(indoc! {" + Test + |TestTest + Test"}); + cx.simulate_keystroke("k"); + cx.assert_editor_state(indoc! {" + |Test + TestTest + Test"}); + cx.simulate_keystroke("j"); + cx.assert_editor_state(indoc! {" + Test + |TestTest + Test"}); + + // When moving left, cursor does not wrap to the previous line + cx.simulate_keystroke("h"); + cx.assert_editor_state(indoc! {" + Test + |TestTest + Test"}); + + // When moving right, cursor does not reach the line end or wrap to the next line + for _ in 0..9 { + cx.simulate_keystroke("l"); + } + cx.assert_editor_state(indoc! {" + Test + TestTes|t + Test"}); + + // Goal column respects the inability to reach the end of the line + cx.simulate_keystroke("k"); + cx.assert_editor_state(indoc! {" + Tes|t + TestTest + Test"}); + cx.simulate_keystroke("j"); + cx.assert_editor_state(indoc! {" + Test + TestTes|t + Test"}); + } + + #[gpui::test] + async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) { + let initial_content = indoc! {" + Test Test + + T"}; + let mut cx = VimTestContext::new(cx, true, initial_content).await; + + cx.simulate_keystroke("shift-$"); + cx.assert_editor_state(indoc! {" + Test Tes|t + + T"}); + cx.simulate_keystroke("0"); + cx.assert_editor_state(indoc! {" + |Test Test + + T"}); + + cx.simulate_keystroke("j"); + cx.simulate_keystroke("shift-$"); + cx.assert_editor_state(indoc! {" + Test Test + | + T"}); + cx.simulate_keystroke("0"); + cx.assert_editor_state(indoc! {" + Test Test + | + T"}); + + cx.simulate_keystroke("j"); + cx.simulate_keystroke("shift-$"); + cx.assert_editor_state(indoc! {" + Test Test + + |T"}); + cx.simulate_keystroke("0"); + cx.assert_editor_state(indoc! {" + Test Test + + |T"}); + } + + #[gpui::test] + async fn test_jump_to_end(cx: &mut gpui::TestAppContext) { + let initial_content = indoc! {" + The quick + + brown fox jumps + over the lazy dog"}; + let mut cx = VimTestContext::new(cx, true, initial_content).await; + + cx.simulate_keystroke("shift-G"); + cx.assert_editor_state(indoc! {" + The quick + + brown fox jumps + over the lazy do|g"}); + + // Repeat the action doesn't move + cx.simulate_keystroke("shift-G"); + cx.assert_editor_state(indoc! {" + The quick + + brown fox jumps + over the lazy do|g"}); + } + + #[gpui::test] + async fn test_next_word_start(cx: &mut gpui::TestAppContext) { + let (initial_content, cursor_offsets) = marked_text(indoc! {" + The |quick|-|brown + | + | + |fox_jumps |over + |th||e"}); + let mut cx = VimTestContext::new(cx, true, &initial_content).await; + + for cursor_offset in cursor_offsets { + cx.simulate_keystroke("w"); + cx.assert_newest_selection_head_offset(cursor_offset); + } + + // Reset and test ignoring punctuation + cx.simulate_keystrokes(&["g", "g"]); + let (_, cursor_offsets) = marked_text(indoc! {" + The |quick-brown + | + | + |fox_jumps |over + |th||e"}); + + for cursor_offset in cursor_offsets { + cx.simulate_keystroke("shift-W"); + cx.assert_newest_selection_head_offset(cursor_offset); + } + } + + #[gpui::test] + async fn test_next_word_end(cx: &mut gpui::TestAppContext) { + let (initial_content, cursor_offsets) = marked_text(indoc! {" + Th|e quic|k|-brow|n + + + fox_jump|s ove|r + th|e"}); + let mut cx = VimTestContext::new(cx, true, &initial_content).await; + + for cursor_offset in cursor_offsets { + cx.simulate_keystroke("e"); + cx.assert_newest_selection_head_offset(cursor_offset); + } + + // Reset and test ignoring punctuation + cx.simulate_keystrokes(&["g", "g"]); + let (_, cursor_offsets) = marked_text(indoc! {" + Th|e quick-brow|n + + + fox_jump|s ove|r + th||e"}); + for cursor_offset in cursor_offsets { + cx.simulate_keystroke("shift-E"); + cx.assert_newest_selection_head_offset(cursor_offset); + } + } + + #[gpui::test] + async fn test_previous_word_start(cx: &mut gpui::TestAppContext) { + let (initial_content, cursor_offsets) = marked_text(indoc! {" + ||The |quick|-|brown + | + | + |fox_jumps |over + |the"}); + let mut cx = VimTestContext::new(cx, true, &initial_content).await; + cx.simulate_keystroke("shift-G"); + + for cursor_offset in cursor_offsets.into_iter().rev() { + cx.simulate_keystroke("b"); + cx.assert_newest_selection_head_offset(cursor_offset); + } + + // Reset and test ignoring punctuation + cx.simulate_keystroke("shift-G"); + let (_, cursor_offsets) = marked_text(indoc! {" + ||The |quick-brown + | + | + |fox_jumps |over + |the"}); + for cursor_offset in cursor_offsets.into_iter().rev() { + cx.simulate_keystroke("shift-B"); + cx.assert_newest_selection_head_offset(cursor_offset); + } + } +} diff --git a/crates/vim/src/normal/g_prefix.rs b/crates/vim/src/normal/g_prefix.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b710892459ae62497e7f9ad49ca623eba407303 --- /dev/null +++ b/crates/vim/src/normal/g_prefix.rs @@ -0,0 +1,82 @@ +use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; +use workspace::Workspace; + +use crate::{mode::Mode, SwitchMode, VimState}; + +action!(MoveToStart); + +pub fn init(cx: &mut MutableAppContext) { + let context = Some("Editor && vim_mode == normal && vim_submode == g"); + cx.add_bindings(vec![ + Binding::new("g", MoveToStart, context), + Binding::new("escape", SwitchMode(Mode::normal()), context), + ]); + + cx.add_action(move_to_start); +} + +fn move_to_start(_: &mut Workspace, _: &MoveToStart, cx: &mut ViewContext) { + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_to_beginning(&editor::MoveToBeginning, cx); + }); + state.switch_mode(&SwitchMode(Mode::normal()), cx); + }) +} + +#[cfg(test)] +mod test { + use indoc::indoc; + + use crate::{ + mode::{Mode, NormalState}, + vim_test_context::VimTestContext, + }; + + #[gpui::test] + async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true, "").await; + + // Can abort with escape to get back to normal mode + cx.simulate_keystroke("g"); + assert_eq!(cx.mode(), Mode::Normal(NormalState::GPrefix)); + cx.simulate_keystroke("escape"); + assert_eq!(cx.mode(), Mode::normal()); + } + + #[gpui::test] + async fn test_move_to_start(cx: &mut gpui::TestAppContext) { + let initial_content = indoc! {" + The quick + + brown fox jumps + over the lazy dog"}; + let mut cx = VimTestContext::new(cx, true, initial_content).await; + + // Jump to the end to + cx.simulate_keystroke("shift-G"); + cx.assert_editor_state(indoc! {" + The quick + + brown fox jumps + over the lazy do|g"}); + + // Jump to the start + cx.simulate_keystrokes(&["g", "g"]); + cx.assert_editor_state(indoc! {" + |The quick + + brown fox jumps + over the lazy dog"}); + assert_eq!(cx.mode(), Mode::normal()); + + // Repeat action doesn't change + cx.simulate_keystrokes(&["g", "g"]); + cx.assert_editor_state(indoc! {" + |The quick + + brown fox jumps + over the lazy dog"}); + assert_eq!(cx.mode(), Mode::normal()); + } +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 26f7e24cf29bc7cbb919fc1c65597b7d826beb1a..609843d9696c215037cb9bef05cde9bc5c28ac14 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -3,7 +3,7 @@ mod insert; mod mode; mod normal; #[cfg(test)] -mod vim_tests; +mod vim_test_context; use collections::HashMap; use editor::{CursorShape, Editor}; @@ -65,8 +65,9 @@ impl VimState { fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) { if self.enabled != enabled { self.enabled = enabled; + self.mode = Default::default(); if enabled { - self.mode = Mode::Normal; + self.mode = Mode::normal(); } self.sync_editor_options(cx); } @@ -95,3 +96,43 @@ impl VimState { } } } + +#[cfg(test)] +mod test { + use crate::{mode::Mode, vim_test_context::VimTestContext}; + + #[gpui::test] + async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, false, "").await; + cx.simulate_keystrokes(&["h", "j", "k", "l"]); + cx.assert_editor_state("hjkl|"); + } + + #[gpui::test] + async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true, "").await; + + cx.simulate_keystroke("i"); + assert_eq!(cx.mode(), Mode::Insert); + + // Editor acts as though vim is disabled + cx.disable_vim(); + cx.simulate_keystrokes(&["h", "j", "k", "l"]); + cx.assert_editor_state("hjkl|"); + + // Enabling dynamically sets vim mode again and restores normal mode + cx.enable_vim(); + assert_eq!(cx.mode(), Mode::normal()); + cx.simulate_keystrokes(&["h", "h", "h", "l"]); + assert_eq!(cx.editor_text(), "hjkl".to_owned()); + cx.assert_editor_state("hj|kl"); + cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]); + cx.assert_editor_state("hjTest|kl"); + + // Disabling and enabling resets to normal mode + assert_eq!(cx.mode(), Mode::Insert); + cx.disable_vim(); + cx.enable_vim(); + assert_eq!(cx.mode(), Mode::normal()); + } +} diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..91acc8de6ced11caded405415f97997ea4e770ff --- /dev/null +++ b/crates/vim/src/vim_test_context.rs @@ -0,0 +1,179 @@ +use std::ops::Deref; + +use editor::{display_map::ToDisplayPoint, Bias, DisplayPoint}; +use gpui::{json::json, keymap::Keystroke, ViewHandle}; +use language::{Point, Selection}; +use util::test::marked_text; +use workspace::{WorkspaceHandle, WorkspaceParams}; + +use crate::*; + +pub struct VimTestContext<'a> { + cx: &'a mut gpui::TestAppContext, + window_id: usize, + editor: ViewHandle, +} + +impl<'a> VimTestContext<'a> { + pub async fn new( + cx: &'a mut gpui::TestAppContext, + enabled: bool, + initial_editor_text: &str, + ) -> VimTestContext<'a> { + cx.update(|cx| { + editor::init(cx); + crate::init(cx); + }); + let params = cx.update(WorkspaceParams::test); + + cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = enabled; + }); + }); + + params + .fs + .as_fake() + .insert_tree( + "/root", + json!({ "dir": { "test.txt": initial_editor_text } }), + ) + .await; + + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + params + .project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root", true, cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + let item = workspace + .update(cx, |workspace, cx| workspace.open_path(file, cx)) + .await + .expect("Could not open test file"); + + let editor = cx.update(|cx| { + item.act_as::(cx) + .expect("Opened test file wasn't an editor") + }); + editor.update(cx, |_, cx| cx.focus_self()); + + Self { + cx, + window_id, + editor, + } + } + + pub fn enable_vim(&mut self) { + self.cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = true; + }); + }) + } + + pub fn disable_vim(&mut self) { + self.cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = false; + }); + }) + } + + pub fn newest_selection(&mut self) -> Selection { + self.editor.update(self.cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + editor + .newest_selection::(cx) + .map(|point| point.to_display_point(&snapshot.display_snapshot)) + }) + } + + pub fn mode(&mut self) -> Mode { + self.cx.update(|cx| cx.global::().mode) + } + + pub fn editor_text(&mut self) -> String { + self.editor + .update(self.cx, |editor, cx| editor.snapshot(cx).text()) + } + + pub fn simulate_keystroke(&mut self, keystroke_text: &str) { + let keystroke = Keystroke::parse(keystroke_text).unwrap(); + let input = if keystroke.modified() { + None + } else { + Some(keystroke.key.clone()) + }; + self.cx + .dispatch_keystroke(self.window_id, keystroke, input, false); + } + + pub fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) { + for keystroke_text in keystroke_texts.into_iter() { + self.simulate_keystroke(keystroke_text); + } + } + + pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) { + let actual_head = self.newest_selection().head(); + let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + ( + actual_head.to_offset(&snapshot, Bias::Left), + expected_offset.to_display_point(&snapshot), + ) + }); + let mut actual_position_text = self.editor_text(); + let mut expected_position_text = actual_position_text.clone(); + actual_position_text.insert(actual_offset, '|'); + expected_position_text.insert(expected_offset, '|'); + assert_eq!( + actual_head, expected_head, + "\nActual Position: {}\nExpected Position: {}", + actual_position_text, expected_position_text + ) + } + + pub fn assert_editor_state(&mut self, text: &str) { + let (unmarked_text, markers) = marked_text(&text); + let editor_text = self.editor_text(); + assert_eq!( + editor_text, unmarked_text, + "Unmarked text doesn't match editor text" + ); + let expected_offset = markers[0]; + let actual_head = self.newest_selection().head(); + let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + ( + actual_head.to_offset(&snapshot, Bias::Left), + expected_offset.to_display_point(&snapshot), + ) + }); + let mut actual_position_text = self.editor_text(); + let mut expected_position_text = actual_position_text.clone(); + actual_position_text.insert(actual_offset, '|'); + expected_position_text.insert(expected_offset, '|'); + assert_eq!( + actual_head, expected_head, + "\nActual Position: {}\nExpected Position: {}", + actual_position_text, expected_position_text + ) + } +} + +impl<'a> Deref for VimTestContext<'a> { + type Target = gpui::TestAppContext; + + fn deref(&self) -> &Self::Target { + self.cx + } +} diff --git a/crates/vim/src/vim_tests.rs b/crates/vim/src/vim_tests.rs deleted file mode 100644 index 051ff21ce76575e480192024ed4b953ba1ca840e..0000000000000000000000000000000000000000 --- a/crates/vim/src/vim_tests.rs +++ /dev/null @@ -1,253 +0,0 @@ -use indoc::indoc; -use std::ops::Deref; - -use editor::{display_map::ToDisplayPoint, DisplayPoint}; -use gpui::{json::json, keymap::Keystroke, ViewHandle}; -use language::{Point, Selection}; -use util::test::marked_text; -use workspace::{WorkspaceHandle, WorkspaceParams}; - -use crate::*; - -#[gpui::test] -async fn test_insert_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestAppContext::new(cx, true, "").await; - cx.simulate_keystroke("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes(&["T", "e", "s", "t"]); - cx.assert_newest_selection_head("Test|"); - cx.simulate_keystroke("escape"); - assert_eq!(cx.mode(), Mode::Normal); - cx.assert_newest_selection_head("Tes|t"); -} - -#[gpui::test] -async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestAppContext::new(cx, true, "Test\nTestTest\nTest").await; - cx.simulate_keystroke("l"); - cx.assert_newest_selection_head(indoc! {" - T|est - TestTest - Test"}); - cx.simulate_keystroke("h"); - cx.assert_newest_selection_head(indoc! {" - |Test - TestTest - Test"}); - cx.simulate_keystroke("j"); - cx.assert_newest_selection_head(indoc! {" - Test - |TestTest - Test"}); - cx.simulate_keystroke("k"); - cx.assert_newest_selection_head(indoc! {" - |Test - TestTest - Test"}); - cx.simulate_keystroke("j"); - cx.assert_newest_selection_head(indoc! {" - Test - |TestTest - Test"}); - - // When moving left, cursor does not wrap to the previous line - cx.simulate_keystroke("h"); - cx.assert_newest_selection_head(indoc! {" - Test - |TestTest - Test"}); - - // When moving right, cursor does not reach the line end or wrap to the next line - for _ in 0..9 { - cx.simulate_keystroke("l"); - } - cx.assert_newest_selection_head(indoc! {" - Test - TestTes|t - Test"}); - - // Goal column respects the inability to reach the end of the line - cx.simulate_keystroke("k"); - cx.assert_newest_selection_head(indoc! {" - Tes|t - TestTest - Test"}); - cx.simulate_keystroke("j"); - cx.assert_newest_selection_head(indoc! {" - Test - TestTes|t - Test"}); -} - -#[gpui::test] -async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestAppContext::new(cx, true, "").await; - - cx.simulate_keystroke("i"); - assert_eq!(cx.mode(), Mode::Insert); - - // Editor acts as though vim is disabled - cx.disable_vim(); - cx.simulate_keystrokes(&["h", "j", "k", "l"]); - cx.assert_newest_selection_head("hjkl|"); - - // Enabling dynamically sets vim mode again and restores normal mode - cx.enable_vim(); - assert_eq!(cx.mode(), Mode::Normal); - cx.simulate_keystrokes(&["h", "h", "h", "l"]); - assert_eq!(cx.editor_text(), "hjkl".to_owned()); - cx.assert_newest_selection_head("hj|kl"); - cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]); - cx.assert_newest_selection_head("hjTest|kl"); - - // Disabling and enabling resets to normal mode - assert_eq!(cx.mode(), Mode::Insert); - cx.disable_vim(); - assert_eq!(cx.mode(), Mode::Insert); - cx.enable_vim(); - assert_eq!(cx.mode(), Mode::Normal); -} - -#[gpui::test] -async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestAppContext::new(cx, false, "").await; - cx.simulate_keystrokes(&["h", "j", "k", "l"]); - cx.assert_newest_selection_head("hjkl|"); -} - -struct VimTestAppContext<'a> { - cx: &'a mut gpui::TestAppContext, - window_id: usize, - editor: ViewHandle, -} - -impl<'a> VimTestAppContext<'a> { - async fn new( - cx: &'a mut gpui::TestAppContext, - enabled: bool, - initial_editor_text: &str, - ) -> VimTestAppContext<'a> { - cx.update(|cx| { - editor::init(cx); - crate::init(cx); - }); - let params = cx.update(WorkspaceParams::test); - - cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.vim_mode = enabled; - }); - }); - - params - .fs - .as_fake() - .insert_tree( - "/root", - json!({ "dir": { "test.txt": initial_editor_text } }), - ) - .await; - - let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - params - .project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root", true, cx) - }) - .await - .unwrap(); - cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) - .await; - - let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); - let item = workspace - .update(cx, |workspace, cx| workspace.open_path(file, cx)) - .await - .expect("Could not open test file"); - - let editor = cx.update(|cx| { - item.act_as::(cx) - .expect("Opened test file wasn't an editor") - }); - editor.update(cx, |_, cx| cx.focus_self()); - - Self { - cx, - window_id, - editor, - } - } - - fn enable_vim(&mut self) { - self.cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.vim_mode = true; - }); - }) - } - - fn disable_vim(&mut self) { - self.cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.vim_mode = false; - }); - }) - } - - fn newest_selection(&mut self) -> Selection { - self.editor.update(self.cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - editor - .newest_selection::(cx) - .map(|point| point.to_display_point(&snapshot.display_snapshot)) - }) - } - - fn mode(&mut self) -> Mode { - self.cx.update(|cx| cx.global::().mode) - } - - fn editor_text(&mut self) -> String { - self.editor - .update(self.cx, |editor, cx| editor.snapshot(cx).text()) - } - - fn simulate_keystroke(&mut self, keystroke_text: &str) { - let keystroke = Keystroke::parse(keystroke_text).unwrap(); - let input = if keystroke.modified() { - None - } else { - Some(keystroke.key.clone()) - }; - self.cx - .dispatch_keystroke(self.window_id, keystroke, input, false); - } - - fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) { - for keystroke_text in keystroke_texts.into_iter() { - self.simulate_keystroke(keystroke_text); - } - } - - fn assert_newest_selection_head(&mut self, text: &str) { - let (unmarked_text, markers) = marked_text(&text); - assert_eq!( - self.editor_text(), - unmarked_text, - "Unmarked text doesn't match editor text" - ); - let newest_selection = self.newest_selection(); - let expected_head = self.editor.update(self.cx, |editor, cx| { - markers[0].to_display_point(&editor.snapshot(cx)) - }); - assert_eq!(newest_selection.head(), expected_head) - } -} - -impl<'a> Deref for VimTestAppContext<'a> { - type Target = gpui::TestAppContext; - - fn deref(&self) -> &Self::Target { - self.cx - } -} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d48a5711a3373ce9aa8752fe2b78a5fcd46f2aec..1421072b131e4ed72f05547edb1e30080f8ef2ce 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,17 +1,18 @@ use super::{ItemHandle, SplitDirection}; use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace}; use collections::{HashMap, VecDeque}; +use futures::StreamExt; use gpui::{ action, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::{CursorStyle, NavigationDirection}, - AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad, RenderContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{ProjectEntryId, ProjectPath}; -use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc}; +use project::{Project, ProjectEntryId, ProjectPath}; +use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc}; use util::ResultExt; action!(Split, SplitDirection); @@ -37,13 +38,13 @@ pub fn init(cx: &mut MutableAppContext) { pane.activate_next_item(cx); }); cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| { - pane.close_active_item(cx); + pane.close_active_item(cx).detach(); }); cx.add_action(|pane: &mut Pane, _: &CloseInactiveItems, cx| { - pane.close_inactive_items(cx); + pane.close_inactive_items(cx).detach(); }); cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| { - pane.close_item(action.0, cx); + pane.close_item(action.0, cx).detach(); }); cx.add_action(|pane: &mut Pane, action: &Split, cx| { pane.split(action.0, cx); @@ -97,6 +98,7 @@ pub struct Pane { active_item_index: usize, nav_history: Rc>, toolbar: ViewHandle, + project: ModelHandle, } pub struct ItemNavHistory { @@ -132,12 +134,13 @@ pub struct NavigationEntry { } impl Pane { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { Self { items: Vec::new(), active_item_index: 0, nav_history: Default::default(), toolbar: cx.add_view(|_| Toolbar::new()), + project, } } @@ -204,13 +207,16 @@ impl Pane { let prev_active_index = mem::replace(&mut pane.active_item_index, index); pane.focus_active_item(cx); + pane.update_toolbar(cx); + cx.emit(Event::ActivateItem { local: true }); + cx.notify(); + let mut navigated = prev_active_index != pane.active_item_index; if let Some(data) = entry.data { navigated |= pane.active_item()?.navigate(data, cx); } if navigated { - cx.notify(); break None; } } @@ -403,65 +409,173 @@ impl Pane { self.activate_item(index, true, cx); } - pub fn close_active_item(&mut self, cx: &mut ViewContext) { - if !self.items.is_empty() { + pub fn close_active_item(&mut self, cx: &mut ViewContext) -> Task<()> { + if self.items.is_empty() { + Task::ready(()) + } else { self.close_item(self.items[self.active_item_index].id(), cx) } } - pub fn close_inactive_items(&mut self, cx: &mut ViewContext) { - if !self.items.is_empty() { + pub fn close_inactive_items(&mut self, cx: &mut ViewContext) -> Task<()> { + if self.items.is_empty() { + Task::ready(()) + } else { let active_item_id = self.items[self.active_item_index].id(); - self.close_items(cx, |id| id != active_item_id); + self.close_items(cx, move |id| id != active_item_id) } } - pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext) { - self.close_items(cx, |view_id| view_id == view_id_to_close); + pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext) -> Task<()> { + self.close_items(cx, move |view_id| view_id == view_id_to_close) } pub fn close_items( &mut self, cx: &mut ViewContext, - should_close: impl Fn(usize) -> bool, - ) { - let mut item_ix = 0; - let mut new_active_item_index = self.active_item_index; - self.items.retain(|item| { - if should_close(item.id()) { - if item_ix == self.active_item_index { - item.deactivated(cx); - } + should_close: impl 'static + Fn(usize) -> bool, + ) -> Task<()> { + const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + const DIRTY_MESSAGE: &'static str = + "This file contains unsaved edits. Do you want to save it?"; + + let project = self.project.clone(); + cx.spawn(|this, mut cx| async move { + while let Some(item_to_close_ix) = this.read_with(&cx, |this, _| { + this.items.iter().position(|item| should_close(item.id())) + }) { + let item = + this.read_with(&cx, |this, _| this.items[item_to_close_ix].boxed_clone()); + if cx.read(|cx| item.is_dirty(cx)) { + if cx.read(|cx| item.can_save(cx)) { + let mut answer = this.update(&mut cx, |this, cx| { + this.activate_item(item_to_close_ix, true, cx); + cx.prompt( + PromptLevel::Warning, + DIRTY_MESSAGE, + &["Save", "Don't Save", "Cancel"], + ) + }); - if item_ix < self.active_item_index { - new_active_item_index -= 1; - } + match answer.next().await { + Some(0) => { + if cx + .update(|cx| item.save(project.clone(), cx)) + .await + .log_err() + .is_none() + { + break; + } + } + Some(1) => {} + _ => break, + } + } else if cx.read(|cx| item.can_save_as(cx)) { + let mut answer = this.update(&mut cx, |this, cx| { + this.activate_item(item_to_close_ix, true, cx); + cx.prompt( + PromptLevel::Warning, + DIRTY_MESSAGE, + &["Save", "Don't Save", "Cancel"], + ) + }); - let mut nav_history = self.nav_history.borrow_mut(); - if let Some(path) = item.project_path(cx) { - nav_history.paths_by_item.insert(item.id(), path); - } else { - nav_history.paths_by_item.remove(&item.id()); + match answer.next().await { + Some(0) => { + let start_abs_path = project + .read_with(&cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next()?; + Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) + }) + .unwrap_or(Path::new("").into()); + + let mut abs_path = + cx.update(|cx| cx.prompt_for_new_path(&start_abs_path)); + if let Some(abs_path) = abs_path.next().await.flatten() { + if cx + .update(|cx| item.save_as(project.clone(), abs_path, cx)) + .await + .log_err() + .is_none() + { + break; + } + } else { + break; + } + } + Some(1) => {} + _ => break, + } + } + } else if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) { + let mut answer = this.update(&mut cx, |this, cx| { + this.activate_item(item_to_close_ix, true, cx); + cx.prompt( + PromptLevel::Warning, + CONFLICT_MESSAGE, + &["Overwrite", "Discard", "Cancel"], + ) + }); + + match answer.next().await { + Some(0) => { + if cx + .update(|cx| item.save(project.clone(), cx)) + .await + .log_err() + .is_none() + { + break; + } + } + Some(1) => { + if cx + .update(|cx| item.reload(project.clone(), cx)) + .await + .log_err() + .is_none() + { + break; + } + } + _ => break, + } } - item_ix += 1; - false - } else { - item_ix += 1; - true + this.update(&mut cx, |this, cx| { + if let Some(item_ix) = this.items.iter().position(|i| i.id() == item.id()) { + if item_ix == this.active_item_index { + if item_ix + 1 < this.items.len() { + this.activate_next_item(cx); + } else if item_ix > 0 { + this.activate_prev_item(cx); + } + } + + let item = this.items.remove(item_ix); + if this.items.is_empty() { + item.deactivated(cx); + cx.emit(Event::Remove); + } + + if item_ix < this.active_item_index { + this.active_item_index -= 1; + } + + let mut nav_history = this.nav_history.borrow_mut(); + if let Some(path) = item.project_path(cx) { + nav_history.paths_by_item.insert(item.id(), path); + } else { + nav_history.paths_by_item.remove(&item.id()); + } + } + }); } - }); - if self.items.is_empty() { - cx.emit(Event::Remove); - } else { - self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1); - self.focus_active_item(cx); - self.activate(cx); - } - self.update_toolbar(cx); - - cx.notify(); + this.update(&mut cx, |_, cx| cx.notify()); + }) } pub fn focus_active_item(&mut self, cx: &mut ViewContext) { @@ -743,3 +857,211 @@ impl NavHistory { } } } + +#[cfg(test)] +mod tests { + use crate::WorkspaceParams; + + use super::*; + use gpui::TestAppContext; + + #[gpui::test] + async fn test_close_items(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + + let params = cx.update(WorkspaceParams::test); + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + let item1 = cx.add_view(window_id, |_| { + let mut item = TestItem::new(); + item.has_conflict = true; + item + }); + let item2 = cx.add_view(window_id, |_| { + let mut item = TestItem::new(); + item.is_dirty = true; + item.has_conflict = true; + item + }); + let item3 = cx.add_view(window_id, |_| { + let mut item = TestItem::new(); + item.has_conflict = true; + item + }); + let item4 = cx.add_view(window_id, |_| { + let mut item = TestItem::new(); + item.is_dirty = true; + item + }); + let item5 = cx.add_view(window_id, |_| { + let mut item = TestItem::new(); + item.is_dirty = true; + item.can_save = false; + item + }); + let pane = workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item1.clone()), cx); + workspace.add_item(Box::new(item2.clone()), cx); + workspace.add_item(Box::new(item3.clone()), cx); + workspace.add_item(Box::new(item4.clone()), cx); + workspace.add_item(Box::new(item5.clone()), cx); + workspace.active_pane().clone() + }); + + let close_items = pane.update(cx, |pane, cx| { + pane.activate_item(1, true, cx); + assert_eq!(pane.active_item().unwrap().id(), item2.id()); + + let item1_id = item1.id(); + let item3_id = item3.id(); + let item4_id = item4.id(); + let item5_id = item5.id(); + pane.close_items(cx, move |id| { + [item1_id, item3_id, item4_id, item5_id].contains(&id) + }) + }); + + cx.foreground().run_until_parked(); + pane.read_with(cx, |pane, _| { + assert_eq!(pane.items.len(), 5); + assert_eq!(pane.active_item().unwrap().id(), item1.id()); + }); + + cx.simulate_prompt_answer(window_id, 0); + cx.foreground().run_until_parked(); + pane.read_with(cx, |pane, cx| { + assert_eq!(item1.read(cx).save_count, 1); + assert_eq!(item1.read(cx).save_as_count, 0); + assert_eq!(item1.read(cx).reload_count, 0); + assert_eq!(pane.items.len(), 4); + assert_eq!(pane.active_item().unwrap().id(), item3.id()); + }); + + cx.simulate_prompt_answer(window_id, 1); + cx.foreground().run_until_parked(); + pane.read_with(cx, |pane, cx| { + assert_eq!(item3.read(cx).save_count, 0); + assert_eq!(item3.read(cx).save_as_count, 0); + assert_eq!(item3.read(cx).reload_count, 1); + assert_eq!(pane.items.len(), 3); + assert_eq!(pane.active_item().unwrap().id(), item4.id()); + }); + + cx.simulate_prompt_answer(window_id, 0); + cx.foreground().run_until_parked(); + pane.read_with(cx, |pane, cx| { + assert_eq!(item4.read(cx).save_count, 1); + assert_eq!(item4.read(cx).save_as_count, 0); + assert_eq!(item4.read(cx).reload_count, 0); + assert_eq!(pane.items.len(), 2); + assert_eq!(pane.active_item().unwrap().id(), item5.id()); + }); + + cx.simulate_prompt_answer(window_id, 0); + cx.foreground().run_until_parked(); + cx.simulate_new_path_selection(|_| Some(Default::default())); + close_items.await; + pane.read_with(cx, |pane, cx| { + assert_eq!(item5.read(cx).save_count, 0); + assert_eq!(item5.read(cx).save_as_count, 1); + assert_eq!(item5.read(cx).reload_count, 0); + assert_eq!(pane.items.len(), 1); + assert_eq!(pane.active_item().unwrap().id(), item2.id()); + }); + } + + struct TestItem { + save_count: usize, + save_as_count: usize, + reload_count: usize, + is_dirty: bool, + has_conflict: bool, + can_save: bool, + } + + impl TestItem { + fn new() -> Self { + Self { + save_count: 0, + save_as_count: 0, + reload_count: 0, + is_dirty: false, + has_conflict: false, + can_save: true, + } + } + } + + impl Entity for TestItem { + type Event = (); + } + + impl View for TestItem { + fn ui_name() -> &'static str { + "TestItem" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().boxed() + } + } + + impl Item for TestItem { + fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox { + Empty::new().boxed() + } + + fn project_path(&self, _: &AppContext) -> Option { + None + } + + fn project_entry_id(&self, _: &AppContext) -> Option { + None + } + + fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} + + fn is_dirty(&self, _: &AppContext) -> bool { + self.is_dirty + } + + fn has_conflict(&self, _: &AppContext) -> bool { + self.has_conflict + } + + fn can_save(&self, _: &AppContext) -> bool { + self.can_save + } + + fn save( + &mut self, + _: ModelHandle, + _: &mut ViewContext, + ) -> Task> { + self.save_count += 1; + Task::ready(Ok(())) + } + + fn can_save_as(&self, _: &AppContext) -> bool { + true + } + + fn save_as( + &mut self, + _: ModelHandle, + _: std::path::PathBuf, + _: &mut ViewContext, + ) -> Task> { + self.save_as_count += 1; + Task::ready(Ok(())) + } + + fn reload( + &mut self, + _: ModelHandle, + _: &mut ViewContext, + ) -> Task> { + self.reload_count += 1; + Task::ready(Ok(())) + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9929cd9a51cd675e80953ad371ed1f94032e7459..c447f3a5fd567c1f5f89a11f2e93edce632648b9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -237,6 +237,11 @@ pub trait Item: View { abs_path: PathBuf, cx: &mut ViewContext, ) -> Task>; + fn reload( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task>; fn should_activate_item_on_event(_: &Self::Event) -> bool { false } @@ -380,6 +385,8 @@ pub trait ItemHandle: 'static + fmt::Debug { abs_path: PathBuf, cx: &mut MutableAppContext, ) -> Task>; + fn reload(&self, project: ModelHandle, cx: &mut MutableAppContext) + -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; } @@ -490,7 +497,8 @@ impl ItemHandle for ViewHandle { } if T::should_close_item_on_event(event) { - pane.update(cx, |pane, cx| pane.close_item(item.id(), cx)); + pane.update(cx, |pane, cx| pane.close_item(item.id(), cx)) + .detach(); return; } @@ -531,6 +539,14 @@ impl ItemHandle for ViewHandle { self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) } + fn reload( + &self, + project: ModelHandle, + cx: &mut MutableAppContext, + ) -> Task> { + self.update(cx, |item, cx| item.reload(project, cx)) + } + fn is_dirty(&self, cx: &AppContext) -> bool { self.read(cx).is_dirty(cx) } @@ -722,7 +738,7 @@ impl Workspace { }) .detach(); - let pane = cx.add_view(|cx| Pane::new(cx)); + let pane = cx.add_view(|cx| Pane::new(params.project.clone(), cx)); let pane_id = pane.id(); cx.observe(&pane, move |me, _, cx| { let active_entry = me.active_project_path(cx); @@ -1054,7 +1070,7 @@ impl Workspace { } fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { - let pane = cx.add_view(|cx| Pane::new(cx)); + let pane = cx.add_view(|cx| Pane::new(self.project.clone(), cx)); let pane_id = pane.id(); cx.observe(&pane, move |me, _, cx| { let active_entry = me.active_project_path(cx); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ede24aae71cc4d45451c98cb8f68df0460b82b31..2e1ef780caf77d6e0ffd0352e60b137ee7a07a71 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.23.0" +version = "0.24.0" [lib] name = "zed" @@ -99,6 +99,7 @@ tree-sitter-c = "0.20.1" tree-sitter-json = "0.19.0" tree-sitter-rust = "0.20.1" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } +tree-sitter-typescript = "0.20.1" url = "2.2" [dev-dependencies] diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs new file mode 100644 index 0000000000000000000000000000000000000000..75a5030ec6a9991ef87d7ebc764c5aabaa6d27d6 --- /dev/null +++ b/crates/zed/src/languages.rs @@ -0,0 +1,112 @@ +use gpui::Task; +pub use language::*; +use rust_embed::RustEmbed; +use std::{borrow::Cow, str, sync::Arc}; + +mod c; +mod installation; +mod json; +mod rust; +mod typescript; + +#[derive(RustEmbed)] +#[folder = "src/languages"] +#[exclude = "*.rs"] +struct LanguageDir; + +pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { + let languages = LanguageRegistry::new(login_shell_env_loaded); + for (name, grammar, lsp_adapter) in [ + ( + "c", + tree_sitter_c::language(), + Some(Arc::new(c::CLspAdapter) as Arc), + ), + ( + "json", + tree_sitter_json::language(), + Some(Arc::new(json::JsonLspAdapter)), + ), + ( + "markdown", + tree_sitter_markdown::language(), + None, // + ), + ( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(rust::RustLspAdapter)), + ), + ( + "tsx", + tree_sitter_typescript::language_tsx(), + Some(Arc::new(typescript::TypeScriptLspAdapter)), + ), + ( + "typescript", + tree_sitter_typescript::language_typescript(), + Some(Arc::new(typescript::TypeScriptLspAdapter)), + ), + ] { + languages.add(Arc::new(language(name, grammar, lsp_adapter))); + } + languages +} + +fn language( + name: &str, + grammar: tree_sitter::Language, + lsp_adapter: Option>, +) -> Language { + let config = toml::from_slice( + &LanguageDir::get(&format!("{}/config.toml", name)) + .unwrap() + .data, + ) + .unwrap(); + let mut language = Language::new(config, Some(grammar)); + + if let Some(query) = load_query(name, "/highlights") { + language = language + .with_highlights_query(query.as_ref()) + .expect("failed to evaluate highlights query"); + } + if let Some(query) = load_query(name, "/brackets") { + language = language + .with_brackets_query(query.as_ref()) + .expect("failed to load brackets query"); + } + if let Some(query) = load_query(name, "/indents") { + language = language + .with_indents_query(query.as_ref()) + .expect("failed to load indents query"); + } + if let Some(query) = load_query(name, "/outline") { + language = language + .with_outline_query(query.as_ref()) + .expect("failed to load outline query"); + } + if let Some(lsp_adapter) = lsp_adapter { + language = language.with_lsp_adapter(lsp_adapter) + } + language +} + +fn load_query(name: &str, filename_prefix: &str) -> Option> { + let mut result = None; + for path in LanguageDir::iter() { + if let Some(remainder) = path.strip_prefix(name) { + if remainder.starts_with(filename_prefix) { + let contents = match LanguageDir::get(path.as_ref()).unwrap().data { + Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), + Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), + }; + match &mut result { + None => result = Some(contents), + Some(r) => r.to_mut().push_str(contents.as_ref()), + } + } + } + } + result +} diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs new file mode 100644 index 0000000000000000000000000000000000000000..f2ce41a2378cd27c20e8134900ab862eed0fa0b5 --- /dev/null +++ b/crates/zed/src/languages/c.rs @@ -0,0 +1,114 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use anyhow::{anyhow, Result}; +use client::http::{HttpClient, Method}; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +pub use language::*; +use smol::fs::{self, File}; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + +pub struct CLspAdapter; + +impl super::LspAdapter for CLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("clangd".into()) + } + + fn fetch_latest_server_version( + &self, + http: Arc, + ) -> BoxFuture<'static, Result>> { + async move { + let version = latest_github_release("clangd/clangd", http, |release_name| { + format!("clangd-mac-{release_name}.zip") + }) + .await?; + Ok(Box::new(version) as Box<_>) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: Box, + http: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + let version = version.downcast::().unwrap(); + async move { + let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); + let version_dir = container_dir.join(format!("clangd_{}", version.name)); + let binary_path = version_dir.join("bin/clangd"); + + if fs::metadata(&binary_path).await.is_err() { + let response = http + .send( + surf::RequestBuilder::new(Method::Get, version.url) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let mut file = File::create(&zip_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response, &mut file).await?; + + let unzip_status = smol::process::Command::new("unzip") + .current_dir(&container_dir) + .arg(&zip_path) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip clangd archive"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + async move { + let mut last_clangd_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_clangd_dir = Some(entry.path()); + } + } + let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let clangd_bin = clangd_dir.join("bin/clangd"); + if clangd_bin.exists() { + Ok(clangd_bin) + } else { + Err(anyhow!( + "missing clangd binary in directory {:?}", + clangd_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} +} diff --git a/crates/zed/languages/c/brackets.scm b/crates/zed/src/languages/c/brackets.scm similarity index 100% rename from crates/zed/languages/c/brackets.scm rename to crates/zed/src/languages/c/brackets.scm diff --git a/crates/zed/languages/c/config.toml b/crates/zed/src/languages/c/config.toml similarity index 88% rename from crates/zed/languages/c/config.toml rename to crates/zed/src/languages/c/config.toml index aeee919ac6158d9c424fb72d6c086e432dcb14f7..b7e1f0744308b202065b6052fa9994700edb5bcd 100644 --- a/crates/zed/languages/c/config.toml +++ b/crates/zed/src/languages/c/config.toml @@ -9,6 +9,3 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "/*", end = " */", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = [] \ No newline at end of file diff --git a/crates/zed/languages/c/highlights.scm b/crates/zed/src/languages/c/highlights.scm similarity index 100% rename from crates/zed/languages/c/highlights.scm rename to crates/zed/src/languages/c/highlights.scm diff --git a/crates/zed/languages/c/indents.scm b/crates/zed/src/languages/c/indents.scm similarity index 100% rename from crates/zed/languages/c/indents.scm rename to crates/zed/src/languages/c/indents.scm diff --git a/crates/zed/languages/c/outline.scm b/crates/zed/src/languages/c/outline.scm similarity index 100% rename from crates/zed/languages/c/outline.scm rename to crates/zed/src/languages/c/outline.scm diff --git a/crates/zed/src/languages/installation.rs b/crates/zed/src/languages/installation.rs new file mode 100644 index 0000000000000000000000000000000000000000..212ff472fcc224c6586ebc7e59cf450e49230f8c --- /dev/null +++ b/crates/zed/src/languages/installation.rs @@ -0,0 +1,111 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::{self, HttpClient, Method}; +use serde::Deserialize; +use std::{path::Path, sync::Arc}; + +pub struct GitHubLspBinaryVersion { + pub name: String, + pub url: http::Url, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct NpmInfo { + #[serde(default)] + dist_tags: NpmInfoDistTags, + versions: Vec, +} + +#[derive(Deserialize, Default)] +struct NpmInfoDistTags { + latest: Option, +} + +#[derive(Deserialize)] +pub(crate) struct GithubRelease { + name: String, + assets: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct GithubReleaseAsset { + name: String, + browser_download_url: http::Url, +} + +pub async fn npm_package_latest_version(name: &str) -> Result { + let output = smol::process::Command::new("npm") + .args(["info", name, "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm info: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + info.dist_tags + .latest + .or_else(|| info.versions.pop()) + .ok_or_else(|| anyhow!("no version found for npm package {}", name)) +} + +pub async fn npm_install_packages( + packages: impl IntoIterator, + directory: &Path, +) -> Result<()> { + let output = smol::process::Command::new("npm") + .arg("install") + .arg("--prefix") + .arg(directory) + .args( + packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")), + ) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm install: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + Ok(()) +} + +pub async fn latest_github_release( + repo_name_with_owner: &str, + http: Arc, + asset_name: impl Fn(&str) -> String, +) -> Result { + let release = http + .send( + surf::RequestBuilder::new( + Method::Get, + http::Url::parse(&format!( + "https://api.github.com/repos/{repo_name_with_owner}/releases/latest" + )) + .unwrap(), + ) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error fetching latest release: {}", err))? + .body_json::() + .await + .map_err(|err| anyhow!("error parsing latest release: {}", err))?; + let asset_name = asset_name(&release.name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + Ok(GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }) +} diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs new file mode 100644 index 0000000000000000000000000000000000000000..4069413f11129f826add608995bf5c792db709d1 --- /dev/null +++ b/crates/zed/src/languages/json.rs @@ -0,0 +1,130 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::HttpClient; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use language::{LanguageServerName, LspAdapter}; +use serde::Deserialize; +use serde_json::json; +use smol::fs; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + +pub struct JsonLspAdapter; + +impl JsonLspAdapter { + const BIN_PATH: &'static str = + "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; +} + +impl LspAdapter for JsonLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-json-languageserver".into()) + } + + fn server_args(&self) -> &[&str] { + &["--stdio"] + } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result>> { + async move { + #[derive(Deserialize)] + struct NpmInfo { + versions: Vec, + } + + let output = smol::process::Command::new("npm") + .args(["info", "vscode-json-languageserver", "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!("failed to execute npm info"))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + + Ok(Box::new( + info.versions + .pop() + .ok_or_else(|| anyhow!("no versions found in npm info"))?, + ) as Box<_>) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: Box, + _: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + let version = version.downcast::().unwrap(); + async move { + let version_dir = container_dir.join(version.as_str()); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + let output = smol::process::Command::new("npm") + .current_dir(&version_dir) + .arg("install") + .arg(format!("vscode-json-languageserver@{}", version)) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!("failed to install vscode-json-languageserver"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, 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 bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} diff --git a/crates/zed/languages/json/brackets.scm b/crates/zed/src/languages/json/brackets.scm similarity index 100% rename from crates/zed/languages/json/brackets.scm rename to crates/zed/src/languages/json/brackets.scm diff --git a/crates/zed/languages/json/config.toml b/crates/zed/src/languages/json/config.toml similarity index 83% rename from crates/zed/languages/json/config.toml rename to crates/zed/src/languages/json/config.toml index 27d1193b0d75fb9eb126a5d635d1813b11b83299..ad87dcf63322f321d025a1480e77d6e9bfdb11f7 100644 --- a/crates/zed/languages/json/config.toml +++ b/crates/zed/src/languages/json/config.toml @@ -6,6 +6,3 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = [] \ No newline at end of file diff --git a/crates/zed/languages/json/highlights.scm b/crates/zed/src/languages/json/highlights.scm similarity index 100% rename from crates/zed/languages/json/highlights.scm rename to crates/zed/src/languages/json/highlights.scm diff --git a/crates/zed/languages/json/indents.scm b/crates/zed/src/languages/json/indents.scm similarity index 100% rename from crates/zed/languages/json/indents.scm rename to crates/zed/src/languages/json/indents.scm diff --git a/crates/zed/languages/json/outline.scm b/crates/zed/src/languages/json/outline.scm similarity index 100% rename from crates/zed/languages/json/outline.scm rename to crates/zed/src/languages/json/outline.scm diff --git a/crates/zed/languages/markdown/config.toml b/crates/zed/src/languages/markdown/config.toml similarity index 100% rename from crates/zed/languages/markdown/config.toml rename to crates/zed/src/languages/markdown/config.toml diff --git a/crates/zed/languages/markdown/highlights.scm b/crates/zed/src/languages/markdown/highlights.scm similarity index 100% rename from crates/zed/languages/markdown/highlights.scm rename to crates/zed/src/languages/markdown/highlights.scm diff --git a/crates/zed/src/language.rs b/crates/zed/src/languages/rust.rs similarity index 52% rename from crates/zed/src/language.rs rename to crates/zed/src/languages/rust.rs index 0d69ebee690a02a7dff73593c538dd978f996835..f419f59abb4bdc89e2202e84879988ade69ba045 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,92 +1,50 @@ -use anyhow::{anyhow, Context, Result}; +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; -use client::http::{self, HttpClient, Method}; +use client::http::{HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use gpui::Task; pub use language::*; use lazy_static::lazy_static; use regex::Regex; -use rust_embed::RustEmbed; -use serde::Deserialize; -use serde_json::json; use smol::fs::{self, File}; -use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; -#[derive(RustEmbed)] -#[folder = "languages"] -struct LanguageDir; - -struct RustLspAdapter; -struct CLspAdapter; -struct JsonLspAdapter; - -#[derive(Deserialize)] -struct GithubRelease { - name: String, - assets: Vec, -} - -#[derive(Deserialize)] -struct GithubReleaseAsset { - name: String, - browser_download_url: http::Url, -} +pub struct RustLspAdapter; impl LspAdapter for RustLspAdapter { - fn name(&self) -> &'static str { - "rust-analyzer" + fn name(&self) -> LanguageServerName { + LanguageServerName("rust-analyzer".into()) } fn fetch_latest_server_version( &self, http: Arc, - ) -> BoxFuture<'static, Result> { + ) -> BoxFuture<'static, Result>> { async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(LspBinaryVersion { - name: release.name, - url: Some(asset.browser_download_url.clone()), + let version = latest_github_release("rust-analyzer/rust-analyzer", http, |_| { + format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH) }) + .await?; + Ok(Box::new(version) as Box<_>) } .boxed() } fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, http: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result> { async move { + let version = version.downcast::().unwrap(); let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); if fs::metadata(&destination_path).await.is_err() { let response = http .send( - surf::RequestBuilder::new(Method::Get, version.url.unwrap()) + surf::RequestBuilder::new(Method::Get, version.url) .middleware(surf::middleware::Redirect::default()) .build(), ) @@ -131,6 +89,14 @@ impl LspAdapter for RustLspAdapter { .boxed() } + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + &["rustc"] + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + Some("rustAnalyzer/cargo check") + } + fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { lazy_static! { static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); @@ -287,325 +253,11 @@ impl LspAdapter for RustLspAdapter { } } -impl LspAdapter for CLspAdapter { - fn name(&self) -> &'static str { - "clangd" - } - - fn fetch_latest_server_version( - &self, - http: Arc, - ) -> BoxFuture<'static, Result> { - async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/clangd/clangd/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("clangd-mac-{}.zip", release.name); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(LspBinaryVersion { - name: release.name, - url: Some(asset.browser_download_url.clone()), - }) - } - .boxed() - } - - fn fetch_server_binary( - &self, - version: LspBinaryVersion, - http: Arc, - container_dir: PathBuf, - ) -> BoxFuture<'static, Result> { - async move { - let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); - let version_dir = container_dir.join(format!("clangd_{}", version.name)); - let binary_path = version_dir.join("bin/clangd"); - - if fs::metadata(&binary_path).await.is_err() { - let response = http - .send( - surf::RequestBuilder::new(Method::Get, version.url.unwrap()) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - let mut file = File::create(&zip_path).await?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response, &mut file).await?; - - let unzip_status = smol::process::Command::new("unzip") - .current_dir(&container_dir) - .arg(&zip_path) - .output() - .await? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip clangd archive"))?; - } - - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - - Ok(binary_path) - } - .boxed() - } - - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { - async move { - let mut last_clangd_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_clangd_dir = Some(entry.path()); - } - } - let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let clangd_bin = clangd_dir.join("bin/clangd"); - if clangd_bin.exists() { - Ok(clangd_bin) - } else { - Err(anyhow!( - "missing clangd binary in directory {:?}", - clangd_dir - )) - } - } - .log_err() - .boxed() - } - - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} -} - -impl JsonLspAdapter { - const BIN_PATH: &'static str = - "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; -} - -impl LspAdapter for JsonLspAdapter { - fn name(&self) -> &'static str { - "vscode-json-languageserver" - } - - fn server_args(&self) -> &[&str] { - &["--stdio"] - } - - fn fetch_latest_server_version( - &self, - _: Arc, - ) -> BoxFuture<'static, Result> { - async move { - #[derive(Deserialize)] - struct NpmInfo { - versions: Vec, - } - - let output = smol::process::Command::new("npm") - .args(["info", "vscode-json-languageserver", "--json"]) - .output() - .await?; - if !output.status.success() { - Err(anyhow!("failed to execute npm info"))?; - } - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - - Ok(LspBinaryVersion { - name: info - .versions - .pop() - .ok_or_else(|| anyhow!("no versions found in npm info"))?, - url: Default::default(), - }) - } - .boxed() - } - - fn fetch_server_binary( - &self, - version: LspBinaryVersion, - _: Arc, - container_dir: PathBuf, - ) -> BoxFuture<'static, Result> { - async move { - let version_dir = container_dir.join(&version.name); - fs::create_dir_all(&version_dir) - .await - .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::BIN_PATH); - - if fs::metadata(&binary_path).await.is_err() { - let output = smol::process::Command::new("npm") - .current_dir(&version_dir) - .arg("install") - .arg(format!("vscode-json-languageserver@{}", version.name)) - .output() - .await - .context("failed to run npm install")?; - if !output.status.success() { - Err(anyhow!("failed to install vscode-json-languageserver"))?; - } - - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - - Ok(binary_path) - } - .boxed() - } - - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, 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 bin_path = last_version_dir.join(Self::BIN_PATH); - if bin_path.exists() { - Ok(bin_path) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - } - .log_err() - .boxed() - } - - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} - - fn initialization_options(&self) -> Option { - Some(json!({ - "provideFormatter": true - })) - } -} - -pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { - let languages = LanguageRegistry::new(login_shell_env_loaded); - languages.add(Arc::new(c())); - languages.add(Arc::new(json())); - languages.add(Arc::new(rust())); - languages.add(Arc::new(markdown())); - languages -} - -fn rust() -> Language { - let grammar = tree_sitter_rust::language(); - let config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("rust/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("rust/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("rust/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("rust/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(RustLspAdapter) -} - -fn c() -> Language { - let grammar = tree_sitter_c::language(); - let config = toml::from_slice(&LanguageDir::get("c/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("c/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("c/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("c/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("c/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(CLspAdapter) -} - -fn json() -> Language { - let grammar = tree_sitter_json::language(); - let config = toml::from_slice(&LanguageDir::get("json/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("json/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("json/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("json/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("json/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(JsonLspAdapter) -} - -fn markdown() -> Language { - let grammar = tree_sitter_markdown::language(); - let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("markdown/highlights.scm").as_ref()) - .unwrap() -} - -fn load_query(path: &str) -> Cow<'static, str> { - match LanguageDir::get(path).unwrap().data { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } -} - #[cfg(test)] mod tests { use super::*; + use crate::languages::{language, LspAdapter}; use gpui::color::Color; - use language::LspAdapter; use theme::SyntaxTheme; #[test] @@ -651,7 +303,11 @@ mod tests { #[test] fn test_rust_label_for_completion() { - let language = rust(); + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ); let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), @@ -726,7 +382,11 @@ mod tests { #[test] fn test_rust_label_for_symbol() { - let language = rust(); + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ); let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), diff --git a/crates/zed/languages/rust/brackets.scm b/crates/zed/src/languages/rust/brackets.scm similarity index 100% rename from crates/zed/languages/rust/brackets.scm rename to crates/zed/src/languages/rust/brackets.scm diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/src/languages/rust/config.toml similarity index 79% rename from crates/zed/languages/rust/config.toml rename to crates/zed/src/languages/rust/config.toml index 97fe231e0fa2bd2cb457c295659b9da10b8c656c..e4d222cddedaa5a004b71a74ed7d3d4dd9608bb2 100644 --- a/crates/zed/languages/rust/config.toml +++ b/crates/zed/src/languages/rust/config.toml @@ -10,7 +10,3 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "/*", end = " */", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = ["rustc"] -disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check" diff --git a/crates/zed/languages/rust/highlights.scm b/crates/zed/src/languages/rust/highlights.scm similarity index 100% rename from crates/zed/languages/rust/highlights.scm rename to crates/zed/src/languages/rust/highlights.scm diff --git a/crates/zed/languages/rust/indents.scm b/crates/zed/src/languages/rust/indents.scm similarity index 100% rename from crates/zed/languages/rust/indents.scm rename to crates/zed/src/languages/rust/indents.scm diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/src/languages/rust/outline.scm similarity index 100% rename from crates/zed/languages/rust/outline.scm rename to crates/zed/src/languages/rust/outline.scm diff --git a/crates/zed/src/languages/tsx/brackets.scm b/crates/zed/src/languages/tsx/brackets.scm new file mode 120000 index 0000000000000000000000000000000000000000..e6835c943b05c54ca6ecccc0b3bbd7673f668788 --- /dev/null +++ b/crates/zed/src/languages/tsx/brackets.scm @@ -0,0 +1 @@ +../typescript/brackets.scm \ No newline at end of file diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..a6f4a6d2d000d5bcf83219a27b52446178c7e079 --- /dev/null +++ b/crates/zed/src/languages/tsx/config.toml @@ -0,0 +1,12 @@ +name = "TSX" +path_suffixes = ["tsx", "js"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, +] diff --git a/crates/zed/src/languages/tsx/highlights-jsx.scm b/crates/zed/src/languages/tsx/highlights-jsx.scm new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/zed/src/languages/tsx/highlights.scm b/crates/zed/src/languages/tsx/highlights.scm new file mode 120000 index 0000000000000000000000000000000000000000..226302a5d1605c7110145345be31d8e0cd96818a --- /dev/null +++ b/crates/zed/src/languages/tsx/highlights.scm @@ -0,0 +1 @@ +../typescript/highlights.scm \ No newline at end of file diff --git a/crates/zed/src/languages/tsx/indents.scm b/crates/zed/src/languages/tsx/indents.scm new file mode 120000 index 0000000000000000000000000000000000000000..502c2a060af208e476e793db6b0d69060f0a5377 --- /dev/null +++ b/crates/zed/src/languages/tsx/indents.scm @@ -0,0 +1 @@ +../typescript/indents.scm \ No newline at end of file diff --git a/crates/zed/src/languages/tsx/outline.scm b/crates/zed/src/languages/tsx/outline.scm new file mode 120000 index 0000000000000000000000000000000000000000..a0df409fda15ec9b384fc7659b5c56a6797f9034 --- /dev/null +++ b/crates/zed/src/languages/tsx/outline.scm @@ -0,0 +1 @@ +../typescript/outline.scm \ No newline at end of file diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs new file mode 100644 index 0000000000000000000000000000000000000000..aca8cdef52df87e5e1d566726f75901f834543c1 --- /dev/null +++ b/crates/zed/src/languages/typescript.rs @@ -0,0 +1,146 @@ +use super::installation::{npm_install_packages, npm_package_latest_version}; +use anyhow::{anyhow, Context, Result}; +use client::http::HttpClient; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use language::{LanguageServerName, LspAdapter}; +use serde_json::json; +use smol::fs; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + +pub struct TypeScriptLspAdapter; + +impl TypeScriptLspAdapter { + const BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js"; +} + +struct Versions { + typescript_version: String, + server_version: String, +} + +impl LspAdapter for TypeScriptLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("typescript-language-server".into()) + } + + fn server_args(&self) -> &[&str] { + &["--stdio", "--tsserver-path", "node_modules/typescript/lib"] + } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result>> { + async move { + Ok(Box::new(Versions { + typescript_version: npm_package_latest_version("typescript").await?, + server_version: npm_package_latest_version("typescript-language-server").await?, + }) as Box<_>) + } + .boxed() + } + + fn fetch_server_binary( + &self, + versions: Box, + _: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + let versions = versions.downcast::().unwrap(); + async move { + let version_dir = container_dir.join(&format!( + "typescript-{}:server-{}", + versions.typescript_version, versions.server_version + )); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + npm_install_packages( + [ + ("typescript", versions.typescript_version.as_str()), + ( + "typescript-language-server", + &versions.server_version.as_str(), + ), + ], + &version_dir, + ) + .await?; + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, 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 bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &language::Language, + ) -> Option { + use lsp::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"), + _ => None, + }?; + Some(language::CodeLabel { + text: item.label.clone(), + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } + + fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} diff --git a/crates/zed/src/languages/typescript/brackets.scm b/crates/zed/src/languages/typescript/brackets.scm new file mode 100644 index 0000000000000000000000000000000000000000..63395f81d84e6452c631a9e582e2d697cba445ef --- /dev/null +++ b/crates/zed/src/languages/typescript/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/typescript/config.toml b/crates/zed/src/languages/typescript/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..5d491d2d329e89fe2a97ff8121c2d30c861b61d5 --- /dev/null +++ b/crates/zed/src/languages/typescript/config.toml @@ -0,0 +1,12 @@ +name = "TypeScript" +path_suffixes = ["ts"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, +] diff --git a/crates/zed/src/languages/typescript/highlights.scm b/crates/zed/src/languages/typescript/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..cb4e82b33d8b04da41e90c6926499669fee33b60 --- /dev/null +++ b/crates/zed/src/languages/typescript/highlights.scm @@ -0,0 +1,219 @@ +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function) (arrow_function)]) + +; Special identifiers + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^[A-Z_][A-Z\\d_]+$")) + +; Literals + +(this) @variable.builtin +(super) @variable.builtin + +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.special +(number) @number + +; Tokens + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +[ + ";" + "?." + "." + "," +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword + +; Types + +(type_identifier) @type +(predefined_type) @type.builtin + +((identifier) @type + (#match? @type "^[A-Z]")) + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Keywords + +[ "abstract" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "private" + "protected" + "public" + "type" + "readonly" + "override" +] @keyword \ No newline at end of file diff --git a/crates/zed/src/languages/typescript/indents.scm b/crates/zed/src/languages/typescript/indents.scm new file mode 100644 index 0000000000000000000000000000000000000000..107e6ff8e03b633f408676243c24d0d9707a2a26 --- /dev/null +++ b/crates/zed/src/languages/typescript/indents.scm @@ -0,0 +1,15 @@ +[ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/typescript/outline.scm b/crates/zed/src/languages/typescript/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..f8691fa41d9f64bc71e8a7ada2e6d64d62268a3a --- /dev/null +++ b/crates/zed/src/languages/typescript/outline.scm @@ -0,0 +1,55 @@ +(internal_module + "namespace" @context + name: (_) @name) @item + +(enum_declaration + "enum" @context + name: (_) @name) @item + +(function_declaration + "async"? @context + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(interface_declaration + "interface" @context + name: (_) @name) @item + +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item)) + +(class_declaration + "class" @context + name: (_) @name) @item + +(method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(public_field_definition + [ + "declare" + "readonly" + "abstract" + "static" + (accessibility_modifier) + ]* @context + name: (_) @name) @item diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 63721346c399ca60ea4f701f330ad78fb61d63e0..49efc9ade2af2749cca42cc20362a1c174dea833 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -19,7 +19,7 @@ use workspace::{ AppState, OpenNew, OpenParams, OpenPaths, Settings, }; use zed::{ - self, assets::Assets, build_window_options, build_workspace, fs::RealFs, language, menus, + self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus, }; fn main() { @@ -34,7 +34,7 @@ fn main() { let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme) .unwrap() .with_overrides( - language::PLAIN_TEXT.name(), + languages::PLAIN_TEXT.name(), settings::LanguageOverride { soft_wrap: Some(settings::SoftWrap::PreferredLineLength), ..Default::default() @@ -60,7 +60,7 @@ fn main() { app.run(move |cx| { let http = http::client(); let client = client::Client::new(http.clone()); - let mut languages = language::build_language_registry(login_shell_env_loaded); + let mut languages = languages::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_list = cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 436555b3daf365c1dc37abf2e7da413e61252e23..dd4268fd76902294345138e4c9d340378c894434 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,5 +1,5 @@ pub mod assets; -pub mod language; +pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -106,18 +106,21 @@ pub fn build_workspace( app_state: &Arc, cx: &mut ViewContext, ) -> Workspace { - cx.subscribe(&cx.handle(), |_, _, event, cx| { - let workspace::Event::PaneAdded(pane) = event; - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let breadcrumbs = cx.add_view(|_| Breadcrumbs::new()); - toolbar.add_item(breadcrumbs, cx); - let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); - toolbar.add_item(buffer_search_bar, cx); - let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, cx); - }) - }); + cx.subscribe(&cx.handle(), { + let project = project.clone(); + move |_, _, event, cx| { + let workspace::Event::PaneAdded(pane) = event; + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone())); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + }) + }); + } }) .detach(); @@ -574,7 +577,7 @@ mod tests { assert_eq!(editor.title(cx), "untitled"); assert!(Arc::ptr_eq( editor.language(cx).unwrap(), - &language::PLAIN_TEXT + &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); assert!(editor.is_dirty(cx)); @@ -664,7 +667,7 @@ mod tests { editor.update(cx, |editor, cx| { assert!(Arc::ptr_eq( editor.language(cx).unwrap(), - &language::PLAIN_TEXT + &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); assert!(editor.is_dirty(cx.as_ref())); @@ -683,6 +686,8 @@ mod tests { #[gpui::test] async fn test_pane_actions(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| pane::init(cx)); let app_state = cx.update(test_app_state); app_state @@ -740,7 +745,9 @@ mod tests { assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone())); cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem); - let workspace = workspace.read(cx); + }); + cx.foreground().run_until_parked(); + workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.panes().len(), 1); assert_eq!(workspace.active_pane(), &pane_1); }); @@ -867,12 +874,15 @@ mod tests { // Go forward to an item that has been closed, ensuring it gets re-opened at the same // location. - workspace.update(cx, |workspace, cx| { - workspace - .active_pane() - .update(cx, |pane, cx| pane.close_item(editor3.id(), cx)); - drop(editor3); - }); + workspace + .update(cx, |workspace, cx| { + let editor3_id = editor3.id(); + drop(editor3); + workspace + .active_pane() + .update(cx, |pane, cx| pane.close_item(editor3_id, cx)) + }) + .await; workspace .update(cx, |w, cx| Pane::go_forward(w, None, cx)) .await; @@ -884,15 +894,17 @@ mod tests { // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. workspace .update(cx, |workspace, cx| { + let editor2_id = editor2.id(); + drop(editor2); workspace .active_pane() - .update(cx, |pane, cx| pane.close_item(editor2.id(), cx)); - drop(editor2); - app_state - .fs - .as_fake() - .remove_file(Path::new("/root/a/file2"), Default::default()) + .update(cx, |pane, cx| pane.close_item(editor2_id, cx)) }) + .await; + app_state + .fs + .as_fake() + .remove_file(Path::new("/root/a/file2"), Default::default()) .await .unwrap(); workspace