logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View 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

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View 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
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
View 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
View 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
View 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),
]
);
}
}

File diff suppressed because it is too large Load Diff

1661
crates/languages/src/go.rs Normal file

File diff suppressed because it is too large Load Diff

View 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
View 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)
}

View 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);
}
}

File diff suppressed because it is too large Load Diff

2239
crates/languages/src/rust.rs Normal file

File diff suppressed because it is too large Load Diff

View 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()
}

View 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()
}

File diff suppressed because it is too large Load Diff

View 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
);
}
}

View 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()
}