Trigger completion when typing words or trigger characters
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
8d2b7ba032
commit
1d1f8df180
6 changed files with 254 additions and 5 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1552,6 +1552,7 @@ dependencies = [
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"lsp",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
|
|
@ -41,6 +41,7 @@ smol = "1.2"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
text = { path = "../text", features = ["test-support"] }
|
text = { path = "../text", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
ctor = "0.1"
|
ctor = "0.1"
|
||||||
|
|
|
@ -1253,6 +1253,7 @@ impl Editor {
|
||||||
self.insert(text, cx);
|
self.insert(text, cx);
|
||||||
self.autoclose_pairs(cx);
|
self.autoclose_pairs(cx);
|
||||||
self.end_transaction(cx);
|
self.end_transaction(cx);
|
||||||
|
self.trigger_completion_on_input(text, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1388,6 +1389,20 @@ impl Editor {
|
||||||
self.end_transaction(cx);
|
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>) {
|
fn autoclose_pairs(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let selections = self.local_selections::<usize>(cx);
|
let selections = self.local_selections::<usize>(cx);
|
||||||
let mut bracket_pair_state = None;
|
let mut bracket_pair_state = None;
|
||||||
|
@ -4398,8 +4413,8 @@ pub fn char_kind(c: char) -> CharKind {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use language::LanguageConfig;
|
use language::{FakeFile, LanguageConfig};
|
||||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
use std::{cell::RefCell, path::Path, rc::Rc, time::Instant};
|
||||||
use text::Point;
|
use text::Point;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::test::sample_text;
|
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]
|
#[gpui::test]
|
||||||
async fn test_toggle_comment(mut cx: gpui::TestAppContext) {
|
async fn test_toggle_comment(mut cx: gpui::TestAppContext) {
|
||||||
let settings = cx.read(EditorSettings::test);
|
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>> {
|
pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
|
||||||
self.buffers
|
self.buffers
|
||||||
.borrow()
|
.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>);
|
pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -759,6 +838,10 @@ impl Buffer {
|
||||||
self.language.as_ref()
|
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 {
|
pub fn parse_count(&self) -> usize {
|
||||||
self.parse_count
|
self.parse_count
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use futures::{io::BufWriter, AsyncRead, AsyncWrite};
|
use futures::{io::BufWriter, AsyncRead, AsyncWrite};
|
||||||
use gpui::{executor, Task};
|
use gpui::{executor, Task};
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock, RwLockReadGuard};
|
||||||
use postage::{barrier, oneshot, prelude::Stream, sink::Sink};
|
use postage::{barrier, oneshot, prelude::Stream, sink::Sink};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, value::RawValue, Value};
|
use serde_json::{json, value::RawValue, Value};
|
||||||
|
@ -34,6 +34,7 @@ type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
|
||||||
pub struct LanguageServer {
|
pub struct LanguageServer {
|
||||||
next_id: AtomicUsize,
|
next_id: AtomicUsize,
|
||||||
outbound_tx: RwLock<Option<channel::Sender<Vec<u8>>>>,
|
outbound_tx: RwLock<Option<channel::Sender<Vec<u8>>>>,
|
||||||
|
capabilities: RwLock<lsp_types::ServerCapabilities>,
|
||||||
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
||||||
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
|
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
|
||||||
executor: Arc<executor::Background>,
|
executor: Arc<executor::Background>,
|
||||||
|
@ -197,6 +198,7 @@ impl LanguageServer {
|
||||||
let this = Arc::new(Self {
|
let this = Arc::new(Self {
|
||||||
notification_handlers,
|
notification_handlers,
|
||||||
response_handlers,
|
response_handlers,
|
||||||
|
capabilities: Default::default(),
|
||||||
next_id: Default::default(),
|
next_id: Default::default(),
|
||||||
outbound_tx: RwLock::new(Some(outbound_tx)),
|
outbound_tx: RwLock::new(Some(outbound_tx)),
|
||||||
executor: executor.clone(),
|
executor: executor.clone(),
|
||||||
|
@ -265,7 +267,8 @@ impl LanguageServer {
|
||||||
this.outbound_tx.read().as_ref(),
|
this.outbound_tx.read().as_ref(),
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
request.await?;
|
let response = request.await?;
|
||||||
|
*this.capabilities.write() = response.capabilities;
|
||||||
Self::notify_internal::<notification::Initialized>(
|
Self::notify_internal::<notification::Initialized>(
|
||||||
this.outbound_tx.read().as_ref(),
|
this.outbound_tx.read().as_ref(),
|
||||||
InitializedParams {},
|
InitializedParams {},
|
||||||
|
@ -324,6 +327,10 @@ impl LanguageServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn capabilities(&self) -> RwLockReadGuard<ServerCapabilities> {
|
||||||
|
self.capabilities.read()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn request<T: request::Request>(
|
pub fn request<T: request::Request>(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
params: T::Params,
|
params: T::Params,
|
||||||
|
@ -458,6 +465,13 @@ pub struct RequestId<T> {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
impl LanguageServer {
|
impl LanguageServer {
|
||||||
pub async fn fake(executor: Arc<executor::Background>) -> (Arc<Self>, FakeLanguageServer) {
|
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 stdin = async_pipe::pipe();
|
||||||
let stdout = async_pipe::pipe();
|
let stdout = async_pipe::pipe();
|
||||||
let mut fake = FakeLanguageServer {
|
let mut fake = FakeLanguageServer {
|
||||||
|
@ -470,7 +484,14 @@ impl LanguageServer {
|
||||||
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
|
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
|
||||||
|
|
||||||
let (init_id, _) = fake.receive_request::<request::Initialize>().await;
|
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>()
|
fake.receive_notification::<notification::Initialized>()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue