logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
82
crates/languages/Cargo.toml
Normal file
82
crates/languages/Cargo.toml
Normal file
@@ -0,0 +1,82 @@
|
||||
[package]
|
||||
name = "languages"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"load-grammars"
|
||||
]
|
||||
load-grammars = [
|
||||
"grammars/load-grammars",
|
||||
"tree-sitter",
|
||||
"tree-sitter-gitcommit",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-fs.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
globset.workspace = true
|
||||
gpui.workspace = true
|
||||
grammars.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
json_schema_store.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pet-conda.workspace = true
|
||||
pet-core.workspace = true
|
||||
pet-fs.workspace = true
|
||||
pet-poetry.workspace = true
|
||||
pet-reporter.workspace = true
|
||||
pet-virtualenv.workspace = true
|
||||
pet.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
semver.workspace = true
|
||||
smol.workspace = true
|
||||
snippet.workspace = true
|
||||
task.workspace = true
|
||||
terminal.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter = { workspace = true, optional = true }
|
||||
tree-sitter-gitcommit = { workspace = true, optional = true }
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
tempfile = { workspace = true}
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-bash.workspace = true
|
||||
tree-sitter-c.workspace = true
|
||||
tree-sitter-cpp.workspace = true
|
||||
tree-sitter-css.workspace = true
|
||||
tree-sitter-go.workspace = true
|
||||
tree-sitter-python.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
tree-sitter-typescript.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
unindent.workspace = true
|
||||
1
crates/languages/LICENSE-GPL
Symbolic link
1
crates/languages/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
325
crates/languages/src/bash.rs
Normal file
325
crates/languages/src/bash.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::ContextProviderWithTasks;
|
||||
use semver::Version;
|
||||
use std::{future::Future, path::PathBuf, sync::Arc, vec};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::{ResultExt, maybe};
|
||||
|
||||
pub(super) fn bash_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "execute selection".to_owned(),
|
||||
command: VariableName::SelectedText.template_value(),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("run '{}'", VariableName::File.template_value()),
|
||||
command: VariableName::File.template_value(),
|
||||
tags: vec!["bash-script".to_owned()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
|
||||
pub struct BashLspAdapter {
|
||||
node: NodeRuntime,
|
||||
}
|
||||
|
||||
impl BashLspAdapter {
|
||||
const PACKAGE_NAME: &str = "bash-language-server";
|
||||
const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "bash-language-server/out/cli.js";
|
||||
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
Self { node }
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
env: HashMap<String, String>,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<lsp::LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let server_path = container_dir
|
||||
.join("node_modules")
|
||||
.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
|
||||
anyhow::ensure!(
|
||||
server_path.exists(),
|
||||
"missing executable in directory {server_path:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: Some(env),
|
||||
arguments: vec![server_path.into(), "start".into()],
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
}
|
||||
|
||||
impl LspInstaller for BashLspAdapter {
|
||||
type BinaryVersion = Version;
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: std::path::PathBuf,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Option<lsp::LanguageServerBinary> {
|
||||
let env = delegate.shell_env().await;
|
||||
Self::get_cached_server_binary(container_dir, env, &self.node).await
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &gpui::AsyncApp,
|
||||
) -> Option<lsp::LanguageServerBinary> {
|
||||
let path = delegate.which(Self::PACKAGE_NAME.as_ref()).await?;
|
||||
let env = delegate.shell_env().await;
|
||||
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
env: Some(env),
|
||||
arguments: vec!["start".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn check_if_version_installed(
|
||||
&self,
|
||||
version: &Self::BinaryVersion,
|
||||
container_dir: &PathBuf,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Option<lsp::LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
let version = version.clone();
|
||||
let container_dir = container_dir.clone();
|
||||
let delegate = delegate.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir
|
||||
.join("node_modules")
|
||||
.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
|
||||
|
||||
let should_install_language_server = node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&version),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
let env = delegate.shell_env().await;
|
||||
Some(LanguageServerBinary {
|
||||
path: node.binary_path().await.ok()?,
|
||||
env: Some(env),
|
||||
arguments: vec![server_path.into(), "start".into()],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut gpui::AsyncApp,
|
||||
) -> Result<Self::BinaryVersion> {
|
||||
self.node
|
||||
.npm_package_latest_version(Self::PACKAGE_NAME)
|
||||
.await
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
container_dir: std::path::PathBuf,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<lsp::LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
let delegate = delegate.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir
|
||||
.join("node_modules")
|
||||
.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
|
||||
let latest_version = latest_version.to_string();
|
||||
|
||||
node.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let env = delegate.shell_env().await;
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: Some(env),
|
||||
arguments: vec![server_path.into(), "start".into()],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for BashLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName::new_static(Self::PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
|
||||
use language::{AutoindentMode, Buffer};
|
||||
use settings::SettingsStore;
|
||||
use std::num::NonZeroU32;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bash_autoindent(cx: &mut TestAppContext) {
|
||||
cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
let language = crate::language("bash", tree_sitter_bash::LANGUAGE.into());
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
let expect_indents_to =
|
||||
|buffer: &mut Buffer, cx: &mut Context<Buffer>, input: &str, expected: &str| {
|
||||
buffer.edit(
|
||||
[(0..buffer.len(), input)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(buffer.text(), expected);
|
||||
};
|
||||
|
||||
// Do not indent after shebang
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"#!/usr/bin/env bash\n#",
|
||||
"#!/usr/bin/env bash\n#",
|
||||
);
|
||||
|
||||
// indent function correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"function name() {\necho \"Hello, World!\"\n}",
|
||||
"function name() {\n echo \"Hello, World!\"\n}",
|
||||
);
|
||||
|
||||
// indent if-else correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"if true;then\nfoo\nelse\nbar\nfi",
|
||||
"if true;then\n foo\nelse\n bar\nfi",
|
||||
);
|
||||
|
||||
// indent if-elif-else correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"if true;then\nfoo\nelif true;then\nbar\nelse\nbar\nfi",
|
||||
"if true;then\n foo\nelif true;then\n bar\nelse\n bar\nfi",
|
||||
);
|
||||
|
||||
// indent case-when-else correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"case $1 in\nfoo) echo \"Hello, World!\";;\n*) echo \"Unknown argument\";;\nesac",
|
||||
"case $1 in\n foo) echo \"Hello, World!\";;\n *) echo \"Unknown argument\";;\nesac",
|
||||
);
|
||||
|
||||
// indent for-loop correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"for i in {1..10};do\nfoo\ndone",
|
||||
"for i in {1..10};do\n foo\ndone",
|
||||
);
|
||||
|
||||
// indent while-loop correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"while true; do\nfoo\ndone",
|
||||
"while true; do\n foo\ndone",
|
||||
);
|
||||
|
||||
// indent array correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"array=(\n1\n2\n3\n)",
|
||||
"array=(\n 1\n 2\n 3\n)",
|
||||
);
|
||||
|
||||
// indents non-"function" function correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"foo() {\necho \"Hello, World!\"\n}",
|
||||
"foo() {\n echo \"Hello, World!\"\n}",
|
||||
);
|
||||
|
||||
let (input, offsets) = marked_text_offsets(
|
||||
&r#"
|
||||
if foo; then
|
||||
1ˇ
|
||||
else
|
||||
3
|
||||
fi
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), input)], None, cx);
|
||||
buffer.edit(
|
||||
[(offsets[0]..offsets[0], "\n")],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
buffer.edit(
|
||||
[(offsets[0] + 3..offsets[0] + 3, "elif")],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
let expected = r#"
|
||||
if foo; then
|
||||
1
|
||||
elif
|
||||
else
|
||||
3
|
||||
fi
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
pretty_assertions::assert_eq!(buffer.text(), expected);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
}
|
||||
667
crates/languages/src/c.rs
Normal file
667
crates/languages/src/c.rs
Normal file
@@ -0,0 +1,667 @@
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, AsyncApp};
|
||||
use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
|
||||
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
|
||||
pub use language::*;
|
||||
use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName};
|
||||
use project::lsp_store::clangd_ext;
|
||||
use serde_json::json;
|
||||
use smol::fs;
|
||||
use std::{env::consts, future::Future, path::PathBuf, sync::Arc};
|
||||
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
|
||||
|
||||
pub struct CLspAdapter;
|
||||
|
||||
impl CLspAdapter {
|
||||
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd");
|
||||
}
|
||||
|
||||
impl LspInstaller for CLspAdapter {
|
||||
type BinaryVersion = GitHubLspBinaryVersion;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
pre_release: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<GitHubLspBinaryVersion> {
|
||||
ensure_arch_compatibility()?;
|
||||
|
||||
let release =
|
||||
latest_github_release("clangd/clangd", true, pre_release, delegate.http_client())
|
||||
.await?;
|
||||
let os_suffix = match consts::OS {
|
||||
"macos" => "mac",
|
||||
"linux" => "linux",
|
||||
"windows" => "windows",
|
||||
other => bail!("Running on unsupported os: {other}"),
|
||||
};
|
||||
let asset_name = format!("clangd-{}-{}.zip", os_suffix, release.tag_name);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.with_context(|| format!("no asset found matching {asset_name:?}"))?;
|
||||
let version = GitHubLspBinaryVersion {
|
||||
name: release.tag_name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
digest: asset.digest.clone(),
|
||||
};
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
arguments: Vec::new(),
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: GitHubLspBinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let delegate = delegate.clone();
|
||||
|
||||
async move {
|
||||
ensure_arch_compatibility()?;
|
||||
|
||||
let GitHubLspBinaryVersion {
|
||||
name,
|
||||
url,
|
||||
digest: expected_digest,
|
||||
} = version;
|
||||
let version_dir = container_dir.join(format!("clangd_{name}"));
|
||||
let binary_path = version_dir
|
||||
.join("bin")
|
||||
.join(format!("clangd{}", consts::EXE_SUFFIX));
|
||||
|
||||
let binary = LanguageServerBinary {
|
||||
path: binary_path.clone(),
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
};
|
||||
|
||||
let metadata_path = version_dir.join("metadata");
|
||||
let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
|
||||
.await
|
||||
.ok();
|
||||
if let Some(metadata) = metadata {
|
||||
let validity_check = async || {
|
||||
delegate
|
||||
.try_exec(LanguageServerBinary {
|
||||
path: binary_path.clone(),
|
||||
arguments: vec!["--version".into()],
|
||||
env: None,
|
||||
})
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
log::warn!(
|
||||
"Unable to run {binary_path:?} asset, redownloading: {err:#}",
|
||||
)
|
||||
})
|
||||
};
|
||||
if let (Some(actual_digest), Some(expected_digest)) =
|
||||
(&metadata.digest, &expected_digest)
|
||||
{
|
||||
if actual_digest == expected_digest {
|
||||
if validity_check().await.is_ok() {
|
||||
return Ok(binary);
|
||||
}
|
||||
} else {
|
||||
log::info!(
|
||||
"SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
|
||||
);
|
||||
}
|
||||
} else if validity_check().await.is_ok() {
|
||||
return Ok(binary);
|
||||
}
|
||||
}
|
||||
download_server_binary(
|
||||
&*delegate.http_client(),
|
||||
&url,
|
||||
expected_digest.as_deref(),
|
||||
&container_dir,
|
||||
AssetKind::Zip,
|
||||
)
|
||||
.await?;
|
||||
remove_matching(&container_dir, |entry| entry != version_dir).await;
|
||||
GithubBinaryMetadata::write_to_file(
|
||||
&GithubBinaryMetadata {
|
||||
metadata_version: 1,
|
||||
digest: expected_digest,
|
||||
},
|
||||
&metadata_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(binary)
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_arch_compatibility() -> Result<()> {
|
||||
let arch = consts::ARCH;
|
||||
if consts::OS == "linux" && !["x86_64", "x86"].contains(&arch) {
|
||||
anyhow::bail!(
|
||||
"Clangd does not provide prebuilt binaries for {arch} to fetch from GitHub. Consider installing the binary manually."
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl super::LspAdapter for CLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
Self::SERVER_NAME
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
completion: &lsp::CompletionItem,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let label_detail = match &completion.label_details {
|
||||
Some(label_detail) => match &label_detail.detail {
|
||||
Some(detail) => detail.trim(),
|
||||
None => "",
|
||||
},
|
||||
None => "",
|
||||
};
|
||||
|
||||
let mut label = completion
|
||||
.label
|
||||
.strip_prefix('•')
|
||||
.unwrap_or(&completion.label)
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
if !label_detail.is_empty() {
|
||||
let should_add_space = match completion.kind {
|
||||
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if should_add_space && !label.ends_with(' ') && !label_detail.starts_with(' ') {
|
||||
label.push(' ');
|
||||
}
|
||||
label.push_str(label_detail);
|
||||
}
|
||||
|
||||
match completion.kind {
|
||||
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
|
||||
let detail = completion.detail.as_ref().unwrap();
|
||||
let text = format!("{} {}", detail, label);
|
||||
let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
|
||||
let runs = language.highlight_text(&source, 11..11 + text.len());
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
.as_deref()
|
||||
.and_then(|filter_text| {
|
||||
text.find(filter_text)
|
||||
.map(|start| start..start + filter_text.len())
|
||||
})
|
||||
.unwrap_or(detail.len() + 1..text.len());
|
||||
return Some(CodeLabel::new(text, filter_range, runs));
|
||||
}
|
||||
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
|
||||
if completion.detail.is_some() =>
|
||||
{
|
||||
let detail = completion.detail.as_ref().unwrap();
|
||||
let text = format!("{} {}", detail, label);
|
||||
let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
.as_deref()
|
||||
.and_then(|filter_text| {
|
||||
text.find(filter_text)
|
||||
.map(|start| start..start + filter_text.len())
|
||||
})
|
||||
.unwrap_or(detail.len() + 1..text.len());
|
||||
return Some(CodeLabel::new(text, filter_range, runs));
|
||||
}
|
||||
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
|
||||
if completion.detail.is_some() =>
|
||||
{
|
||||
let detail = completion.detail.as_ref().unwrap();
|
||||
let text = format!("{} {}", detail, label);
|
||||
let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
.as_deref()
|
||||
.and_then(|filter_text| {
|
||||
text.find(filter_text)
|
||||
.map(|start| start..start + filter_text.len())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let filter_start = detail.len() + 1;
|
||||
let filter_end = text
|
||||
.rfind('(')
|
||||
.filter(|end| *end > filter_start)
|
||||
.unwrap_or(text.len());
|
||||
filter_start..filter_end
|
||||
});
|
||||
|
||||
return Some(CodeLabel::new(text, filter_range, runs));
|
||||
}
|
||||
Some(kind) => {
|
||||
let highlight_name = match kind {
|
||||
lsp::CompletionItemKind::STRUCT
|
||||
| lsp::CompletionItemKind::INTERFACE
|
||||
| lsp::CompletionItemKind::CLASS
|
||||
| lsp::CompletionItemKind::ENUM => Some("type"),
|
||||
lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
|
||||
lsp::CompletionItemKind::KEYWORD => Some("keyword"),
|
||||
lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
|
||||
Some("constant")
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(highlight_id) = language
|
||||
.grammar()
|
||||
.and_then(|g| g.highlight_id_for_name(highlight_name?))
|
||||
{
|
||||
let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
|
||||
label.runs.push((
|
||||
0..label.text.rfind('(').unwrap_or(label.text.len()),
|
||||
highlight_id,
|
||||
));
|
||||
return Some(label);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(CodeLabel::plain(label, completion.filter_text.as_deref()))
|
||||
}
|
||||
|
||||
async fn label_for_symbol(
|
||||
&self,
|
||||
symbol: &language::Symbol,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let name = &symbol.name;
|
||||
let (text, filter_range, display_range) = match symbol.kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
let text = format!("void {} () {{}}", name);
|
||||
let filter_range = 0..name.len();
|
||||
let display_range = 5..5 + name.len();
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::STRUCT => {
|
||||
let text = format!("struct {} {{}}", name);
|
||||
let filter_range = 7..7 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::ENUM => {
|
||||
let text = format!("enum {} {{}}", name);
|
||||
let filter_range = 5..5 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
|
||||
let text = format!("class {} {{}}", name);
|
||||
let filter_range = 6..6 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CONSTANT => {
|
||||
let text = format!("const int {} = 0;", name);
|
||||
let filter_range = 10..10 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::MODULE => {
|
||||
let text = format!("namespace {} {{}}", name);
|
||||
let filter_range = 10..10 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::TYPE_PARAMETER => {
|
||||
let text = format!("typename {} {{}};", name);
|
||||
let filter_range = 9..9 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(CodeLabel::new(
|
||||
text[display_range.clone()].to_string(),
|
||||
filter_range,
|
||||
language.highlight_text(&text.as_str().into(), display_range),
|
||||
))
|
||||
}
|
||||
|
||||
fn prepare_initialize_params(
|
||||
&self,
|
||||
mut original: InitializeParams,
|
||||
_: &App,
|
||||
) -> Result<InitializeParams> {
|
||||
let experimental = json!({
|
||||
"textDocument": {
|
||||
"completion" : {
|
||||
// enable clangd's dot-to-arrow feature.
|
||||
"editsNearCursor": true
|
||||
},
|
||||
"inactiveRegionsCapabilities": {
|
||||
"inactiveRegions": true,
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(ref mut original_experimental) = original.capabilities.experimental {
|
||||
merge_json_value_into(experimental, original_experimental);
|
||||
} else {
|
||||
original.capabilities.experimental = Some(experimental);
|
||||
}
|
||||
Ok(original)
|
||||
}
|
||||
|
||||
fn retain_old_diagnostic(&self, previous_diagnostic: &Diagnostic) -> bool {
|
||||
clangd_ext::is_inactive_region(previous_diagnostic)
|
||||
}
|
||||
|
||||
fn underline_diagnostic(&self, diagnostic: &lsp::Diagnostic) -> bool {
|
||||
!clangd_ext::is_lsp_inactive_region(diagnostic)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
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.context("no cached binary")?;
|
||||
let clangd_bin = clangd_dir
|
||||
.join("bin")
|
||||
.join(format!("clangd{}", consts::EXE_SUFFIX));
|
||||
anyhow::ensure!(
|
||||
clangd_bin.exists(),
|
||||
"missing clangd binary in directory {clangd_dir:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: clangd_bin,
|
||||
env: None,
|
||||
arguments: Vec::new(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext as _, BorrowAppContext, TestAppContext};
|
||||
use language::{AutoindentMode, Buffer};
|
||||
use settings::SettingsStore;
|
||||
use std::num::NonZeroU32;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_c_autoindent_basic(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit([(0..0, "int main() {}")], None, cx);
|
||||
|
||||
let ix = buffer.len() - 1;
|
||||
buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"int main() {\n \n}",
|
||||
"content inside braces should be indented"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_c_autoindent_if_else(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"body of if-statement without braces should be indented"
|
||||
);
|
||||
|
||||
let ix = buffer.len() - 4;
|
||||
buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b
|
||||
.c;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"field expression (.c) should be indented further than the statement body"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a) a++;
|
||||
else b++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a) a++;
|
||||
else b++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"single-line if/else without braces should align at the same level"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"multi-line if/else without braces should indent statement bodies"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
if (b)
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
if (b)
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"nested if statements without braces should indent properly"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else if (c)
|
||||
d++;
|
||||
else
|
||||
f++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else if (c)
|
||||
d++;
|
||||
else
|
||||
f++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"else-if chains should align all conditions at same level with indented bodies"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a) {
|
||||
b++;
|
||||
} else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a) {
|
||||
b++;
|
||||
} else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"mixed braces should indent properly"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
}
|
||||
588
crates/languages/src/cpp.rs
Normal file
588
crates/languages/src/cpp.rs
Normal file
@@ -0,0 +1,588 @@
|
||||
use settings::SemanticTokenRules;
|
||||
|
||||
pub(crate) fn semantic_token_rules() -> SemanticTokenRules {
|
||||
let content = grammars::get_file("cpp/semantic_token_rules.json")
|
||||
.expect("missing cpp/semantic_token_rules.json");
|
||||
let json = std::str::from_utf8(&content.data).expect("invalid utf-8 in semantic_token_rules");
|
||||
settings::parse_json_with_comments::<SemanticTokenRules>(json)
|
||||
.expect("failed to parse cpp semantic_token_rules.json")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext as _, BorrowAppContext, TestAppContext};
|
||||
use language::{AutoindentMode, Buffer};
|
||||
use settings::SettingsStore;
|
||||
use std::num::NonZeroU32;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_access_specifier(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
void bar();
|
||||
private:
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
void bar();
|
||||
private:
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
"members after access specifiers should be indented one level deeper than the specifier"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_access_specifier_next_line(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
void bar();
|
||||
void baz();
|
||||
private:
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
void bar();
|
||||
void baz();
|
||||
private:
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
"members after access specifiers should be indented one level deeper than the specifier"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_nested_class_access_specifiers(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
class Outer {
|
||||
public:
|
||||
class Inner {
|
||||
public:
|
||||
void inner_pub();
|
||||
private:
|
||||
int inner_priv;
|
||||
};
|
||||
private:
|
||||
int outer_priv;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
class Outer {
|
||||
public:
|
||||
class Inner {
|
||||
public:
|
||||
void inner_pub();
|
||||
private:
|
||||
int inner_priv;
|
||||
};
|
||||
private:
|
||||
int outer_priv;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
"nested class access specifiers should indent independently at each nesting level"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_consecutive_access_specifiers(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
protected:
|
||||
private:
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
protected:
|
||||
private:
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
"consecutive access specifiers with no members between them should all align at class level"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_indented_access_specifiers(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
class Foo {
|
||||
int default_member;
|
||||
public:
|
||||
void pub_method();
|
||||
private:
|
||||
int priv_member;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
class Foo {
|
||||
int default_member;
|
||||
public:
|
||||
void pub_method();
|
||||
private:
|
||||
int priv_member;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
"access specifiers should be indented one level inside class braces"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_access_specifier_with_method_bodies(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
void bar() {
|
||||
if (x)
|
||||
y++;
|
||||
}
|
||||
private:
|
||||
int get_x() {
|
||||
return x;
|
||||
}
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
class Foo {
|
||||
public:
|
||||
void bar() {
|
||||
if (x)
|
||||
y++;
|
||||
}
|
||||
private:
|
||||
int get_x() {
|
||||
return x;
|
||||
}
|
||||
int x;
|
||||
};
|
||||
"#
|
||||
.unindent(),
|
||||
"method bodies inside access specifier sections should compose brace and specifier indent"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_basic(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit([(0..0, "int main() {}")], None, cx);
|
||||
|
||||
let ix = buffer.len() - 1;
|
||||
buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"int main() {\n \n}",
|
||||
"content inside braces should be indented"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cpp_autoindent_if_else(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let test_settings = SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||||
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"body of if-statement without braces should be indented"
|
||||
);
|
||||
|
||||
let ix = buffer.len() - 4;
|
||||
buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b
|
||||
.c;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"field expression (.c) should be indented further than the statement body"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a) a++;
|
||||
else b++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a) a++;
|
||||
else b++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"single-line if/else without braces should align at the same level"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"multi-line if/else without braces should indent statement bodies"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
if (b)
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
if (b)
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"nested if statements without braces should indent properly"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else if (c)
|
||||
d++;
|
||||
else
|
||||
f++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a)
|
||||
b++;
|
||||
else if (c)
|
||||
d++;
|
||||
else
|
||||
f++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"else-if chains should align all conditions at same level with indented bodies"
|
||||
);
|
||||
|
||||
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
r#"
|
||||
int main() {
|
||||
if (a) {
|
||||
b++;
|
||||
} else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
)],
|
||||
Some(AutoindentMode::EachLine),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
int main() {
|
||||
if (a) {
|
||||
b++;
|
||||
} else
|
||||
c++;
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
"mixed braces should indent properly"
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
}
|
||||
258
crates/languages/src/css.rs
Normal file
258
crates/languages/src/css.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::AsyncApp;
|
||||
use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName, Uri};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::lsp_store::language_server_settings;
|
||||
use semver::Version;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, maybe, merge_json_value_into};
|
||||
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
pub struct CssLspAdapter {
|
||||
node: NodeRuntime,
|
||||
}
|
||||
|
||||
impl CssLspAdapter {
|
||||
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
CssLspAdapter { node }
|
||||
}
|
||||
}
|
||||
|
||||
impl LspInstaller for CssLspAdapter {
|
||||
type BinaryVersion = Version;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Self::BinaryVersion> {
|
||||
self.node
|
||||
.npm_package_latest_version("vscode-langservers-extracted")
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate
|
||||
.which("vscode-css-language-server".as_ref())
|
||||
.await?;
|
||||
let env = delegate.shell_env().await;
|
||||
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
env: Some(env),
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let latest_version = latest_version.to_string();
|
||||
|
||||
node.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn check_if_version_installed(
|
||||
&self,
|
||||
version: &Self::BinaryVersion,
|
||||
container_dir: &PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
let version = version.clone();
|
||||
let container_dir = container_dir.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&version),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for CssLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vscode-css-language-server".into())
|
||||
}
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true
|
||||
})))
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Option<Toolchain>,
|
||||
_: Option<Uri>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut default_config = json!({
|
||||
"css": {
|
||||
"lint": {}
|
||||
},
|
||||
"less": {
|
||||
"lint": {}
|
||||
},
|
||||
"scss": {
|
||||
"lint": {}
|
||||
}
|
||||
});
|
||||
|
||||
let project_options = cx.update(|cx| {
|
||||
language_server_settings(delegate.as_ref(), &self.name(), cx)
|
||||
.and_then(|s| s.settings.clone())
|
||||
});
|
||||
|
||||
if let Some(override_options) = project_options {
|
||||
merge_json_value_into(override_options, &mut default_config);
|
||||
}
|
||||
|
||||
Ok(default_config)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
anyhow::ensure!(
|
||||
server_path.exists(),
|
||||
"missing executable in directory {server_path:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext as _, TestAppContext};
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_outline(cx: &mut TestAppContext) {
|
||||
let language = crate::language("css", tree_sitter_css::LANGUAGE.into());
|
||||
|
||||
let text = r#"
|
||||
/* Import statement */
|
||||
@import './fonts.css';
|
||||
|
||||
/* multiline list of selectors with nesting */
|
||||
.test-class,
|
||||
div {
|
||||
.nested-class {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
/* descendant selectors */
|
||||
.test .descendant {}
|
||||
|
||||
/* pseudo */
|
||||
.test:not(:hover) {}
|
||||
|
||||
/* media queries */
|
||||
@media screen and (min-width: 3000px) {
|
||||
.desktop-class {}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
|
||||
let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
|
||||
assert_eq!(
|
||||
outline
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("@import './fonts.css'", 0),
|
||||
(".test-class, div", 0),
|
||||
(".nested-class", 1),
|
||||
(".test .descendant", 0),
|
||||
(".test:not(:hover)", 0),
|
||||
("@media screen and (min-width: 3000px)", 0),
|
||||
(".desktop-class", 1),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
1147
crates/languages/src/eslint.rs
Normal file
1147
crates/languages/src/eslint.rs
Normal file
File diff suppressed because it is too large
Load Diff
1661
crates/languages/src/go.rs
Normal file
1661
crates/languages/src/go.rs
Normal file
File diff suppressed because it is too large
Load Diff
573
crates/languages/src/json.rs
Normal file
573
crates/languages/src/json.rs
Normal file
@@ -0,0 +1,573 @@
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
|
||||
use language::{
|
||||
Buffer, ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
|
||||
LspAdapterDelegate, LspInstaller, Toolchain,
|
||||
};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName, Uri};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::lsp_store::language_server_settings;
|
||||
use semver::Version;
|
||||
use serde_json::{Value, json};
|
||||
use settings::SettingsLocation;
|
||||
use smol::{
|
||||
fs::{self},
|
||||
io::BufReader,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
env::consts,
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::{
|
||||
ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into,
|
||||
paths::PathStyle, rel_path::RelPath,
|
||||
};
|
||||
|
||||
use crate::PackageJsonData;
|
||||
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
|
||||
|
||||
pub(crate) struct JsonTaskProvider;
|
||||
|
||||
impl ContextProvider for JsonTaskProvider {
|
||||
fn associated_tasks(
|
||||
&self,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
cx: &App,
|
||||
) -> gpui::Task<Option<TaskTemplates>> {
|
||||
let file = buffer.as_ref().and_then(|buf| buf.read(cx).file());
|
||||
let Some(file) = project::File::from_dyn(file).cloned() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let is_package_json = file.path.ends_with(RelPath::unix("package.json").unwrap());
|
||||
let is_composer_json = file.path.ends_with(RelPath::unix("composer.json").unwrap());
|
||||
if !is_package_json && !is_composer_json {
|
||||
return Task::ready(None);
|
||||
}
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let contents = file
|
||||
.worktree
|
||||
.update(cx, |this, cx| this.load_file(&file.path, cx))
|
||||
.await
|
||||
.ok()?;
|
||||
let path = cx.update(|cx| file.abs_path(cx)).as_path().into();
|
||||
|
||||
let task_templates = if is_package_json {
|
||||
let package_json = serde_json_lenient::from_str::<
|
||||
HashMap<String, serde_json_lenient::Value>,
|
||||
>(&contents.text)
|
||||
.ok()?;
|
||||
let package_json = PackageJsonData::new(path, package_json);
|
||||
let command = package_json.package_manager.unwrap_or("npm").to_owned();
|
||||
package_json
|
||||
.scripts
|
||||
.into_iter()
|
||||
.map(|(_, key)| TaskTemplate {
|
||||
label: format!("run {key}"),
|
||||
command: command.clone(),
|
||||
args: vec!["run".into(), key],
|
||||
cwd: Some(VariableName::Dirname.template_value()),
|
||||
..TaskTemplate::default()
|
||||
})
|
||||
.chain([TaskTemplate {
|
||||
label: "package script $ZED_CUSTOM_script".to_owned(),
|
||||
command: command.clone(),
|
||||
args: vec![
|
||||
"run".into(),
|
||||
VariableName::Custom("script".into()).template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::Dirname.template_value()),
|
||||
tags: vec!["package-script".into()],
|
||||
..TaskTemplate::default()
|
||||
}])
|
||||
.collect()
|
||||
} else if is_composer_json {
|
||||
serde_json_lenient::Value::from_str(&contents.text)
|
||||
.ok()?
|
||||
.get("scripts")?
|
||||
.as_object()?
|
||||
.keys()
|
||||
.map(|key| TaskTemplate {
|
||||
label: format!("run {key}"),
|
||||
command: "composer".to_owned(),
|
||||
args: vec!["-d".into(), "$ZED_DIRNAME".into(), key.into()],
|
||||
..TaskTemplate::default()
|
||||
})
|
||||
.chain([TaskTemplate {
|
||||
label: "composer script $ZED_CUSTOM_script".to_owned(),
|
||||
command: "composer".to_owned(),
|
||||
args: vec![
|
||||
"-d".into(),
|
||||
"$ZED_DIRNAME".into(),
|
||||
VariableName::Custom("script".into()).template_value(),
|
||||
],
|
||||
tags: vec!["composer-script".into()],
|
||||
..TaskTemplate::default()
|
||||
}])
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Some(TaskTemplates(task_templates))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
pub struct JsonLspAdapter {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
node: NodeRuntime,
|
||||
}
|
||||
|
||||
impl JsonLspAdapter {
|
||||
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
|
||||
|
||||
pub fn new(languages: Arc<LanguageRegistry>, node: NodeRuntime) -> Self {
|
||||
Self { languages, node }
|
||||
}
|
||||
}
|
||||
|
||||
impl LspInstaller for JsonLspAdapter {
|
||||
type BinaryVersion = Version;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Self::BinaryVersion> {
|
||||
self.node
|
||||
.npm_package_latest_version(Self::PACKAGE_NAME)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate
|
||||
.which("vscode-json-language-server".as_ref())
|
||||
.await?;
|
||||
let env = delegate.shell_env().await;
|
||||
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
env: Some(env),
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn check_if_version_installed(
|
||||
&self,
|
||||
version: &Self::BinaryVersion,
|
||||
container_dir: &PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
let version = version.clone();
|
||||
let container_dir = container_dir.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&version),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let latest_version = latest_version.to_string();
|
||||
|
||||
node.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for JsonLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("json-language-server".into())
|
||||
}
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true
|
||||
})))
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Option<Toolchain>,
|
||||
requested_uri: Option<Uri>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let requested_path = requested_uri.as_ref().and_then(|uri| {
|
||||
(uri.scheme() == "file")
|
||||
.then(|| uri.to_file_path().ok())
|
||||
.flatten()
|
||||
});
|
||||
let path_in_worktree = requested_path
|
||||
.as_ref()
|
||||
.and_then(|abs_path| {
|
||||
let rel_path = abs_path.strip_prefix(delegate.worktree_root_path()).ok()?;
|
||||
RelPath::new(rel_path, PathStyle::local()).ok()
|
||||
})
|
||||
.unwrap_or_else(|| Cow::Borrowed(RelPath::empty()));
|
||||
let mut config = cx.update(|cx| {
|
||||
let schemas = json_schema_store::all_schema_file_associations(
|
||||
&self.languages,
|
||||
Some(SettingsLocation {
|
||||
worktree_id: delegate.worktree_id(),
|
||||
path: path_in_worktree.as_ref(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
// This can be viewed via `dev: open language server logs` -> `json-language-server` ->
|
||||
// `Server Info`
|
||||
serde_json::json!({
|
||||
"json": {
|
||||
"format": {
|
||||
"enable": true,
|
||||
},
|
||||
"validate": {
|
||||
"enable": true,
|
||||
},
|
||||
"schemas": schemas
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(proxy_settings) = cx.update(|cx| {
|
||||
json_schema_proxy_settings(cx.http_client().proxy().map(ToString::to_string))
|
||||
}) {
|
||||
merge_json_value_into(proxy_settings, &mut config);
|
||||
}
|
||||
|
||||
let project_options = cx.update(|cx| {
|
||||
language_server_settings(delegate.as_ref(), &self.name(), cx)
|
||||
.and_then(|s| worktree_root(delegate, s.settings.clone()))
|
||||
});
|
||||
|
||||
if let Some(override_options) = project_options {
|
||||
merge_json_value_into(override_options, &mut config);
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<LanguageName, String> {
|
||||
[
|
||||
(LanguageName::new_static("JSON"), "json".into()),
|
||||
(LanguageName::new_static("JSONC"), "jsonc".into()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_primary_zed_json_schema_adapter(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn worktree_root(delegate: &Arc<dyn LspAdapterDelegate>, settings: Option<Value>) -> Option<Value> {
|
||||
let Some(Value::Object(mut settings_map)) = settings else {
|
||||
return settings;
|
||||
};
|
||||
|
||||
let Some(Value::Object(json_config)) = settings_map.get_mut("json") else {
|
||||
return Some(Value::Object(settings_map));
|
||||
};
|
||||
|
||||
let Some(Value::Array(schemas)) = json_config.get_mut("schemas") else {
|
||||
return Some(Value::Object(settings_map));
|
||||
};
|
||||
|
||||
for schema in schemas.iter_mut() {
|
||||
let Value::Object(schema_map) = schema else {
|
||||
continue;
|
||||
};
|
||||
let Some(Value::String(url)) = schema_map.get_mut("url") else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !url.starts_with(".") && !url.starts_with("~") {
|
||||
continue;
|
||||
}
|
||||
|
||||
*url = delegate
|
||||
.resolve_relative_path(url.clone().into())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
}
|
||||
|
||||
Some(Value::Object(settings_map))
|
||||
}
|
||||
|
||||
fn json_schema_proxy_settings(proxy: Option<String>) -> Option<Value> {
|
||||
proxy.map(|proxy| {
|
||||
json!({
|
||||
"http": {
|
||||
"proxy": proxy,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
anyhow::ensure!(
|
||||
server_path.exists(),
|
||||
"missing executable in directory {server_path:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::json_schema_proxy_settings;
|
||||
|
||||
#[test]
|
||||
fn test_json_schema_proxy_settings_includes_proxy() {
|
||||
assert_eq!(
|
||||
json_schema_proxy_settings(Some("http://proxy.example:8080".to_string())),
|
||||
Some(json!({
|
||||
"http": {
|
||||
"proxy": "http://proxy.example:8080",
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_schema_proxy_settings_ignores_missing_proxy() {
|
||||
assert_eq!(json_schema_proxy_settings(None), None);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeVersionAdapter;
|
||||
|
||||
impl NodeVersionAdapter {
|
||||
const SERVER_NAME: LanguageServerName =
|
||||
LanguageServerName::new_static("package-version-server");
|
||||
}
|
||||
|
||||
impl LspInstaller for NodeVersionAdapter {
|
||||
type BinaryVersion = GitHubLspBinaryVersion;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<GitHubLspBinaryVersion> {
|
||||
let release = latest_github_release(
|
||||
"zed-industries/package-version-server",
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
let os = match consts::OS {
|
||||
"macos" => "apple-darwin",
|
||||
"linux" => "unknown-linux-gnu",
|
||||
"windows" => "pc-windows-msvc",
|
||||
other => bail!("Running on unsupported os: {other}"),
|
||||
};
|
||||
let suffix = if consts::OS == "windows" {
|
||||
".zip"
|
||||
} else {
|
||||
".tar.gz"
|
||||
};
|
||||
let asset_name = format!("{}-{}-{os}{suffix}", Self::SERVER_NAME, consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
|
||||
Ok(GitHubLspBinaryVersion {
|
||||
name: release.tag_name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
digest: asset.digest.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: GitHubLspBinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let delegate = delegate.clone();
|
||||
|
||||
async move {
|
||||
let version = &latest_version;
|
||||
let destination_path = container_dir.join(format!(
|
||||
"{}-{}{}",
|
||||
Self::SERVER_NAME,
|
||||
version.name,
|
||||
std::env::consts::EXE_SUFFIX
|
||||
));
|
||||
let destination_container_path =
|
||||
container_dir.join(format!("{}-{}-tmp", Self::SERVER_NAME, version.name));
|
||||
if fs::metadata(&destination_path).await.is_err() {
|
||||
let mut response = delegate
|
||||
.http_client()
|
||||
.get(&version.url, Default::default(), true)
|
||||
.await
|
||||
.context("downloading release")?;
|
||||
if version.url.ends_with(".zip") {
|
||||
extract_zip(&destination_container_path, response.body_mut()).await?;
|
||||
} else if version.url.ends_with(".tar.gz") {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&destination_container_path).await?;
|
||||
}
|
||||
|
||||
fs::copy(
|
||||
destination_container_path.join(format!(
|
||||
"{}{}",
|
||||
Self::SERVER_NAME,
|
||||
std::env::consts::EXE_SUFFIX
|
||||
)),
|
||||
&destination_path,
|
||||
)
|
||||
.await?;
|
||||
remove_matching(&container_dir, |entry| entry != destination_path).await;
|
||||
}
|
||||
Ok(LanguageServerBinary {
|
||||
path: destination_path,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_delegate: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_version_server_binary(container_dir).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for NodeVersionAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
Self::SERVER_NAME
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
|
||||
anyhow::Ok(LanguageServerBinary {
|
||||
path: last.context("no cached binary")?,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
388
crates/languages/src/lib.rs
Normal file
388
crates/languages/src/lib.rs
Normal file
@@ -0,0 +1,388 @@
|
||||
use gpui::{App, SharedString, UpdateGlobal};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::Fs;
|
||||
use python::PyprojectTomlManifestProvider;
|
||||
use rust::CargoManifestProvider;
|
||||
use settings::{SemanticTokenRules, SettingsStore};
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub use language::*;
|
||||
|
||||
use crate::{
|
||||
json::JsonTaskProvider,
|
||||
python::{BasedPyrightLspAdapter, RuffLspAdapter},
|
||||
};
|
||||
|
||||
mod bash;
|
||||
mod c;
|
||||
mod cpp;
|
||||
mod css;
|
||||
mod eslint;
|
||||
mod go;
|
||||
mod json;
|
||||
mod package_json;
|
||||
mod python;
|
||||
mod rust;
|
||||
mod tailwind;
|
||||
mod tailwindcss;
|
||||
mod typescript;
|
||||
mod vtsls;
|
||||
mod yaml;
|
||||
|
||||
pub(crate) use package_json::{PackageJson, PackageJsonData};
|
||||
|
||||
/// A shared grammar for plain text, exposed for reuse by downstream crates.
|
||||
#[cfg(feature = "tree-sitter-gitcommit")]
|
||||
pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock<Arc<Language>> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Git Commit".into(),
|
||||
soft_wrap: Some(language::SoftWrap::EditorWidth),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["COMMIT_EDITMSG".to_owned()],
|
||||
first_line_pattern: None,
|
||||
..LanguageMatcher::default()
|
||||
},
|
||||
line_comments: vec![Arc::from("#")],
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
Some(tree_sitter_gitcommit::LANGUAGE.into()),
|
||||
))
|
||||
});
|
||||
|
||||
pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime, cx: &mut App) {
|
||||
#[cfg(feature = "load-grammars")]
|
||||
languages.register_native_grammars(grammars::native_grammars());
|
||||
|
||||
let bash_lsp_adapter = Arc::new(bash::BashLspAdapter::new(node.clone()));
|
||||
let c_lsp_adapter = Arc::new(c::CLspAdapter);
|
||||
let css_lsp_adapter = Arc::new(css::CssLspAdapter::new(node.clone()));
|
||||
let eslint_adapter = Arc::new(eslint::EsLintLspAdapter::new(node.clone(), fs.clone()));
|
||||
let go_context_provider = Arc::new(go::GoContextProvider);
|
||||
let go_lsp_adapter = Arc::new(go::GoLspAdapter);
|
||||
let json_context_provider = Arc::new(JsonTaskProvider);
|
||||
let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(languages.clone(), node.clone()));
|
||||
let node_version_lsp_adapter = Arc::new(json::NodeVersionAdapter);
|
||||
let py_lsp_adapter = Arc::new(python::PyLspAdapter::new());
|
||||
let ty_lsp_adapter = Arc::new(python::TyLspAdapter::new(fs.clone()));
|
||||
let python_context_provider = Arc::new(python::PythonContextProvider);
|
||||
let python_lsp_adapter = Arc::new(python::PyrightLspAdapter::new(node.clone()));
|
||||
let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new(node.clone()));
|
||||
let ruff_lsp_adapter = Arc::new(RuffLspAdapter::new(fs.clone()));
|
||||
let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::new(fs.clone()));
|
||||
let rust_context_provider = Arc::new(rust::RustContextProvider);
|
||||
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
|
||||
let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone()));
|
||||
let tailwindcss_adapter = Arc::new(tailwindcss::TailwindCssLspAdapter::new(node.clone()));
|
||||
let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new(fs.clone()));
|
||||
let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(
|
||||
node.clone(),
|
||||
fs.clone(),
|
||||
));
|
||||
let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone(), fs.clone()));
|
||||
let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node));
|
||||
|
||||
let built_in_languages = [
|
||||
LanguageInfo {
|
||||
name: "bash",
|
||||
context: Some(Arc::new(bash::bash_task_context())),
|
||||
adapters: vec![bash_lsp_adapter],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "c",
|
||||
adapters: vec![c_lsp_adapter.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "cpp",
|
||||
adapters: vec![c_lsp_adapter],
|
||||
semantic_token_rules: Some(cpp::semantic_token_rules()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "css",
|
||||
adapters: vec![css_lsp_adapter],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "diff",
|
||||
adapters: vec![],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "go",
|
||||
adapters: vec![go_lsp_adapter.clone()],
|
||||
context: Some(go_context_provider.clone()),
|
||||
semantic_token_rules: Some(go::semantic_token_rules()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "gomod",
|
||||
adapters: vec![go_lsp_adapter.clone()],
|
||||
context: Some(go_context_provider.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "gowork",
|
||||
adapters: vec![go_lsp_adapter],
|
||||
context: Some(go_context_provider),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "json",
|
||||
adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter],
|
||||
context: Some(json_context_provider.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "jsonc",
|
||||
adapters: vec![json_lsp_adapter],
|
||||
context: Some(json_context_provider),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "markdown",
|
||||
adapters: vec![],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "markdown-inline",
|
||||
adapters: vec![],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "python",
|
||||
adapters: vec![
|
||||
basedpyright_lsp_adapter,
|
||||
ruff_lsp_adapter,
|
||||
ty_lsp_adapter,
|
||||
py_lsp_adapter,
|
||||
python_lsp_adapter,
|
||||
],
|
||||
context: Some(python_context_provider),
|
||||
toolchain: Some(python_toolchain_provider),
|
||||
manifest_name: Some(SharedString::new_static("pyproject.toml").into()),
|
||||
semantic_token_rules: Some(python::semantic_token_rules()),
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "rust",
|
||||
adapters: vec![rust_lsp_adapter],
|
||||
context: Some(rust_context_provider),
|
||||
manifest_name: Some(SharedString::new_static("Cargo.toml").into()),
|
||||
semantic_token_rules: Some(rust::semantic_token_rules()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "tsx",
|
||||
adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()],
|
||||
context: Some(typescript_context.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "typescript",
|
||||
adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()],
|
||||
context: Some(typescript_context.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "javascript",
|
||||
adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()],
|
||||
context: Some(typescript_context),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "jsdoc",
|
||||
adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "regex",
|
||||
adapters: vec![],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "yaml",
|
||||
adapters: vec![yaml_lsp_adapter],
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "gitcommit",
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "zed-keybind-context",
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
for registration in built_in_languages {
|
||||
register_language(
|
||||
&languages,
|
||||
registration.name,
|
||||
registration.adapters,
|
||||
registration.context,
|
||||
registration.toolchain,
|
||||
registration.manifest_name,
|
||||
registration.semantic_token_rules,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
// Register globally available language servers.
|
||||
//
|
||||
// This will allow users to add support for a built-in language server (e.g., Tailwind)
|
||||
// for a given language via the `language_servers` setting:
|
||||
//
|
||||
// ```json
|
||||
// {
|
||||
// "languages": {
|
||||
// "My Language": {
|
||||
// "language_servers": ["tailwindcss-language-server", "..."]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
languages.register_available_lsp_adapter(
|
||||
LanguageServerName("tailwindcss-language-server".into()),
|
||||
tailwind_adapter.clone(),
|
||||
);
|
||||
languages.register_available_lsp_adapter(
|
||||
LanguageServerName("tailwindcss-intellisense-css".into()),
|
||||
tailwindcss_adapter,
|
||||
);
|
||||
languages.register_available_lsp_adapter(
|
||||
LanguageServerName("eslint".into()),
|
||||
eslint_adapter.clone(),
|
||||
);
|
||||
languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), vtsls_adapter);
|
||||
languages.register_available_lsp_adapter(
|
||||
LanguageServerName("typescript-language-server".into()),
|
||||
typescript_lsp_adapter,
|
||||
);
|
||||
|
||||
// Register Tailwind for the existing languages that should have it by default.
|
||||
//
|
||||
// This can be driven by the `language_servers` setting once we have a way for
|
||||
// extensions to provide their own default value for that setting.
|
||||
let tailwind_languages = [
|
||||
"Astro",
|
||||
"CSS",
|
||||
"ERB",
|
||||
"HTML+ERB",
|
||||
"HEEx",
|
||||
"HTML",
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"PHP",
|
||||
"Svelte",
|
||||
"TSX",
|
||||
"Vue.js",
|
||||
];
|
||||
|
||||
for language in tailwind_languages {
|
||||
languages.register_lsp_adapter(language.into(), tailwind_adapter.clone());
|
||||
}
|
||||
|
||||
let eslint_languages = ["TSX", "TypeScript", "JavaScript", "Vue.js", "Svelte"];
|
||||
for language in eslint_languages {
|
||||
languages.register_lsp_adapter(language.into(), eslint_adapter.clone());
|
||||
}
|
||||
|
||||
let mut subscription = languages.subscribe();
|
||||
let mut prev_language_settings = languages.language_settings();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
while subscription.next().await.is_some() {
|
||||
let language_settings = languages.language_settings();
|
||||
if language_settings != prev_language_settings {
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |settings, cx| {
|
||||
settings
|
||||
.set_extension_settings(
|
||||
settings::ExtensionsSettingsContent {
|
||||
all_languages: language_settings.clone(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
});
|
||||
});
|
||||
prev_language_settings = language_settings;
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
let manifest_providers: [Arc<dyn ManifestProvider>; 2] = [
|
||||
Arc::from(CargoManifestProvider),
|
||||
Arc::from(PyprojectTomlManifestProvider),
|
||||
];
|
||||
for provider in manifest_providers {
|
||||
project::ManifestProvidersStore::global(cx).register(provider);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LanguageInfo {
|
||||
name: &'static str,
|
||||
adapters: Vec<Arc<dyn LspAdapter>>,
|
||||
context: Option<Arc<dyn ContextProvider>>,
|
||||
toolchain: Option<Arc<dyn ToolchainLister>>,
|
||||
manifest_name: Option<ManifestName>,
|
||||
semantic_token_rules: Option<SemanticTokenRules>,
|
||||
}
|
||||
|
||||
fn register_language(
|
||||
languages: &LanguageRegistry,
|
||||
name: &'static str,
|
||||
adapters: Vec<Arc<dyn LspAdapter>>,
|
||||
context: Option<Arc<dyn ContextProvider>>,
|
||||
toolchain: Option<Arc<dyn ToolchainLister>>,
|
||||
manifest_name: Option<ManifestName>,
|
||||
semantic_token_rules: Option<SemanticTokenRules>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let config = load_config(name);
|
||||
if let Some(rules) = &semantic_token_rules {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.set_language_semantic_token_rules(config.name.0.clone(), rules.clone(), cx);
|
||||
});
|
||||
}
|
||||
for adapter in adapters {
|
||||
languages.register_lsp_adapter(config.name.clone(), adapter);
|
||||
}
|
||||
languages.register_language(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
manifest_name.clone(),
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: grammars::load_queries(name),
|
||||
context_provider: context.clone(),
|
||||
toolchain_provider: toolchain.clone(),
|
||||
manifest_name: manifest_name.clone(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn language(name: &str, grammar: tree_sitter::Language) -> Arc<Language> {
|
||||
Arc::new(
|
||||
Language::new(grammars::load_config(name), Some(grammar))
|
||||
.with_queries(grammars::load_queries(name))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_config(name: &str) -> LanguageConfig {
|
||||
let grammars_loaded = cfg!(any(feature = "load-grammars", test));
|
||||
grammars::load_config_for_feature(name, grammars_loaded)
|
||||
}
|
||||
128
crates/languages/src/package_json.rs
Normal file
128
crates/languages/src/package_json.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use serde_json_lenient::Value;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PackageJson {
|
||||
pub mtime: DateTime<Local>,
|
||||
pub data: PackageJsonData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct PackageJsonData {
|
||||
pub jest_package_path: Option<Arc<Path>>,
|
||||
pub mocha_package_path: Option<Arc<Path>>,
|
||||
pub vitest_package_path: Option<Arc<Path>>,
|
||||
pub jasmine_package_path: Option<Arc<Path>>,
|
||||
pub bun_package_path: Option<Arc<Path>>,
|
||||
pub node_package_path: Option<Arc<Path>>,
|
||||
pub scripts: BTreeSet<(Arc<Path>, String)>,
|
||||
pub package_manager: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl PackageJsonData {
|
||||
pub fn new(path: Arc<Path>, package_json: HashMap<String, Value>) -> Self {
|
||||
let mut scripts = BTreeSet::new();
|
||||
if let Some(Value::Object(package_json_scripts)) = package_json.get("scripts") {
|
||||
scripts.extend(
|
||||
package_json_scripts
|
||||
.keys()
|
||||
.cloned()
|
||||
.map(|name| (path.clone(), name)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut jest_package_path = None;
|
||||
let mut mocha_package_path = None;
|
||||
let mut vitest_package_path = None;
|
||||
let mut jasmine_package_path = None;
|
||||
let mut bun_package_path = None;
|
||||
let mut node_package_path = None;
|
||||
if let Some(Value::Object(dependencies)) = package_json.get("devDependencies") {
|
||||
if dependencies.contains_key("jest") {
|
||||
jest_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dependencies.contains_key("mocha") {
|
||||
mocha_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dependencies.contains_key("vitest") {
|
||||
vitest_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dependencies.contains_key("jasmine") {
|
||||
jasmine_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dependencies.contains_key("@types/bun") {
|
||||
bun_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dependencies.contains_key("@types/node") {
|
||||
node_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
}
|
||||
if let Some(Value::Object(dev_dependencies)) = package_json.get("dependencies") {
|
||||
if dev_dependencies.contains_key("jest") {
|
||||
jest_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dev_dependencies.contains_key("mocha") {
|
||||
mocha_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dev_dependencies.contains_key("vitest") {
|
||||
vitest_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dev_dependencies.contains_key("jasmine") {
|
||||
jasmine_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dev_dependencies.contains_key("@types/bun") {
|
||||
bun_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
if dev_dependencies.contains_key("@types/node") {
|
||||
node_package_path.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let package_manager = package_json
|
||||
.get("packageManager")
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(|value| {
|
||||
if value.starts_with("pnpm") {
|
||||
Some("pnpm")
|
||||
} else if value.starts_with("yarn") {
|
||||
Some("yarn")
|
||||
} else if value.starts_with("npm") {
|
||||
Some("npm")
|
||||
} else if value.starts_with("bun") {
|
||||
Some("bun")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
jest_package_path,
|
||||
mocha_package_path,
|
||||
vitest_package_path,
|
||||
jasmine_package_path,
|
||||
bun_package_path,
|
||||
node_package_path,
|
||||
scripts,
|
||||
package_manager,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: Self) {
|
||||
self.jest_package_path = self.jest_package_path.take().or(other.jest_package_path);
|
||||
self.mocha_package_path = self.mocha_package_path.take().or(other.mocha_package_path);
|
||||
self.vitest_package_path = self
|
||||
.vitest_package_path
|
||||
.take()
|
||||
.or(other.vitest_package_path);
|
||||
self.jasmine_package_path = self
|
||||
.jasmine_package_path
|
||||
.take()
|
||||
.or(other.jasmine_package_path);
|
||||
self.bun_package_path = self.bun_package_path.take().or(other.bun_package_path);
|
||||
self.node_package_path = self.node_package_path.take().or(other.node_package_path);
|
||||
self.scripts.extend(other.scripts);
|
||||
self.package_manager = self.package_manager.or(other.package_manager);
|
||||
}
|
||||
}
|
||||
3243
crates/languages/src/python.rs
Normal file
3243
crates/languages/src/python.rs
Normal file
File diff suppressed because it is too large
Load Diff
2239
crates/languages/src/rust.rs
Normal file
2239
crates/languages/src/rust.rs
Normal file
File diff suppressed because it is too large
Load Diff
237
crates/languages/src/tailwind.rs
Normal file
237
crates/languages/src/tailwind.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use gpui::AsyncApp;
|
||||
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName, Uri};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::lsp_store::language_server_settings;
|
||||
use semver::Version;
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, maybe};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/@tailwindcss/language-server/bin/tailwindcss-language-server";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
pub struct TailwindLspAdapter {
|
||||
node: NodeRuntime,
|
||||
}
|
||||
|
||||
impl TailwindLspAdapter {
|
||||
const SERVER_NAME: LanguageServerName =
|
||||
LanguageServerName::new_static("tailwindcss-language-server");
|
||||
const PACKAGE_NAME: &str = "@tailwindcss/language-server";
|
||||
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
TailwindLspAdapter { node }
|
||||
}
|
||||
}
|
||||
|
||||
impl LspInstaller for TailwindLspAdapter {
|
||||
type BinaryVersion = Version;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Self::BinaryVersion> {
|
||||
self.node
|
||||
.npm_package_latest_version(Self::PACKAGE_NAME)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
let env = delegate.shell_env().await;
|
||||
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
env: Some(env),
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let latest_version = latest_version.to_string();
|
||||
|
||||
node.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn check_if_version_installed(
|
||||
&self,
|
||||
version: &Self::BinaryVersion,
|
||||
container_dir: &PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
let version = version.clone();
|
||||
let container_dir = container_dir.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&version),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for TailwindLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
Self::SERVER_NAME
|
||||
}
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Option<Toolchain>,
|
||||
_: Option<Uri>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let mut tailwind_user_settings = cx.update(|cx| {
|
||||
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
|
||||
.and_then(|s| s.settings.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
if tailwind_user_settings.get("emmetCompletions").is_none() {
|
||||
tailwind_user_settings["emmetCompletions"] = Value::Bool(true);
|
||||
}
|
||||
|
||||
if tailwind_user_settings.get("includeLanguages").is_none() {
|
||||
tailwind_user_settings["includeLanguages"] = json!({
|
||||
"html": "html",
|
||||
"css": "css",
|
||||
"javascript": "javascript",
|
||||
"typescript": "typescript",
|
||||
"typescriptreact": "typescriptreact",
|
||||
});
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"tailwindCSS": tailwind_user_settings
|
||||
}))
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<LanguageName, String> {
|
||||
HashMap::from_iter([
|
||||
(LanguageName::new_static("Astro"), "astro".to_string()),
|
||||
(LanguageName::new_static("HTML"), "html".to_string()),
|
||||
(LanguageName::new_static("Gleam"), "html".to_string()),
|
||||
(LanguageName::new_static("CSS"), "css".to_string()),
|
||||
(
|
||||
LanguageName::new_static("JavaScript"),
|
||||
"javascript".to_string(),
|
||||
),
|
||||
(
|
||||
LanguageName::new_static("TypeScript"),
|
||||
"typescript".to_string(),
|
||||
),
|
||||
(
|
||||
LanguageName::new_static("TSX"),
|
||||
"typescriptreact".to_string(),
|
||||
),
|
||||
(LanguageName::new_static("Svelte"), "svelte".to_string()),
|
||||
(LanguageName::new_static("Elixir"), "elixir".to_string()),
|
||||
(LanguageName::new_static("HEEx"), "heex".to_string()),
|
||||
(LanguageName::new_static("ERB"), "erb".to_string()),
|
||||
(LanguageName::new_static("HTML+ERB"), "erb".to_string()),
|
||||
(LanguageName::new_static("PHP"), "php".to_string()),
|
||||
(LanguageName::new_static("Vue.js"), "vue".to_string()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
anyhow::ensure!(
|
||||
server_path.exists(),
|
||||
"missing executable in directory {server_path:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
204
crates/languages/src/tailwindcss.rs
Normal file
204
crates/languages/src/tailwindcss.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::AsyncApp;
|
||||
use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName, Uri};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::lsp_store::language_server_settings;
|
||||
use semver::Version;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, maybe, merge_json_value_into};
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/@tailwindcss/language-server/bin/css-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
pub struct TailwindCssLspAdapter {
|
||||
node: NodeRuntime,
|
||||
}
|
||||
|
||||
// Implements the LSP adapter for the Tailwind CSS LSP fork: https://github.com/zed-industries/zed/pull/39517#issuecomment-3368206678
|
||||
impl TailwindCssLspAdapter {
|
||||
const SERVER_NAME: LanguageServerName =
|
||||
LanguageServerName::new_static("tailwindcss-intellisense-css");
|
||||
const PACKAGE_NAME: &str = "@tailwindcss/language-server";
|
||||
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
TailwindCssLspAdapter { node }
|
||||
}
|
||||
}
|
||||
|
||||
impl LspInstaller for TailwindCssLspAdapter {
|
||||
type BinaryVersion = Version;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Self::BinaryVersion> {
|
||||
self.node
|
||||
.npm_package_latest_version(Self::PACKAGE_NAME)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
let env = delegate.shell_env().await;
|
||||
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
env: Some(env),
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let latest_version = latest_version.to_string();
|
||||
|
||||
node.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn check_if_version_installed(
|
||||
&self,
|
||||
version: &Self::BinaryVersion,
|
||||
container_dir: &PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
let version = version.clone();
|
||||
let container_dir = container_dir.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&version),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for TailwindCssLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
Self::SERVER_NAME
|
||||
}
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true
|
||||
})))
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Option<Toolchain>,
|
||||
_: Option<Uri>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut default_config = json!({
|
||||
"css": {
|
||||
"lint": {}
|
||||
},
|
||||
"less": {
|
||||
"lint": {}
|
||||
},
|
||||
"scss": {
|
||||
"lint": {}
|
||||
}
|
||||
});
|
||||
|
||||
let project_options = cx.update(|cx| {
|
||||
language_server_settings(delegate.as_ref(), &self.name(), cx)
|
||||
.and_then(|s| s.settings.clone())
|
||||
});
|
||||
|
||||
if let Some(override_options) = project_options {
|
||||
merge_json_value_into(override_options, &mut default_config);
|
||||
}
|
||||
|
||||
Ok(default_config)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
anyhow::ensure!(
|
||||
server_path.exists(),
|
||||
"missing executable in directory {server_path:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
1693
crates/languages/src/typescript.rs
Normal file
1693
crates/languages/src/typescript.rs
Normal file
File diff suppressed because it is too large
Load Diff
431
crates/languages/src/vtsls.rs
Normal file
431
crates/languages/src/vtsls.rs
Normal file
@@ -0,0 +1,431 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use gpui::AsyncApp;
|
||||
use language::{
|
||||
LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, PromptResponseContext, Toolchain,
|
||||
};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use util::{ResultExt, maybe, merge_json_value_into};
|
||||
|
||||
const ACTION_ALWAYS: &str = "Always";
|
||||
const ACTION_NEVER: &str = "Never";
|
||||
const UPDATE_IMPORTS_MESSAGE_PATTERN: &str = "Update imports for";
|
||||
const VTSLS_SERVER_NAME: &str = "vtsls";
|
||||
|
||||
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
pub struct VtslsLspAdapter {
|
||||
node: NodeRuntime,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl VtslsLspAdapter {
|
||||
const PACKAGE_NAME: &'static str = "@vtsls/language-server";
|
||||
const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
|
||||
|
||||
const TYPESCRIPT_PACKAGE_NAME: &'static str = "typescript";
|
||||
const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib";
|
||||
const TYPESCRIPT_YARN_TSDK_PATH: &'static str = ".yarn/sdks/typescript/lib";
|
||||
|
||||
pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
|
||||
VtslsLspAdapter { node, fs }
|
||||
}
|
||||
|
||||
async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
|
||||
let yarn_sdk = adapter
|
||||
.worktree_root_path()
|
||||
.join(Self::TYPESCRIPT_YARN_TSDK_PATH);
|
||||
|
||||
let tsdk_path = if self.fs.is_dir(&yarn_sdk).await {
|
||||
Self::TYPESCRIPT_YARN_TSDK_PATH
|
||||
} else {
|
||||
Self::TYPESCRIPT_TSDK_PATH
|
||||
};
|
||||
|
||||
if self
|
||||
.fs
|
||||
.is_dir(&adapter.worktree_root_path().join(tsdk_path))
|
||||
.await
|
||||
{
|
||||
Some(tsdk_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enhance_diagnostic_message(message: &str) -> Option<String> {
|
||||
static SINGLE_WORD_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"'([^\s']*)'").expect("Failed to create REGEX"));
|
||||
|
||||
static MULTI_WORD_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"'([^']+\s+[^']*)'").expect("Failed to create REGEX"));
|
||||
|
||||
let first = SINGLE_WORD_REGEX.replace_all(message, "`$1`").to_string();
|
||||
let second = MULTI_WORD_REGEX
|
||||
.replace_all(&first, "\n```typescript\n$1\n```\n")
|
||||
.to_string();
|
||||
Some(second)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypeScriptVersions {
|
||||
typescript_version: Version,
|
||||
server_version: Version,
|
||||
}
|
||||
|
||||
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
|
||||
|
||||
impl LspInstaller for VtslsLspAdapter {
|
||||
type BinaryVersion = TypeScriptVersions;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Self::BinaryVersion> {
|
||||
Ok(TypeScriptVersions {
|
||||
typescript_version: self.node.npm_package_latest_version("typescript").await?,
|
||||
server_version: self
|
||||
.node
|
||||
.npm_package_latest_version("@vtsls/language-server")
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let env = delegate.shell_env().await;
|
||||
let path = delegate.which(SERVER_NAME.as_ref()).await?;
|
||||
Some(LanguageServerBinary {
|
||||
path: path.clone(),
|
||||
arguments: typescript_server_binary_arguments(&path),
|
||||
env: Some(env),
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
|
||||
let typescript_version = latest_version.typescript_version.to_string();
|
||||
let server_version = latest_version.server_version.to_string();
|
||||
|
||||
let mut packages_to_install = Vec::new();
|
||||
|
||||
if node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&latest_version.server_version),
|
||||
)
|
||||
.await
|
||||
{
|
||||
packages_to_install.push((Self::PACKAGE_NAME, server_version.as_str()));
|
||||
}
|
||||
|
||||
if node
|
||||
.should_install_npm_package(
|
||||
Self::TYPESCRIPT_PACKAGE_NAME,
|
||||
&container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&latest_version.typescript_version),
|
||||
)
|
||||
.await
|
||||
{
|
||||
packages_to_install
|
||||
.push((Self::TYPESCRIPT_PACKAGE_NAME, typescript_version.as_str()));
|
||||
}
|
||||
|
||||
node.npm_install_packages(&container_dir, &packages_to_install)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: typescript_server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_ts_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for VtslsLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
SERVER_NAME
|
||||
}
|
||||
|
||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
Some(vec![
|
||||
CodeActionKind::QUICKFIX,
|
||||
CodeActionKind::REFACTOR,
|
||||
CodeActionKind::REFACTOR_EXTRACT,
|
||||
CodeActionKind::SOURCE,
|
||||
])
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
use lsp::CompletionItemKind as Kind;
|
||||
let label_len = item.label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
Kind::CLASS | Kind::INTERFACE | Kind::ENUM => 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"),
|
||||
Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let text = if let Some(description) = item
|
||||
.label_details
|
||||
.as_ref()
|
||||
.and_then(|label_details| label_details.description.as_ref())
|
||||
{
|
||||
format!("{} {}", item.label, description)
|
||||
} else if let Some(detail) = &item.detail {
|
||||
format!("{} {}", item.label, detail)
|
||||
} else {
|
||||
item.label.clone()
|
||||
};
|
||||
Some(language::CodeLabel::filtered(
|
||||
text,
|
||||
label_len,
|
||||
item.filter_text.as_deref(),
|
||||
vec![(0..label_len, highlight_id)],
|
||||
))
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Option<Toolchain>,
|
||||
_: Option<Uri>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let tsdk_path = self.tsdk_path(delegate).await;
|
||||
let config = serde_json::json!({
|
||||
"tsdk": tsdk_path,
|
||||
"suggest": {
|
||||
"completeFunctionCalls": true
|
||||
},
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": false
|
||||
},
|
||||
"parameterTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"variableTypes": {
|
||||
"enabled": true,
|
||||
"suppressWhenTypeMatchesName": false
|
||||
},
|
||||
"propertyDeclarationTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"functionLikeReturnTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"enumMemberValues": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"implementationsCodeLens": {
|
||||
"enabled": true,
|
||||
"showOnAllClassMethods": true,
|
||||
"showOnInterfaceMethods": true
|
||||
},
|
||||
"referencesCodeLens": {
|
||||
"enabled": true,
|
||||
"showOnAllFunctions": true
|
||||
},
|
||||
"tsserver": {
|
||||
"maxTsServerMemory": 8092
|
||||
},
|
||||
});
|
||||
|
||||
let mut default_workspace_configuration = serde_json::json!({
|
||||
"typescript": config,
|
||||
"javascript": config,
|
||||
"vtsls": {
|
||||
"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
},
|
||||
"autoUseWorkspaceTsdk": true
|
||||
}
|
||||
});
|
||||
|
||||
let override_options = cx.update(|cx| {
|
||||
language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
|
||||
.and_then(|s| s.settings.clone())
|
||||
});
|
||||
|
||||
if let Some(override_options) = override_options {
|
||||
merge_json_value_into(override_options, &mut default_workspace_configuration)
|
||||
}
|
||||
|
||||
Ok(default_workspace_configuration)
|
||||
}
|
||||
|
||||
fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message)
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<LanguageName, String> {
|
||||
HashMap::from_iter([
|
||||
(LanguageName::new_static("TypeScript"), "typescript".into()),
|
||||
(LanguageName::new_static("JavaScript"), "javascript".into()),
|
||||
(LanguageName::new_static("TSX"), "typescriptreact".into()),
|
||||
])
|
||||
}
|
||||
|
||||
fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
|
||||
let selected_title = context.selected_action.title.as_str();
|
||||
let is_preference_response =
|
||||
selected_title == ACTION_ALWAYS || selected_title == ACTION_NEVER;
|
||||
if !is_preference_response {
|
||||
return;
|
||||
}
|
||||
|
||||
if context.message.contains(UPDATE_IMPORTS_MESSAGE_PATTERN) {
|
||||
let setting_value = match selected_title {
|
||||
ACTION_ALWAYS => "always",
|
||||
ACTION_NEVER => "never",
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let settings = json!({
|
||||
"typescript": {
|
||||
"updateImportsOnFileMove": {
|
||||
"enabled": setting_value
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"updateImportsOnFileMove": {
|
||||
"enabled": setting_value
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = cx.update(|cx| {
|
||||
update_settings_file(self.fs.clone(), cx, move |content, _| {
|
||||
let lsp_settings = content
|
||||
.project
|
||||
.lsp
|
||||
.0
|
||||
.entry(VTSLS_SERVER_NAME.into())
|
||||
.or_default();
|
||||
|
||||
if let Some(existing) = &mut lsp_settings.settings {
|
||||
merge_json_value_into(settings, existing);
|
||||
} else {
|
||||
lsp_settings.settings = Some(settings);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_ts_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
|
||||
anyhow::ensure!(
|
||||
server_path.exists(),
|
||||
"missing executable in directory {container_dir:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: typescript_server_binary_arguments(&server_path),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::vtsls::VtslsLspAdapter;
|
||||
|
||||
#[test]
|
||||
fn test_diagnostic_message_to_markdown() {
|
||||
// Leaves simple messages unchanged
|
||||
let message = "The expected type comes from the return type of this signature.";
|
||||
|
||||
let expected = "The expected type comes from the return type of this signature.";
|
||||
|
||||
assert_eq!(
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
|
||||
expected
|
||||
);
|
||||
|
||||
// Parses both multi-word and single-word correctly
|
||||
let message = "Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
|
||||
|
||||
let expected = "Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
|
||||
|
||||
assert_eq!(
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
|
||||
expected
|
||||
);
|
||||
|
||||
// Parses multi-and-single word in any order, and ignores existing newlines
|
||||
let message = "Type '() => { foo: string; bar: string; }' is not assignable to type 'GetUserFunction'.\n Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
|
||||
|
||||
let expected = "Type \n```typescript\n() => { foo: string; bar: string; }\n```\n is not assignable to type `GetUserFunction`.\n Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
|
||||
|
||||
assert_eq!(
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
228
crates/languages/src/yaml.rs
Normal file
228
crates/languages/src/yaml.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::AsyncApp;
|
||||
use language::{
|
||||
LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, language_settings::AllLanguageSettings,
|
||||
};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName, Uri};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::lsp_store::language_server_settings;
|
||||
use semver::Version;
|
||||
use serde_json::Value;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, maybe, merge_json_value_into, rel_path::RelPath};
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/yaml-language-server/bin/yaml-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
pub struct YamlLspAdapter {
|
||||
node: NodeRuntime,
|
||||
}
|
||||
|
||||
impl YamlLspAdapter {
|
||||
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("yaml-language-server");
|
||||
const PACKAGE_NAME: &str = "yaml-language-server";
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
YamlLspAdapter { node }
|
||||
}
|
||||
}
|
||||
|
||||
impl LspInstaller for YamlLspAdapter {
|
||||
type BinaryVersion = Version;
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: bool,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<Self::BinaryVersion> {
|
||||
self.node
|
||||
.npm_package_latest_version("yaml-language-server")
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
let env = delegate.shell_env().await;
|
||||
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
env: Some(env),
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
container_dir: PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let latest_version = latest_version.to_string();
|
||||
|
||||
node.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn check_if_version_installed(
|
||||
&self,
|
||||
version: &Self::BinaryVersion,
|
||||
container_dir: &PathBuf,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
|
||||
let node = self.node.clone();
|
||||
let version = version.clone();
|
||||
let container_dir = container_dir.clone();
|
||||
|
||||
async move {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
VersionStrategy::Latest(&version),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for YamlLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
Self::SERVER_NAME
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Option<Toolchain>,
|
||||
_: Option<Uri>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let location = SettingsLocation {
|
||||
worktree_id: delegate.worktree_id(),
|
||||
path: RelPath::empty(),
|
||||
};
|
||||
|
||||
let tab_size = cx.update(|cx| {
|
||||
AllLanguageSettings::get(Some(location), cx)
|
||||
.language(Some(location), Some(&"YAML".into()), cx)
|
||||
.tab_size
|
||||
});
|
||||
|
||||
let mut options = serde_json::json!({
|
||||
"[yaml]": {"editor.tabSize": tab_size},
|
||||
"yaml": {"format": {"enable": true}}
|
||||
});
|
||||
|
||||
let project_options = cx.update(|cx| {
|
||||
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
|
||||
.and_then(|s| worktree_root(delegate, s.settings.clone()))
|
||||
});
|
||||
if let Some(override_options) = project_options {
|
||||
merge_json_value_into(override_options, &mut options);
|
||||
}
|
||||
Ok(options)
|
||||
}
|
||||
}
|
||||
|
||||
fn worktree_root(delegate: &Arc<dyn LspAdapterDelegate>, settings: Option<Value>) -> Option<Value> {
|
||||
let Some(Value::Object(mut settings_map)) = settings else {
|
||||
return settings;
|
||||
};
|
||||
|
||||
let Some(Value::Object(yaml_config)) = settings_map.get_mut("yaml") else {
|
||||
return Some(Value::Object(settings_map));
|
||||
};
|
||||
|
||||
let Some(Value::Object(schemas)) = yaml_config.remove("schemas") else {
|
||||
return Some(Value::Object(settings_map));
|
||||
};
|
||||
|
||||
let schemas = schemas
|
||||
.into_iter()
|
||||
.map(|(url, v)| {
|
||||
if !url.starts_with(".") && !url.starts_with("~") {
|
||||
(url, v)
|
||||
} else {
|
||||
let resolved_url = delegate
|
||||
.resolve_relative_path(url.into())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
(resolved_url, v)
|
||||
}
|
||||
})
|
||||
.collect::<serde_json::Map<String, Value>>();
|
||||
|
||||
yaml_config.insert("schemas".into(), Value::Object(schemas));
|
||||
Some(Value::Object(settings_map))
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
anyhow::ensure!(
|
||||
server_path.exists(),
|
||||
"missing executable in directory {server_path:?}"
|
||||
);
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
Reference in New Issue
Block a user