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:
2026-08-23 13:55:47 -05:00
parent f17642fc62
commit 7c106eecd5
147 changed files with 2208 additions and 1514 deletions
+26
View File
@@ -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
+8 -1
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -2,4 +2,5 @@ pub mod clangd;
pub mod go;
pub mod pyright;
pub mod rust;
pub mod tombi;
pub mod typescript_language_server;
+212
View File
@@ -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")
}
}
+25 -4
View File
@@ -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(
+18 -1
View File
@@ -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)),
}
}