feat: expand Galaxy agent and remote tooling
Add Wormhole remote helpers, provider and agent improvements, filesystem diagnostics, model metadata support, and schema-aware settings IntelliSense.
This commit is contained in:
@@ -35,6 +35,7 @@ pub enum LanguageId {
|
||||
JavaScriptReact,
|
||||
C,
|
||||
Cpp,
|
||||
Toml,
|
||||
}
|
||||
|
||||
impl LanguageId {
|
||||
@@ -55,6 +56,7 @@ impl LanguageId {
|
||||
// compile_commands.json is present, clangd will use the correct language
|
||||
// regardless of the languageId we send.
|
||||
"h" | "H" | "hh" | "hpp" | "hxx" => Some(Self::Cpp),
|
||||
"toml" => Some(Self::Toml),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -72,6 +74,7 @@ impl LanguageId {
|
||||
LanguageId::JavaScriptReact => "javascriptreact",
|
||||
LanguageId::C => "c",
|
||||
LanguageId::Cpp => "cpp",
|
||||
LanguageId::Toml => "toml",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +89,7 @@ impl LanguageId {
|
||||
| LanguageId::JavaScript
|
||||
| LanguageId::JavaScriptReact => LSPServerType::TypeScriptLanguageServer,
|
||||
LanguageId::C | LanguageId::Cpp => LSPServerType::Clangd,
|
||||
LanguageId::Toml => LSPServerType::Tombi,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +109,8 @@ pub struct LspServerConfig {
|
||||
client: Arc<http_client::Client>,
|
||||
/// Optional path relative to the LSP log namespace for server stderr output.
|
||||
log_relative_path: Option<PathBuf>,
|
||||
/// Notifications sent immediately after the server completes LSP initialization.
|
||||
post_initialize_notifications: Vec<(String, serde_json::Value)>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for LspServerConfig {
|
||||
@@ -115,6 +121,10 @@ impl fmt::Debug for LspServerConfig {
|
||||
.field("path_env_var", &self.path_env_var)
|
||||
.field("client_name", &self.client_name)
|
||||
.field("log_relative_path", &self.log_relative_path)
|
||||
.field(
|
||||
"post_initialize_notifications",
|
||||
&self.post_initialize_notifications,
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -134,6 +144,7 @@ impl LspServerConfig {
|
||||
client_name,
|
||||
client,
|
||||
log_relative_path: None,
|
||||
post_initialize_notifications: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +158,21 @@ impl LspServerConfig {
|
||||
self.log_relative_path.as_ref()
|
||||
}
|
||||
|
||||
/// Adds a custom notification to send after the language server initializes.
|
||||
pub fn with_post_initialize_notification(
|
||||
mut self,
|
||||
method: impl Into<String>,
|
||||
params: serde_json::Value,
|
||||
) -> Self {
|
||||
self.post_initialize_notifications
|
||||
.push((method.into(), params));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn post_initialize_notifications(&self) -> &[(String, serde_json::Value)] {
|
||||
&self.post_initialize_notifications
|
||||
}
|
||||
|
||||
/// Returns the initial workspace path.
|
||||
pub fn initial_workspace(&self) -> &Path {
|
||||
&self.initial_workspace
|
||||
|
||||
@@ -145,6 +145,8 @@ where
|
||||
pub enum AssetKind {
|
||||
/// A gzip-compressed file (e.g., `rust-analyzer-aarch64-apple-darwin.gz`)
|
||||
Gz,
|
||||
/// A gzip-compressed tar archive (e.g., `tombi-cli-1.4.1-aarch64-apple-darwin.tar.gz`)
|
||||
TarGz,
|
||||
/// A zip archive (e.g., `rust-analyzer-x86_64-pc-windows-msvc.zip`)
|
||||
Zip,
|
||||
}
|
||||
@@ -153,7 +155,9 @@ pub enum AssetKind {
|
||||
impl AssetKind {
|
||||
/// Determines the asset kind from a file name based on its extension.
|
||||
pub fn from_filename(filename: &str) -> Option<Self> {
|
||||
if filename.ends_with(".gz") && !filename.ends_with(".tar.gz") {
|
||||
if filename.ends_with(".tar.gz") {
|
||||
Some(AssetKind::TarGz)
|
||||
} else if filename.ends_with(".gz") {
|
||||
Some(AssetKind::Gz)
|
||||
} else if filename.ends_with(".zip") {
|
||||
Some(AssetKind::Zip)
|
||||
@@ -263,6 +267,9 @@ pub async fn install_from_github(
|
||||
let binary_path = install_dir.join(&binary_name);
|
||||
node_runtime::extract_gz(&bytes, &binary_path).await?;
|
||||
}
|
||||
AssetKind::TarGz => {
|
||||
node_runtime::extract_tar_gz(&bytes, &install_dir)?;
|
||||
}
|
||||
AssetKind::Zip => {
|
||||
if binary_finder.is_some() {
|
||||
// Extract the full archive when using a custom binary finder
|
||||
|
||||
@@ -78,6 +78,7 @@ pub async fn spawn_lsp_service(
|
||||
logger: Option<SimpleLogger>,
|
||||
) -> Result<LspServiceInitializationResult> {
|
||||
let workspace_root = config.initial_workspace().to_path_buf();
|
||||
let post_initialize_notifications = config.post_initialize_notifications().to_vec();
|
||||
|
||||
let resolved = match config.command_and_params().await {
|
||||
Ok(resolved) => resolved,
|
||||
@@ -119,6 +120,12 @@ pub async fn spawn_lsp_service(
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
for (method, params) in post_initialize_notifications {
|
||||
service
|
||||
.send_custom_notification_and_wait(method, params)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(LspServiceInitializationResult {
|
||||
service,
|
||||
channel: notify_rx,
|
||||
|
||||
+37
-1
@@ -16,7 +16,8 @@ use jsonrpc::ServerNotificationEvent;
|
||||
use lsp_types::notification::{self, Notification};
|
||||
use lsp_types::{
|
||||
CompletionItem, CompletionTriggerKind, FormattingOptions, NumberOrString, ProgressParams,
|
||||
ProgressParamsValue, PublishDiagnosticsParams, Range as LspRange, WorkDoneProgress,
|
||||
ProgressParamsValue, PublishDiagnosticsParams, Range as LspRange, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, WorkDoneProgress,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use simple_logger::manager::LogManager;
|
||||
@@ -531,6 +532,41 @@ impl LspServerModel {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the server requires each document change to contain the full document.
|
||||
pub fn requires_full_document_sync(&self) -> bool {
|
||||
let Ok(service) = self.service() else {
|
||||
return false;
|
||||
};
|
||||
let Some(sync) = service
|
||||
.server_capabilities()
|
||||
.as_ref()
|
||||
.and_then(|capabilities| capabilities.text_document_sync.as_ref())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match sync {
|
||||
TextDocumentSyncCapability::Kind(kind) => *kind == TextDocumentSyncKind::FULL,
|
||||
TextDocumentSyncCapability::Options(options) => {
|
||||
options.change == Some(TextDocumentSyncKind::FULL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the server can resolve additional details for completion items.
|
||||
pub fn supports_completion_resolve(&self) -> bool {
|
||||
let Ok(service) = self.service() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
service
|
||||
.server_capabilities()
|
||||
.as_ref()
|
||||
.and_then(|capabilities| capabilities.completion_provider.as_ref())
|
||||
.and_then(|options| options.resolve_provider)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn did_change_watched_files(&self, events: Vec<WatchedFileChangeEvent>) -> Result<()> {
|
||||
let service = self.service()?;
|
||||
service.workspace_watched_files_changed(events)
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod clangd;
|
||||
pub mod go;
|
||||
pub mod pyright;
|
||||
pub mod rust;
|
||||
pub mod tombi;
|
||||
pub mod typescript_language_server;
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::install::{
|
||||
fetch_latest_metadata_from_github_dynamic_asset, install_from_github, AssetKind,
|
||||
};
|
||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||
use crate::CommandBuilder;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
const SERVER_NAME: &str = "tombi";
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||
pub struct TombiCandidate {
|
||||
client: Arc<http_client::Client>,
|
||||
}
|
||||
|
||||
impl TombiCandidate {
|
||||
pub fn new(client: Arc<http_client::Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub async fn find_installed_binary_in_data_dir() -> Option<PathBuf> {
|
||||
let install_root = galaxy_core::paths::data_dir().join(SERVER_NAME);
|
||||
let entries = std::fs::read_dir(install_root).ok()?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let version_dir = entry.path();
|
||||
if !version_dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(binary_path) = find_binary(&version_dir) else {
|
||||
continue;
|
||||
};
|
||||
if binary_is_working(&binary_path).await {
|
||||
return Some(binary_path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn asset_target() -> anyhow::Result<&'static str> {
|
||||
match (std::env::consts::OS, std::env::consts::ARCH) {
|
||||
("macos", "aarch64") => Ok("aarch64-apple-darwin"),
|
||||
("macos", "x86_64") => Ok("x86_64-apple-darwin"),
|
||||
("linux", "aarch64") => Ok("aarch64-unknown-linux-musl"),
|
||||
("linux", "x86_64") => Ok("x86_64-unknown-linux-musl"),
|
||||
("windows", "aarch64") => Ok("aarch64-pc-windows-msvc"),
|
||||
("windows", "x86_64") => Ok("x86_64-pc-windows-msvc"),
|
||||
(os, arch) => anyhow::bail!("Unsupported platform for Tombi: {os}/{arch}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn asset_name(tag: &str) -> anyhow::Result<String> {
|
||||
let version = tag.strip_prefix('v').unwrap_or(tag);
|
||||
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
|
||||
Ok(format!(
|
||||
"tombi-cli-{version}-{}.{extension}",
|
||||
asset_target()?
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn find_binary(install_dir: &Path) -> Option<PathBuf> {
|
||||
let binary_name = if cfg!(windows) {
|
||||
"tombi.exe"
|
||||
} else {
|
||||
SERVER_NAME
|
||||
};
|
||||
let direct_path = install_dir.join(binary_name);
|
||||
if direct_path.is_file() {
|
||||
return Some(direct_path);
|
||||
}
|
||||
|
||||
std::fs::read_dir(install_dir)
|
||||
.ok()?
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
path.is_dir().then(|| path.join(binary_name))
|
||||
})
|
||||
.find(|path| path.is_file())
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
async fn binary_is_working(binary_path: &Path) -> bool {
|
||||
if !binary_path.is_file() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut command = command::r#async::Command::new(binary_path);
|
||||
command.arg("--version");
|
||||
command
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "local_fs")]
|
||||
impl LanguageServerCandidate for TombiCandidate {
|
||||
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
let Ok(entries) = std::fs::read_dir(path) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
entries.flatten().any(|entry| {
|
||||
let file_path = entry.path();
|
||||
file_path.is_file()
|
||||
&& file_path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
== Some("toml")
|
||||
})
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
Self::find_installed_binary_in_data_dir().await.is_some()
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool {
|
||||
executor
|
||||
.command(SERVER_NAME)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
let asset_name = asset_name(&metadata.version)?;
|
||||
let asset_kind = AssetKind::from_filename(&asset_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Unsupported archive format for asset: {asset_name}"))?;
|
||||
let binary_path = install_from_github(
|
||||
&self.client,
|
||||
&metadata,
|
||||
SERVER_NAME,
|
||||
asset_kind,
|
||||
Some(find_binary),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !binary_is_working(&binary_path).await {
|
||||
anyhow::bail!(
|
||||
"Installed Tombi binary at {} failed its version check",
|
||||
binary_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
let target = asset_target()?;
|
||||
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
|
||||
fetch_latest_metadata_from_github_dynamic_asset(
|
||||
&self.client,
|
||||
"tombi-toml",
|
||||
"tombi",
|
||||
|tag| {
|
||||
let version = tag.strip_prefix('v').unwrap_or(tag);
|
||||
format!("tombi-cli-{version}-{target}.{extension}")
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
impl LanguageServerCandidate for TombiCandidate {
|
||||
async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
_metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
anyhow::bail!("Tombi installation is unavailable without local filesystem support")
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
anyhow::bail!("Tombi metadata is unavailable without local filesystem support")
|
||||
}
|
||||
}
|
||||
@@ -170,7 +170,8 @@ impl LspService {
|
||||
let response = self.send_request::<request::Initialize>(params).await?;
|
||||
self.server_capabilities = Some(response.capabilities);
|
||||
|
||||
self.send_notification::<notification::Initialized>(InitializedParams {})?;
|
||||
self.send_notification_and_wait::<notification::Initialized>(InitializedParams {})
|
||||
.await?;
|
||||
self.subscribe::<notification::Progress>().await;
|
||||
self.subscribe::<notification::PublishDiagnostics>().await;
|
||||
|
||||
@@ -297,6 +298,23 @@ impl LspService {
|
||||
.send_notification(N::METHOD.to_string(), params)
|
||||
}
|
||||
|
||||
async fn send_notification_and_wait<N: Notification>(&self, params: N::Params) -> Result<()> {
|
||||
let params = serde_json::to_value(params)?;
|
||||
self.jsonrpc_service
|
||||
.send_notification_and_wait(N::METHOD.to_string(), params)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn send_custom_notification_and_wait(
|
||||
&self,
|
||||
method: impl Into<String>,
|
||||
params: Value,
|
||||
) -> Result<()> {
|
||||
self.jsonrpc_service
|
||||
.send_notification_and_wait(method.into(), params)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_request<R: Request>(&self, params: R::Params) -> Result<R::Result> {
|
||||
let params = serde_json::to_value(params)?;
|
||||
let request = self.send_request_internal(R::METHOD.to_string(), params);
|
||||
@@ -538,7 +556,8 @@ impl<'a> TextDocumentService<'a> {
|
||||
};
|
||||
|
||||
self.service
|
||||
.send_notification::<notification::DidOpenTextDocument>(did_open_params)
|
||||
.send_notification_and_wait::<notification::DidOpenTextDocument>(did_open_params)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn did_close(&self, path: &Path) -> Result<()> {
|
||||
@@ -560,7 +579,8 @@ impl<'a> TextDocumentService<'a> {
|
||||
};
|
||||
|
||||
self.service
|
||||
.send_notification::<notification::DidCloseTextDocument>(did_close_params)
|
||||
.send_notification_and_wait::<notification::DidCloseTextDocument>(did_close_params)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn did_change(
|
||||
@@ -592,7 +612,8 @@ impl<'a> TextDocumentService<'a> {
|
||||
};
|
||||
|
||||
self.service
|
||||
.send_notification::<notification::DidChangeTextDocument>(did_change_params)
|
||||
.send_notification_and_wait::<notification::DidChangeTextDocument>(did_change_params)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn definition(
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::servers::clangd::ClangdCandidate;
|
||||
use crate::servers::go::GoPlsCandidate;
|
||||
use crate::servers::pyright::PyrightCandidate;
|
||||
use crate::servers::rust::RustAnalyzerCandidate;
|
||||
use crate::servers::tombi::TombiCandidate;
|
||||
use crate::servers::typescript_language_server::TypeScriptLanguageServerCandidate;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::CommandBuilder;
|
||||
@@ -45,6 +46,7 @@ pub enum LSPServerType {
|
||||
Pyright,
|
||||
TypeScriptLanguageServer,
|
||||
Clangd,
|
||||
Tombi,
|
||||
}
|
||||
|
||||
/// Provides server-specific configuration for each LSP server type.
|
||||
@@ -112,6 +114,12 @@ impl LSPServerType {
|
||||
binary_path: path,
|
||||
prepend_args: vec![],
|
||||
}),
|
||||
LSPServerType::Tombi => TombiCandidate::find_installed_binary_in_data_dir()
|
||||
.await
|
||||
.map(|path| CustomBinaryConfig {
|
||||
binary_path: path,
|
||||
prepend_args: vec![],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +143,7 @@ impl LSPServerType {
|
||||
LSPServerType::Pyright => "pyright-langserver",
|
||||
LSPServerType::TypeScriptLanguageServer => "typescript-language-server",
|
||||
LSPServerType::Clangd => "clangd",
|
||||
LSPServerType::Tombi => "tombi",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +153,7 @@ impl LSPServerType {
|
||||
match self {
|
||||
LSPServerType::RustAnalyzer | LSPServerType::GoPls | LSPServerType::Clangd => vec![],
|
||||
LSPServerType::Pyright | LSPServerType::TypeScriptLanguageServer => vec!["--stdio"],
|
||||
LSPServerType::Tombi => vec!["lsp", "--offline"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +167,7 @@ impl LSPServerType {
|
||||
LSPServerType::Pyright => vec!["--stdio"],
|
||||
LSPServerType::TypeScriptLanguageServer => vec!["--stdio"],
|
||||
LSPServerType::Clangd => vec![],
|
||||
LSPServerType::Tombi => vec!["lsp", "--offline"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +186,7 @@ impl LSPServerType {
|
||||
]
|
||||
}
|
||||
LSPServerType::Clangd => vec![LanguageId::C, LanguageId::Cpp],
|
||||
LSPServerType::Tombi => vec![LanguageId::Toml],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +231,11 @@ impl LSPServerType {
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => None,
|
||||
LSPServerType::Tombi => None,
|
||||
LSPServerType::RustAnalyzer
|
||||
| LSPServerType::GoPls
|
||||
| LSPServerType::Pyright
|
||||
| LSPServerType::Clangd => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +248,7 @@ impl LSPServerType {
|
||||
Box::new(TypeScriptLanguageServerCandidate::new(client))
|
||||
}
|
||||
LSPServerType::Clangd => Box::new(ClangdCandidate::new(client)),
|
||||
LSPServerType::Tombi => Box::new(TombiCandidate::new(client)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user