Add LSP completion/code actions/rename/signature help infrastructure, fix TypeScript LSP, rebrand app identifiers

- LSP Layer: Add completion, completion_resolve, signature_help, code_action,
  prepare_rename, and rename methods to TextDocumentService and LspServerModel
- Types: Add CompletionItemData, CompletionResult, SignatureHelpResult,
  CodeActionData, PrepareRenameResult, RenameResult, FileEdits
- Feature Flags: Add LspCompletion, LspCodeActions, LspRename, LspSignatureHelp
- Client Capabilities: Declare completion, signature help, rename, and code
  action capabilities so servers advertise these features
- Completion UI: Add completion state machine with debounced triggers, fuzzy
  filtering, positioned overlay menu, and edit application
- TypeScript LSP: Switch to npx for running typescript-language-server (handles
  download/caching automatically, survives node version switches)
- PATH Resolution: Add interactive shell PATH fallback for LSP server discovery
  and spawning (fixes nvm/fnm/volta users)
- App Identity: Rebrand from com.samsung.Galaxy/dev.warp.WarpOss to
  samsung.galaxy.GalaxyOss across bundle IDs, URL schemes, and plists
- Add scripts/reset-galaxy.sh for clean slate testing
- Galaxy status messages and other in-progress work

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Ward
2026-05-18 15:57:47 -05:00
co-authored by Claude Opus 4.6
parent f37a744692
commit a75bc99852
15 changed files with 769 additions and 162 deletions
+7 -7
View File
@@ -37,7 +37,7 @@ pub struct ChannelState {
impl ChannelState {
pub fn init() -> Self {
let channel = Channel::Oss;
let app_id = AppId::new("dev", "warp", "WarpOss");
let app_id = AppId::new("samsung", "galaxy", "GalaxyOss");
Self {
channel,
additional_features: Default::default(),
@@ -379,13 +379,13 @@ impl ChannelState {
pub fn url_scheme() -> &'static str {
match Self::channel() {
Channel::Stable => "warp",
Channel::Preview => "warppreview",
Channel::Dev => "warpdev",
Channel::Stable => "galaxy",
Channel::Preview => "galaxypreview",
Channel::Dev => "galaxydev",
// Dummy value--integration tests shouldn't support URL schemes.
Channel::Integration => "warpintegration",
Channel::Local => "warplocal",
Channel::Oss => "warposs",
Channel::Integration => "galaxyintegration",
Channel::Local => "galaxylocal",
Channel::Oss => "galaxyoss",
}
}
}
+33
View File
@@ -48,4 +48,37 @@ impl CommandBuilder {
}
cmd
}
/// Attempts to capture PATH from the user's login shell.
/// This is a fallback for when the PATH passed in from the terminal session
/// doesn't include paths set by tools like nvm, pyenv, rbenv, etc. that
/// modify PATH in shell rc files.
#[cfg(not(target_arch = "wasm32"))]
pub async fn capture_interactive_shell_path() -> Option<String> {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
let output = Command::new(&shell)
.args(["-i", "-l", "-c", "echo $PATH"])
.output()
.await
.ok()?;
if !output.status.success() {
log::warn!(
"Failed to capture PATH from interactive shell ({}): {}",
shell,
String::from_utf8_lossy(&output.stderr)
);
return None;
}
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if path.is_empty() {
log::warn!("Interactive shell returned empty PATH");
return None;
}
log::info!("Captured PATH from interactive shell ({shell})");
Some(path)
}
}
+39 -7
View File
@@ -167,25 +167,24 @@ impl LspServerConfig {
/// and working on PATH, we use that. Otherwise, we fall back to our custom installation.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn command_and_params(self) -> Result<ResolvedLspCommand> {
// PATH takes precedence - only use custom installation if not working on PATH
let executor = crate::CommandBuilder::new(self.path_env_var.clone());
// Resolve the effective PATH for this server. We try multiple sources:
// 1. The PATH passed from the terminal session
// 2. PATH captured from the user's interactive login shell (picks up nvm, fnm, volta, etc.)
let executor = self.resolve_effective_executor().await;
let is_working_on_path = self
.server_type
.is_working_on_path(&executor, self.client.clone())
.await;
let custom_binary_config = if is_working_on_path {
// Binary works on PATH, don't use custom installation
None
} else {
// Not working on PATH, check for custom installation
self.server_type
.find_installed_binary_config(executor.path_env_var())
.await
};
// Bail early with a clear error instead of attempting to spawn a
// binary that doesn't exist (which would fail with a confusing
// "No such file or directory" OS error).
if !is_working_on_path && custom_binary_config.is_none() {
anyhow::bail!(
"{} is not installed. Binary was not found on PATH and no custom installation exists",
@@ -212,6 +211,39 @@ impl LspServerConfig {
Ok(ResolvedLspCommand { command, params })
}
/// Resolves the best available PATH for running LSP-related commands.
/// Tries the session PATH first, then falls back to capturing PATH from
/// the user's interactive login shell (which includes nvm, fnm, volta, pyenv, etc.).
#[cfg(not(target_arch = "wasm32"))]
async fn resolve_effective_executor(&self) -> crate::CommandBuilder {
// If we have a session PATH, check if it can at least find common tools
if let Some(ref path) = self.path_env_var {
if !path.is_empty() {
// Quick sanity check: can this PATH find the LSP binary or node?
let executor = crate::CommandBuilder::new(Some(path.clone()));
let works = self
.server_type
.is_working_on_path(&executor, self.client.clone())
.await;
if works {
return executor;
}
}
}
// Session PATH didn't work — try interactive shell PATH
if let Some(shell_path) = crate::CommandBuilder::capture_interactive_shell_path().await {
log::info!(
"Using interactive shell PATH for {} (session PATH insufficient)",
self.server_type.binary_name()
);
return crate::CommandBuilder::new(Some(shell_path));
}
// Fall back to whatever we have
crate::CommandBuilder::new(self.path_env_var.clone())
}
pub(crate) fn server_type(&self) -> LSPServerType {
self.server_type
}
@@ -9,8 +9,6 @@ use async_trait::async_trait;
#[cfg(feature = "local_fs")]
use anyhow::Context;
#[cfg(feature = "local_fs")]
use command::r#async::Command;
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
pub struct TypeScriptLanguageServerCandidate {
@@ -18,88 +16,31 @@ pub struct TypeScriptLanguageServerCandidate {
}
impl TypeScriptLanguageServerCandidate {
/// Path to the new langserver JS file (v4.0.0+) relative to the install directory.
#[cfg(feature = "local_fs")]
const NEW_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.mjs";
/// Path to the old langserver JS file (pre-4.0.0) relative to the install directory.
#[cfg(feature = "local_fs")]
const OLD_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.js";
pub fn new(client: Arc<http_client::Client>) -> Self {
Self { client }
}
/// Finds the configuration for running typescript-language-server from our custom installation.
///
/// Instead of running the wrapper script (which has a shebang requiring node in PATH),
/// we run node directly with the CLI JS file. This is the same pattern used by Zed.
///
/// # Arguments
/// * `path_env_var` - The PATH environment variable to use when checking for system node.
/// Returns a CustomBinaryConfig that runs the server via npx.
/// npx handles downloading/caching the package automatically.
#[cfg(feature = "local_fs")]
pub async fn find_installed_binary_config(
path_env_var: Option<&str>,
) -> Option<CustomBinaryConfig> {
let install_dir = galaxy_core::paths::data_dir().join("typescript-language-server");
// Check for the JS file - prefer new path (cli.mjs) over old path (cli.js)
let server_js = {
let new_path = install_dir.join(Self::NEW_SERVER_PATH);
if new_path.is_file() {
new_path
} else {
let old_path = install_dir.join(Self::OLD_SERVER_PATH);
if old_path.is_file() {
old_path
} else {
log::info!(
"typescript-language-server JS file not found at {} or {}",
new_path.display(),
old_path.display()
);
return None;
}
}
};
// Try to find a working node binary - first custom, then system
let node_binary = node_runtime::find_working_node_binary(path_env_var).await?;
// Verify the installation works by running `node cli.mjs --version`
let mut cmd = Command::new(&node_binary);
// Propagate PATH so "node" (bare name) resolves when using system node
if let Some(path) = path_env_var {
cmd.env("PATH", path);
}
cmd.arg(&server_js).arg("--version");
match cmd.output().await {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout);
log::info!(
"Verified typescript-language-server installation: {}",
version.trim()
);
}
Ok(output) => {
log::warn!(
"typescript-language-server version check failed: {}",
String::from_utf8_lossy(&output.stderr)
);
return None;
}
Err(e) => {
log::warn!(
"Failed to run typescript-language-server version check: {}",
e
);
return None;
}
// npx is available wherever npm/node is — verify it exists
let path_env = path_env_var?;
let mut cmd = command::r#async::Command::new("npx");
cmd.env("PATH", path_env);
cmd.arg("--version");
let output = cmd.output().await.ok()?;
if !output.status.success() {
return None;
}
// Use npx to run typescript-language-server
// --yes skips the install prompt, npx handles download/caching
Some(CustomBinaryConfig {
binary_path: node_binary,
prepend_args: vec![server_js.to_string_lossy().to_string()],
binary_path: "npx".into(),
prepend_args: vec!["--yes".into(), "typescript-language-server".into()],
})
}
}
@@ -108,13 +49,13 @@ impl TypeScriptLanguageServerCandidate {
#[cfg(feature = "local_fs")]
impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
// Check for common JavaScript/TypeScript project indicators
path.join("package.json").exists()
|| path.join("tsconfig.json").exists()
|| path.join("jsconfig.json").exists()
}
async fn is_installed_in_data_dir(&self, executor: &CommandBuilder) -> bool {
// npx is our "installed" state — if npx is available, we can run the server
Self::find_installed_binary_config(executor.path_env_var())
.await
.is_some()
@@ -132,74 +73,23 @@ impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
async fn install(
&self,
metadata: LanguageServerMetadata,
_metadata: LanguageServerMetadata,
executor: &CommandBuilder,
) -> anyhow::Result<()> {
log::info!(
"Installing typescript-language-server version {}",
metadata.version
);
let install_dir = galaxy_core::paths::data_dir().join("typescript-language-server");
// Create the installation directory
async_fs::create_dir_all(&install_dir)
// With npx, there's nothing to install — npx downloads on first use.
// Just verify that npx is available.
let output = executor
.command("npx")
.arg("--version")
.output()
.await
.context("Failed to create typescript-language-server installation directory")?;
// First, check if system node is available and meets requirements
let use_system_node = match executor.path_env_var() {
Some(path) => node_runtime::detect_system_node(path).await.is_ok(),
None => false,
};
let custom_node_paths = if use_system_node {
log::info!("Using system Node.js for typescript-language-server installation");
None
} else {
log::info!("System Node.js not found or too old, installing custom Node.js");
node_runtime::install_npm(&self.client).await?;
Some((
node_runtime::node_binary_path()?,
node_runtime::npm_binary_path()?,
))
};
// Install typescript-language-server and typescript using npm
// typescript is a peer dependency required for the language server to work
log::info!(
"Installing typescript-language-server@{} using npm",
metadata.version
);
// Build the npm install command:
// - System node: run `npm` directly (it's on PATH)
// - Custom node: run `node <npm_path>` to avoid relying on shebang resolution
let mut cmd = if let Some((node_path, npm_path)) = &custom_node_paths {
let mut c = executor.command(node_path);
c.arg(npm_path);
c
} else {
executor.command("npm")
};
cmd.arg("install")
.arg("--ignore-scripts")
.arg(format!("typescript-language-server@{}", metadata.version))
.arg("typescript")
.current_dir(&install_dir);
let output = cmd.output().await.context("Failed to run npm install")?;
.context("npx not found — is Node.js installed?")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"Failed to install typescript-language-server via npm: {}",
stderr
);
anyhow::bail!("npx is not working. Ensure Node.js and npm are installed.");
}
log::info!("typescript-language-server installed successfully");
log::info!("typescript-language-server will run via npx (no pre-install needed)");
Ok(())
}
@@ -211,7 +101,7 @@ impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
Ok(LanguageServerMetadata {
version,
url: None, // npm packages don't have direct download URLs
url: None,
digest: None,
})
}