Detailed changes
@@ -5428,6 +5428,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"
@@ -6039,6 +6049,7 @@ dependencies = [
"tree-sitter-json",
"tree-sitter-markdown",
"tree-sitter-rust",
+ "tree-sitter-typescript",
"unindent",
"url",
"util",
@@ -2593,6 +2593,8 @@ impl Editor {
}
}
}
+ } else {
+ return Ok(());
}
let mut ranges_to_highlight = Vec::new();
@@ -6451,13 +6453,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 +8894,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 +8928,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 +8944,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 +8973,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 +8986,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 +9016,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 +9175,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 +9190,7 @@ mod tests {
..Default::default()
})
.collect(),
- ))
+ )))
}
})
.next()
@@ -9197,7 +9204,7 @@ mod tests {
fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(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 +9215,7 @@ mod tests {
)]
}),
..Default::default()
- }
+ })
}
})
.next()
@@ -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},
@@ -34,6 +34,23 @@ pub struct Summary {
count: usize,
}
+impl<T> DiagnosticEntry<T> {
+ // 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<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
where
@@ -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<http::Url>,
-}
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct LanguageServerName(pub Arc<str>);
pub trait LspAdapter: 'static + Send + Sync {
- fn name(&self) -> &'static str;
+ fn name(&self) -> LanguageServerName;
fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
- ) -> BoxFuture<'static, Result<LspBinaryVersion>>;
+ ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>>;
fn fetch_server_binary(
&self,
- version: LspBinaryVersion,
+ version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> BoxFuture<'static, Result<PathBuf>>;
@@ -96,6 +94,14 @@ pub trait LspAdapter: 'static + Send + Sync {
fn initialization_options(&self) -> Option<Value> {
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<String>,
- pub language_server: Option<LanguageServerConfig>,
}
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<String>,
- pub disk_based_diagnostics_progress_token: Option<String>,
- #[cfg(any(test, feature = "test-support"))]
- #[serde(skip)]
- fake_config: Option<FakeLanguageServerConfig>,
-}
-
#[cfg(any(test, feature = "test-support"))]
-struct FakeLanguageServerConfig {
- servers_tx: mpsc::UnboundedSender<lsp::FakeLanguageServer>,
- capabilities: lsp::ServerCapabilities,
- initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
+pub struct FakeLspAdapter {
+ pub name: &'static str,
+ pub capabilities: lsp::ServerCapabilities,
+ pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
+ 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<Arc<Grammar>>,
pub(crate) adapter: Option<Arc<dyn LspAdapter>>,
- lsp_binary_path: Mutex<Option<Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>>>,
+
+ #[cfg(any(test, feature = "test-support"))]
+ fake_adapter: Option<(
+ mpsc::UnboundedSender<lsp::FakeLanguageServer>,
+ Arc<FakeLspAdapter>,
+ )>,
}
pub struct Grammar {
@@ -184,6 +186,12 @@ pub struct LanguageRegistry {
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
login_shell_env_loaded: Shared<Task<()>>,
+ lsp_binary_paths: Mutex<
+ HashMap<
+ LanguageServerName,
+ Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>,
+ >,
+ >,
}
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<Self>,
server_id: usize,
language: Arc<Language>,
root_path: Arc<Path>,
@@ -252,34 +261,20 @@ impl LanguageRegistry {
cx: &mut MutableAppContext,
) -> Option<Task<Result<lsp::LanguageServer>>> {
#[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<Path>,
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
) -> Result<PathBuf> {
- 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<Arc<dyn LspAdapter>> {
+ self.adapter.clone()
+ }
+
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
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<dyn LspAdapter>) -> 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<lsp::FakeLanguageServer> {
+ 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<str> {
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<String>> {
- 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<lsp::FakeLanguageServer>) {
- 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<dyn HttpClient>,
+ ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
+ 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<dyn 'static + Send + Any>,
+ _: Arc<dyn HttpClient>,
+ _: PathBuf,
+ ) -> BoxFuture<'static, Result<PathBuf>> {
+ unreachable!();
}
+
+ fn cached_server_binary(&self, _: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+ 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<PointUtf16>) -> 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<PointUtf16> {
- 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)
}
@@ -927,7 +927,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()),
@@ -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<dyn Send + Sync + FnMut(Option<usize>, &str, &mut channel::Sender<Vec<u8>>) -> Result<()>>;
+type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
pub struct LanguageServer {
@@ -39,18 +38,17 @@ pub struct LanguageServer {
outbound_tx: channel::Sender<Vec<u8>>,
name: String,
capabilities: ServerCapabilities,
- notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
+ notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
executor: Arc<executor::Background>,
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
output_done_rx: Mutex<Option<barrier::Receiver>>,
root_path: PathBuf,
- options: Option<Value>,
}
pub struct Subscription {
method: &'static str,
- notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
+ notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
}
#[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<T> {
id: usize,
- result: T,
+ result: Option<T>,
+ error: Option<Error>,
}
#[derive(Serialize, Deserialize)]
@@ -118,15 +105,14 @@ impl LanguageServer {
binary_path: &Path,
args: &[&str],
root_path: &Path,
- options: Option<Value>,
- background: Arc<executor::Background>,
+ cx: AsyncAppContext,
) -> Result<Self> {
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<Stdin, Stdout>(
+ fn new_internal<Stdin, Stdout, F>(
server_id: usize,
stdin: Stdin,
stdout: Stdout,
root_path: &Path,
- options: Option<Value>,
- executor: Arc<executor::Background>,
+ 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::<Vec<u8>>();
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::<AnyNotification>(&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<Arc<Self>> {
- 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<Value>) -> Result<Arc<Self>> {
+ 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::<request::Initialize>(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::<request::Initialize>(params).await?;
+ if let Some(info) = response.server_info {
+ self.name = info.name;
}
- this.notify::<notification::Initialized>(InitializedParams {})?;
- Ok(this)
+ self.capabilities = response.capabilities;
+
+ self.notify::<notification::Initialized>(InitializedParams {})?;
+ Ok(Arc::new(self))
}
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>>> {
@@ -368,37 +348,42 @@ impl LanguageServer {
}
}
- pub fn on_notification<T, F>(&mut self, f: F) -> Subscription
+ #[must_use]
+ pub fn on_notification<T, F>(&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<T, F>(&mut self, f: F) -> Subscription
+ #[must_use]
+ pub fn on_request<T, F, Fut>(&self, f: F) -> Subscription
where
T: request::Request,
- F: 'static + Send + Sync + FnMut(T::Params) -> Result<T::Result>,
+ T::Params: 'static + Send,
+ F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut,
+ Fut: 'static + Future<Output = Result<T::Result>>,
{
self.on_custom_request(T::METHOD, f)
}
- pub fn on_custom_notification<Params, F>(
- &mut self,
- method: &'static str,
- mut f: F,
- ) -> Subscription
+ pub fn remove_request_handler<T: request::Request>(&self) {
+ self.notification_handlers.lock().remove(T::METHOD);
+ }
+
+ #[must_use]
+ pub fn on_custom_notification<Params, F>(&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<Params, Res, F>(
- &mut self,
+ #[must_use]
+ pub fn on_custom_request<Params, Res, Fut, F>(
+ &self,
method: &'static str,
mut f: F,
) -> Subscription
where
- F: 'static + Send + Sync + FnMut(Params) -> Result<Res>,
- Params: DeserializeOwned,
+ F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut,
+ Fut: 'static + Future<Output = Result<Res>>,
+ 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<T: request::Request>(
- self: &Arc<Self>,
+ &self,
params: T::Params,
) -> impl Future<Output = Result<T::Result>>
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<Vec<u8>>,
- incoming_rx: futures::channel::mpsc::UnboundedReceiver<Vec<u8>>,
- _input_task: Task<Result<()>>,
- _output_task: Task<Result<()>>,
+ pub server: Arc<LanguageServer>,
+ 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<u8>>,
- >,
- >,
- >,
->;
-
#[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::<request::Initialize, _, _>({
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::<AnyRequest>(&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<T: notification::Notification>(&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<T: notification::Notification>(&self, params: T::Params) {
+ self.server.notify::<T>(params).ok();
}
pub async fn receive_notification<T: notification::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::<Notification<T::Params>>(&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::<T::Params>(¶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<T, F, Fut>(
- &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<Output = T::Result>,
+ Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
- 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::<T::Params>(params).unwrap(), cx);
+ self.server.remove_request_handler::<T>();
+ self.server
+ .on_request::<T, _, _>(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::<T>();
}
pub async fn start_progress(&mut self, token: impl Into<String>) {
@@ -783,25 +699,6 @@ impl FakeLanguageServer {
value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())),
});
}
-
- async fn receive(
- stdin: &mut smol::io::BufReader<async_pipe::PipeReader>,
- buffer: &mut Vec<u8>,
- ) -> 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<Mutex<HashMap<usize, ResponseHandler>>>);
@@ -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::<notification::ShowMessage, _>(move |params| {
+ .on_notification::<notification::ShowMessage, _>(move |params, _| {
message_tx.try_send(params).unwrap()
})
.detach();
server
- .on_notification::<notification::PublishDiagnostics, _>(move |params| {
+ .on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
diagnostics_tx.try_send(params).unwrap()
})
.detach();
- let server = server.initialize().await.unwrap();
+ let server = server.initialize(None).await.unwrap();
server
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(
@@ -876,7 +773,7 @@ mod tests {
"file://b/c"
);
- fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
+ fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
drop(server);
fake.receive_notification::<notification::Exit>().await;
@@ -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<ProjectTransaction> {
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<Vec<Location>> {
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<Vec<Location>> {
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(),
@@ -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<WorktreeHandle>,
active_entry: Option<ProjectEntryId>,
languages: Arc<LanguageRegistry>,
- language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
- started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
+ language_servers:
+ HashMap<(WorktreeId, LanguageServerName), (Arc<dyn LspAdapter>, Arc<LanguageServer>)>,
+ started_language_servers:
+ HashMap<(WorktreeId, LanguageServerName), Task<Option<Arc<LanguageServer>>>>,
language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
language_server_settings: Arc<Mutex<serde_json::Value>>,
+ last_workspace_edits_by_language_server: HashMap<usize, ProjectTransaction>,
next_language_server_id: usize,
client: Arc<client::Client>,
next_entry_id: Arc<AtomicUsize>,
@@ -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<String, LanguageServerProgress>,
@@ -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,
@@ -342,6 +332,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 +420,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 +949,8 @@ impl Project {
fn open_local_buffer_via_lsp(
&mut self,
abs_path: lsp::Url,
- lang_name: Arc<str>,
- lang_server: Arc<LanguageServer>,
+ lsp_adapter: Arc<dyn LspAdapter>,
+ lsp_server: Arc<LanguageServer>,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
cx.spawn(|this, mut cx| async move {
@@ -976,8 +968,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 +1114,7 @@ impl Project {
}
}
- if let Some(server) = language_server {
+ if let Some((_, server)) = language_server {
server
.notify::<lsp::notification::DidOpenTextDocument>(
lsp::DidOpenTextDocumentParams {
@@ -1153,7 +1147,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::notification::DidCloseTextDocument>(
lsp::DidCloseTextDocumentParams {
@@ -1189,7 +1183,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 +1205,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 +1256,11 @@ impl Project {
fn language_servers_for_worktree(
&self,
worktree_id: WorktreeId,
- ) -> impl Iterator<Item = (&str, &Arc<LanguageServer>)> {
+ ) -> impl Iterator<Item = &(Arc<dyn LspAdapter>, Arc<LanguageServer>)> {
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 +1296,12 @@ impl Project {
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
- 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 +1314,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::<lsp::notification::PublishDiagnostics, _>({
- 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::<lsp::request::WorkspaceConfiguration, _>({
+ .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
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::<lsp::notification::Progress, _>(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::<lsp::request::ApplyWorkspaceEdit, _, _>({
+ 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::<lsp::notification::Progress, _>({
+ 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 +1450,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 +1532,20 @@ impl Project {
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
- 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 +1559,138 @@ impl Project {
.detach();
}
- fn on_lsp_event(
+ fn on_lsp_diagnostics_published(
&mut self,
- language_server_id: usize,
- event: LanguageServerEvent,
- language: &Arc<Language>,
+ server_id: usize,
+ mut params: lsp::PublishDiagnosticsParams,
+ adapter: &Arc<dyn LspAdapter>,
+ disk_based_diagnostics_progress_token: Option<&str>,
cx: &mut ModelContext<Self>,
) {
- 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<Self>,
+ ) {
+ 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 +1738,40 @@ impl Project {
}
}
+ async fn on_lsp_workspace_edit(
+ this: WeakModelHandle<Self>,
+ params: lsp::ApplyWorkspaceEditParams,
+ server_id: usize,
+ adapter: Arc<dyn LspAdapter>,
+ language_server: Arc<LanguageServer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<lsp::ApplyWorkspaceEditResponse> {
+ 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 +1789,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::notification::DidChangeConfiguration>(
lsp::DidChangeConfigurationParams {
@@ -1756,7 +1810,7 @@ impl Project {
pub fn update_diagnostics(
&mut self,
params: lsp::PublishDiagnosticsParams,
- disk_based_sources: &HashSet<String>,
+ disk_based_sources: &[&str],
cx: &mut ModelContext<Self>,
) -> Result<()> {
let abs_path = params
@@ -1799,8 +1853,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
@@ -1985,7 +2040,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 +2089,12 @@ impl Project {
language_server
.request::<lsp::request::Formatting>(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 +2104,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::request::RangeFormatting>(
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 +2186,24 @@ impl Project {
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
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::request::WorkspaceSymbol>(
lsp::WorkspaceSymbolParams {
query: query.to_string(),
@@ -2155,7 +2218,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 +2235,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 +2250,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 +2297,9 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
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 +2324,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 +2365,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 +2379,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 +2401,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 +2445,7 @@ impl Project {
lsp_completion,
})
} else {
+ log::info!("completion out of expected range");
None
}
})
@@ -2414,7 +2493,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 +2564,7 @@ impl Project {
}
}
- pub fn code_actions<T: ToOffset>(
+ pub fn code_actions<T: Clone + ToOffset>(
&self,
buffer_handle: &ModelHandle<Buffer>,
range: Range<T>,
@@ -2492,6 +2572,11 @@ impl Project {
) -> Task<Result<Vec<CodeAction>>> {
let buffer_handle = buffer_handle.clone();
let buffer = buffer_handle.read(cx);
+ let snapshot = buffer.snapshot();
+ let relevant_diagnostics = snapshot
+ .diagnostics_in_range::<usize, usize>(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 +2590,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 +2612,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 +2676,12 @@ impl Project {
) -> Task<Result<ProjectTransaction>> {
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 +2692,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::<lsp::request::CodeActionResolveRequest>(action.lsp_action)
.await?;
@@ -2638,11 +2714,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::request::ExecuteCommand>(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())
}
@@ -229,7 +229,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;
@@ -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;
@@ -1088,10 +1088,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,
};
- use lsp;
+ use lsp::{self, FakeLanguageServer};
use parking_lot::Mutex;
use postage::barrier;
use project::{
@@ -2039,22 +2039,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 +2259,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 +2342,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 +2368,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 +2395,7 @@ mod tests {
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
..Default::default()
},
- ]))
+ ])))
})
.next()
.await
@@ -2427,7 +2425,7 @@ mod tests {
fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
|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 +2441,7 @@ mod tests {
}]),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
..Default::default()
- }
+ })
},
);
@@ -2463,22 +2461,20 @@ mod tests {
#[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 +2530,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::<lsp::request::Formatting, _, _>(|_, _| 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 +2541,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 +2559,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 +2578,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 +2637,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::<lsp::request::GotoDefinition, _, _>(
|_, _| 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 +2671,11 @@ mod tests {
// the previous call to `definition`.
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
|_, _| 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 +2703,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 +2723,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 +2782,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::<lsp::request::References, _, _>(
|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 +2811,7 @@ mod tests {
lsp::Position::new(0, 40),
),
},
- ])
+ ]))
},
);
@@ -2967,16 +2963,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 +3022,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::<lsp::request::DocumentHighlightRequest, _, _>(
|params, _| async move {
assert_eq!(
@@ -3041,7 +3037,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 +3059,7 @@ mod tests {
lsp::Position::new(0, 47),
),
},
- ])
+ ]))
},
);
@@ -3092,7 +3088,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 +3108,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 +3166,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::<lsp::request::WorkspaceSymbol, _, _>(
|_, _| 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 +3180,7 @@ mod tests {
tags: None,
container_name: None,
deprecated: None,
- }])
+ }]))
},
);
@@ -3231,7 +3225,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 +3238,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 +3296,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::<lsp::request::GotoDefinition, _, _>(
|_, _| 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 +3330,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 +3419,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 +3439,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 +3481,7 @@ mod tests {
})),
..Default::default()
},
- )])
+ )]))
})
.next()
.await;
@@ -3513,7 +3504,7 @@ mod tests {
.unwrap();
fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
|_, _| async move {
- lsp::CodeAction {
+ Ok(lsp::CodeAction {
title: "Inline into all callers".to_string(),
edit: Some(lsp::WorkspaceEdit {
changes: Some(
@@ -3545,7 +3536,7 @@ mod tests {
..Default::default()
}),
..Default::default()
- }
+ })
},
);
@@ -3573,23 +3564,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 +3643,7 @@ mod tests {
.unwrap()
.downcast::<Editor>()
.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 +3655,10 @@ mod tests {
.handle_request::<lsp::request::PrepareRenameRequest, _, _>(|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 +3692,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 +3729,7 @@ mod tests {
.collect(),
),
..Default::default()
- })
+ }))
})
.next()
.await
@@ -4838,7 +4827,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 +4841,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 +4858,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 +4881,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::<lsp::request::Completion, _, _>(
+ |_, _| 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::<lsp::request::CodeActionRequest, _, _>(
+ |_, _| async move {
+ Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
+ lsp::CodeAction {
+ title: "the-code-action".to_string(),
+ ..Default::default()
+ },
+ )]))
+ },
+ );
+
+ fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
+ |params, _| async move {
+ Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+ params.position,
+ params.position,
+ ))))
+ },
+ );
+
+ fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
+ 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::<usize, _>(1..3);
+ let files = (0..count)
+ .map(|_| files.choose(&mut *rng).unwrap())
+ .collect::<Vec<_>>();
+ 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::<lsp::request::DocumentHighlightRequest, _, _>({
+ 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 +5441,128 @@ mod tests {
})
}
- fn simulate_host(
+ async fn simulate_host(
mut self,
project: ModelHandle<Project>,
- mut language_server_config: LanguageServerConfig,
+ files: Arc<Mutex<Vec<PathBuf>>>,
operations: Rc<Cell<usize>>,
max_operations: usize,
rng: Arc<Mutex<StdRng>>,
mut cx: TestAppContext,
- ) -> impl Future<Output = (Self, TestAppContext)> {
- let files: Arc<Mutex<Vec<PathBuf>>> = 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::<lsp::request::Completion, _, _>(
- |_, _| 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::<lsp::request::CodeActionRequest, _, _>(
- |_, _| async move {
- Some(vec![lsp::CodeActionOrCommand::CodeAction(
- lsp::CodeAction {
- title: "the-code-action".to_string(),
- ..Default::default()
- },
- )])
- },
- );
-
- fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
- |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::<lsp::request::GotoDefinition, _, _>({
- 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::<usize, _>(1..3);
- let files = (0..count)
- .map(|_| files.choose(&mut *rng).unwrap())
- .collect::<Vec<_>>();
- 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::<usize, _>(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::<lsp::request::DocumentHighlightRequest, _, _>({
- 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::<usize, _>(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::<usize, _>(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::<usize, _>(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(
@@ -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<usize> {
+ let offset = offset_after(text, part);
+ offset..offset
+ }
+
+ fn range_of(text: &str, part: &str) -> Range<usize> {
+ 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()));
@@ -1508,6 +1508,30 @@ impl BufferSnapshot {
.eq(needle.bytes())
}
+ pub fn common_prefix_at<T>(&self, position: T, needle: &str) -> Range<T>
+ 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()
}
@@ -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]
@@ -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<dyn LspAdapter>),
+ ),
+ (
+ "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<Arc<dyn LspAdapter>>,
+) -> 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<Cow<'static, str>> {
+ 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
+}
@@ -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<dyn HttpClient>,
+ ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
+ 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<dyn 'static + Send + Any>,
+ http: Arc<dyn HttpClient>,
+ container_dir: PathBuf,
+ ) -> BoxFuture<'static, Result<PathBuf>> {
+ let version = version.downcast::<GitHubLspBinaryVersion>().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<PathBuf>> {
+ 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) {}
+}
@@ -9,6 +9,3 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false },
{ start = "/*", end = " */", close = true, newline = false },
]
-
-[language_server]
-disk_based_diagnostic_sources = []
@@ -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<String>,
+}
+
+#[derive(Deserialize, Default)]
+struct NpmInfoDistTags {
+ latest: Option<String>,
+}
+
+#[derive(Deserialize)]
+pub(crate) struct GithubRelease {
+ name: String,
+ assets: Vec<GithubReleaseAsset>,
+}
+
+#[derive(Deserialize)]
+pub(crate) struct GithubReleaseAsset {
+ name: String,
+ browser_download_url: http::Url,
+}
+
+pub async fn npm_package_latest_version(name: &str) -> Result<String> {
+ 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<Item = (&str, &str)>,
+ 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<dyn HttpClient>,
+ asset_name: impl Fn(&str) -> String,
+) -> Result<GitHubLspBinaryVersion> {
+ 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::<GithubRelease>()
+ .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(),
+ })
+}
@@ -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<dyn HttpClient>,
+ ) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
+ async move {
+ #[derive(Deserialize)]
+ struct NpmInfo {
+ versions: Vec<String>,
+ }
+
+ 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<dyn 'static + Send + Any>,
+ _: Arc<dyn HttpClient>,
+ container_dir: PathBuf,
+ ) -> BoxFuture<'static, Result<PathBuf>> {
+ let version = version.downcast::<String>().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<PathBuf>> {
+ 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<serde_json::Value> {
+ Some(json!({
+ "provideFormatter": true
+ }))
+ }
+}
@@ -6,6 +6,3 @@ brackets = [
{ start = "[", end = "]", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false },
]
-
-[language_server]
-disk_based_diagnostic_sources = []
@@ -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<GithubReleaseAsset>,
-}
-
-#[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<dyn HttpClient>,
- ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+ ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
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::<GithubRelease>()
- .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<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> BoxFuture<'static, Result<PathBuf>> {
async move {
+ let version = version.downcast::<GitHubLspBinaryVersion>().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<dyn HttpClient>,
- ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
- 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::<GithubRelease>()
- .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<dyn HttpClient>,
- container_dir: PathBuf,
- ) -> BoxFuture<'static, Result<PathBuf>> {
- 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<PathBuf>> {
- 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<dyn HttpClient>,
- ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
- async move {
- #[derive(Deserialize)]
- struct NpmInfo {
- versions: Vec<String>,
- }
-
- 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<dyn HttpClient>,
- container_dir: PathBuf,
- ) -> BoxFuture<'static, Result<PathBuf>> {
- 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<PathBuf>> {
- 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<serde_json::Value> {
- 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()),
@@ -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"
@@ -0,0 +1 @@
+../typescript/brackets.scm
@@ -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 },
+]
@@ -0,0 +1 @@
+../typescript/highlights.scm
@@ -0,0 +1 @@
+../typescript/indents.scm
@@ -0,0 +1 @@
+../typescript/outline.scm
@@ -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<dyn HttpClient>,
+ ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
+ 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<dyn 'static + Send + Any>,
+ _: Arc<dyn HttpClient>,
+ container_dir: PathBuf,
+ ) -> BoxFuture<'static, Result<PathBuf>> {
+ let versions = versions.downcast::<Versions>().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<PathBuf>> {
+ 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<language::CodeLabel> {
+ 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<serde_json::Value> {
+ Some(json!({
+ "provideFormatter": true
+ }))
+ }
+}
@@ -0,0 +1,5 @@
+("(" @open ")" @close)
+("[" @open "]" @close)
+("{" @open "}" @close)
+("<" @open ">" @close)
+("\"" @open "\"" @close)
@@ -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 },
+]
@@ -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
@@ -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
@@ -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
@@ -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));
@@ -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;
@@ -574,7 +574,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 +664,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()));