diff --git a/Cargo.lock b/Cargo.lock index f0c2c1a2c7..d7c811bc53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1552,6 +1552,7 @@ dependencies = [ "language", "lazy_static", "log", + "lsp", "parking_lot", "postage", "project", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index b953d160ad..6450db7d39 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -41,6 +41,7 @@ smol = "1.2" [dev-dependencies] text = { path = "../text", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 89e99354d6..4e383d5518 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1253,6 +1253,7 @@ impl Editor { self.insert(text, cx); self.autoclose_pairs(cx); self.end_transaction(cx); + self.trigger_completion_on_input(text, cx); } } @@ -1388,6 +1389,20 @@ impl Editor { self.end_transaction(cx); } + fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { + if self.completion_state.is_none() { + if let Some(selection) = self.newest_anchor_selection() { + if self + .buffer + .read(cx) + .is_completion_trigger(selection.head(), text, cx) + { + self.show_completions(&ShowCompletions, cx); + } + } + } + } + fn autoclose_pairs(&mut self, cx: &mut ViewContext) { let selections = self.local_selections::(cx); let mut bracket_pair_state = None; @@ -4398,8 +4413,8 @@ pub fn char_kind(c: char) -> CharKind { #[cfg(test)] mod tests { use super::*; - use language::LanguageConfig; - use std::{cell::RefCell, rc::Rc, time::Instant}; + use language::{FakeFile, LanguageConfig}; + use std::{cell::RefCell, path::Path, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; use util::test::sample_text; @@ -6456,6 +6471,95 @@ mod tests { }); } + #[gpui::test] + async fn test_completion(mut cx: gpui::TestAppContext) { + let settings = cx.read(EditorSettings::test); + let (language_server, mut fake) = lsp::LanguageServer::fake_with_capabilities( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx.background(), + ) + .await; + + let text = " + one + two + three + " + .unindent(); + let buffer = cx.add_model(|cx| { + Buffer::from_file( + 0, + text, + Box::new(FakeFile { + path: Arc::from(Path::new("/the/file")), + }), + cx, + ) + .with_language_server(language_server, cx) + }); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + editor.update(&mut cx, |editor, cx| { + editor.select_ranges([3..3], None, cx); + editor.handle_input(&Input(".".to_string()), cx); + }); + + let (id, params) = fake.receive_request::().await; + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/the/file").unwrap() + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 4) + ); + + fake.respond( + id, + Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), + new_text: "first_completion".to_string(), + })), + ..Default::default() + }, + lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), + new_text: "second_completion".to_string(), + })), + ..Default::default() + }, + ])), + ) + .await; + + editor.next_notification(&cx).await; + + editor.update(&mut cx, |editor, cx| { + editor.move_down(&MoveDown, cx); + editor.confirm_completion(&ConfirmCompletion, cx); + assert_eq!( + editor.text(cx), + " + one.second_completion + two + three + " + .unindent() + ); + }); + } + #[gpui::test] async fn test_toggle_comment(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a5c2a64bea..11aec10d5c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -882,6 +882,45 @@ impl MultiBuffer { }) } + pub fn is_completion_trigger(&self, position: T, text: &str, cx: &AppContext) -> bool + where + T: ToOffset, + { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + if chars.next().is_some() { + return false; + } + + if char.is_alphanumeric() || char == '_' { + return true; + } + + let snapshot = self.snapshot(cx); + let anchor = snapshot.anchor_before(position); + let buffer = self.buffers.borrow()[&anchor.buffer_id].buffer.clone(); + if let Some(language_server) = buffer.read(cx).language_server() { + language_server + .capabilities() + .completion_provider + .as_ref() + .map_or(false, |provider| { + provider + .trigger_characters + .as_ref() + .map_or(false, |characters| { + characters.iter().any(|string| string == text) + }) + }) + } else { + false + } + } + pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { self.buffers .borrow() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a5fc6a8b84..a8a642920c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -214,6 +214,85 @@ pub trait LocalFile: File { ); } +#[cfg(feature = "test-support")] +pub struct FakeFile { + pub path: Arc, +} + +#[cfg(feature = "test-support")] +impl File for FakeFile { + fn as_local(&self) -> Option<&dyn LocalFile> { + Some(self) + } + + fn mtime(&self) -> SystemTime { + SystemTime::UNIX_EPOCH + } + + fn path(&self) -> &Arc { + &self.path + } + + fn full_path(&self, _: &AppContext) -> PathBuf { + self.path.to_path_buf() + } + + fn file_name(&self, _: &AppContext) -> OsString { + self.path.file_name().unwrap().to_os_string() + } + + fn is_deleted(&self) -> bool { + false + } + + fn save( + &self, + _: u64, + _: Rope, + _: clock::Global, + cx: &mut MutableAppContext, + ) -> Task> { + cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) + } + + fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) + -> Option>> { + None + } + + fn buffer_updated(&self, _: u64, operation: Operation, cx: &mut MutableAppContext) {} + + fn buffer_removed(&self, _: u64, cx: &mut MutableAppContext) {} + + fn as_any(&self) -> &dyn Any { + self + } + + fn to_proto(&self) -> rpc::proto::File { + unimplemented!() + } +} + +#[cfg(feature = "test-support")] +impl LocalFile for FakeFile { + fn abs_path(&self, _: &AppContext) -> PathBuf { + self.path.to_path_buf() + } + + fn load(&self, cx: &AppContext) -> Task> { + cx.background().spawn(async move { Ok(Default::default()) }) + } + + fn buffer_reloaded( + &self, + buffer_id: u64, + version: &clock::Global, + mtime: SystemTime, + cx: &mut MutableAppContext, + ) { + } +} + pub(crate) struct QueryCursorHandle(Option); #[derive(Clone)] @@ -759,6 +838,10 @@ impl Buffer { self.language.as_ref() } + pub fn language_server(&self) -> Option<&Arc> { + self.language_server.as_ref().map(|state| &state.server) + } + pub fn parse_count(&self) -> usize { self.parse_count } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index b2c658dbd6..803148427f 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context, Result}; use futures::{io::BufWriter, AsyncRead, AsyncWrite}; use gpui::{executor, Task}; -use parking_lot::{Mutex, RwLock}; +use parking_lot::{Mutex, RwLock, RwLockReadGuard}; use postage::{barrier, oneshot, prelude::Stream, sink::Sink}; use serde::{Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; @@ -34,6 +34,7 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, outbound_tx: RwLock>>>, + capabilities: RwLock, notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, @@ -197,6 +198,7 @@ impl LanguageServer { let this = Arc::new(Self { notification_handlers, response_handlers, + capabilities: Default::default(), next_id: Default::default(), outbound_tx: RwLock::new(Some(outbound_tx)), executor: executor.clone(), @@ -265,7 +267,8 @@ impl LanguageServer { this.outbound_tx.read().as_ref(), params, ); - request.await?; + let response = request.await?; + *this.capabilities.write() = response.capabilities; Self::notify_internal::( this.outbound_tx.read().as_ref(), InitializedParams {}, @@ -324,6 +327,10 @@ impl LanguageServer { } } + pub fn capabilities(&self) -> RwLockReadGuard { + self.capabilities.read() + } + pub fn request( self: &Arc, params: T::Params, @@ -458,6 +465,13 @@ pub struct RequestId { #[cfg(any(test, feature = "test-support"))] impl LanguageServer { pub async fn fake(executor: Arc) -> (Arc, FakeLanguageServer) { + Self::fake_with_capabilities(Default::default(), executor).await + } + + pub async fn fake_with_capabilities( + capabilities: ServerCapabilities, + executor: Arc, + ) -> (Arc, FakeLanguageServer) { let stdin = async_pipe::pipe(); let stdout = async_pipe::pipe(); let mut fake = FakeLanguageServer { @@ -470,7 +484,14 @@ impl LanguageServer { let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap(); let (init_id, _) = fake.receive_request::().await; - fake.respond(init_id, InitializeResult::default()).await; + fake.respond( + init_id, + InitializeResult { + capabilities, + ..Default::default() + }, + ) + .await; fake.receive_notification::() .await;