Cargo.lock 🔗
@@ -1552,6 +1552,7 @@ dependencies = [
"language",
"lazy_static",
"log",
+ "lsp",
"parking_lot",
"postage",
"project",
Max Brunsfeld and Nathan Sobo created
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Cargo.lock | 1
crates/editor/Cargo.toml | 1
crates/editor/src/editor.rs | 108 ++++++++++++++++++++++++++++++++
crates/editor/src/multi_buffer.rs | 39 +++++++++++
crates/language/src/buffer.rs | 83 +++++++++++++++++++++++++
crates/lsp/src/lsp.rs | 27 +++++++
6 files changed, 254 insertions(+), 5 deletions(-)
@@ -1552,6 +1552,7 @@ dependencies = [
"language",
"lazy_static",
"log",
+ "lsp",
"parking_lot",
"postage",
"project",
@@ -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"
@@ -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<Self>) {
+ 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<Self>) {
let selections = self.local_selections::<usize>(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::<lsp::request::Completion>().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);
@@ -882,6 +882,45 @@ impl MultiBuffer {
})
}
+ pub fn is_completion_trigger<T>(&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<Language>> {
self.buffers
.borrow()
@@ -214,6 +214,85 @@ pub trait LocalFile: File {
);
}
+#[cfg(feature = "test-support")]
+pub struct FakeFile {
+ pub path: Arc<Path>,
+}
+
+#[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<Path> {
+ &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<Result<(clock::Global, SystemTime)>> {
+ cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) })
+ }
+
+ fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
+ -> Option<Task<Result<()>>> {
+ 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<Result<String>> {
+ 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<QueryCursor>);
#[derive(Clone)]
@@ -759,6 +838,10 @@ impl Buffer {
self.language.as_ref()
}
+ pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
+ self.language_server.as_ref().map(|state| &state.server)
+ }
+
pub fn parse_count(&self) -> usize {
self.parse_count
}
@@ -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<dyn Send + FnOnce(Result<&str, Error>)>;
pub struct LanguageServer {
next_id: AtomicUsize,
outbound_tx: RwLock<Option<channel::Sender<Vec<u8>>>>,
+ capabilities: RwLock<lsp_types::ServerCapabilities>,
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
executor: Arc<executor::Background>,
@@ -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::<notification::Initialized>(
this.outbound_tx.read().as_ref(),
InitializedParams {},
@@ -324,6 +327,10 @@ impl LanguageServer {
}
}
+ pub fn capabilities(&self) -> RwLockReadGuard<ServerCapabilities> {
+ self.capabilities.read()
+ }
+
pub fn request<T: request::Request>(
self: &Arc<Self>,
params: T::Params,
@@ -458,6 +465,13 @@ pub struct RequestId<T> {
#[cfg(any(test, feature = "test-support"))]
impl LanguageServer {
pub async fn fake(executor: Arc<executor::Background>) -> (Arc<Self>, FakeLanguageServer) {
+ Self::fake_with_capabilities(Default::default(), executor).await
+ }
+
+ pub async fn fake_with_capabilities(
+ capabilities: ServerCapabilities,
+ executor: Arc<executor::Background>,
+ ) -> (Arc<Self>, 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::<request::Initialize>().await;
- fake.respond(init_id, InitializeResult::default()).await;
+ fake.respond(
+ init_id,
+ InitializeResult {
+ capabilities,
+ ..Default::default()
+ },
+ )
+ .await;
fake.receive_notification::<notification::Initialized>()
.await;