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:
co-authored by
Claude Opus 4.6
parent
f37a744692
commit
a75bc99852
+5
-5
@@ -935,7 +935,7 @@ cloud_mode_input_v2 = ["cloud_mode"]
|
|||||||
[package.metadata.bundle.bin.galaxy-oss]
|
[package.metadata.bundle.bin.galaxy-oss]
|
||||||
category = "public.app-category.developer-tools"
|
category = "public.app-category.developer-tools"
|
||||||
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
||||||
identifier = "com.samsung.Galaxy"
|
identifier = "samsung.galaxy.GalaxyOss"
|
||||||
name = "Galaxy"
|
name = "Galaxy"
|
||||||
resources = ["assets/onboarding"]
|
resources = ["assets/onboarding"]
|
||||||
icon = ["channels/oss/icon/no-padding/512x512.png", "channels/oss/icon/no-padding/icon.ico"]
|
icon = ["channels/oss/icon/no-padding/512x512.png", "channels/oss/icon/no-padding/icon.ico"]
|
||||||
@@ -944,7 +944,7 @@ short_description = "Galaxy - AI-powered terminal for development teams."
|
|||||||
[package.metadata.bundle.bin.galaxy-stable]
|
[package.metadata.bundle.bin.galaxy-stable]
|
||||||
category = "public.app-category.developer-tools"
|
category = "public.app-category.developer-tools"
|
||||||
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
||||||
identifier = "com.samsung.Galaxy-Stable"
|
identifier = "samsung.galaxy.GalaxyStable"
|
||||||
name = "Galaxy"
|
name = "Galaxy"
|
||||||
resources = ["assets/onboarding"]
|
resources = ["assets/onboarding"]
|
||||||
short_description = "Galaxy - AI-powered terminal, stable build."
|
short_description = "Galaxy - AI-powered terminal, stable build."
|
||||||
@@ -952,7 +952,7 @@ short_description = "Galaxy - AI-powered terminal, stable build."
|
|||||||
[package.metadata.bundle.bin.galaxy-preview]
|
[package.metadata.bundle.bin.galaxy-preview]
|
||||||
category = "public.app-category.developer-tools"
|
category = "public.app-category.developer-tools"
|
||||||
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
||||||
identifier = "com.samsung.Galaxy-Preview"
|
identifier = "samsung.galaxy.GalaxyPreview"
|
||||||
name = "Galaxy Preview"
|
name = "Galaxy Preview"
|
||||||
resources = ["assets/onboarding"]
|
resources = ["assets/onboarding"]
|
||||||
short_description = "Galaxy - AI-powered terminal, preview build."
|
short_description = "Galaxy - AI-powered terminal, preview build."
|
||||||
@@ -960,7 +960,7 @@ short_description = "Galaxy - AI-powered terminal, preview build."
|
|||||||
[package.metadata.bundle.bin.galaxy-dev]
|
[package.metadata.bundle.bin.galaxy-dev]
|
||||||
category = "public.app-category.developer-tools"
|
category = "public.app-category.developer-tools"
|
||||||
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
||||||
identifier = "com.samsung.Galaxy-Dev"
|
identifier = "samsung.galaxy.GalaxyDev"
|
||||||
name = "Galaxy Dev"
|
name = "Galaxy Dev"
|
||||||
resources = ["assets/onboarding"]
|
resources = ["assets/onboarding"]
|
||||||
short_description = "Galaxy - AI-powered terminal, developer build."
|
short_description = "Galaxy - AI-powered terminal, developer build."
|
||||||
@@ -968,7 +968,7 @@ short_description = "Galaxy - AI-powered terminal, developer build."
|
|||||||
[package.metadata.bundle.bin.galaxy-local]
|
[package.metadata.bundle.bin.galaxy-local]
|
||||||
category = "public.app-category.developer-tools"
|
category = "public.app-category.developer-tools"
|
||||||
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
copyright = "© 2026, Samsung Electronics Co., Ltd."
|
||||||
identifier = "com.samsung.Galaxy-Local"
|
identifier = "samsung.galaxy.GalaxyLocal"
|
||||||
name = "Galaxy Local"
|
name = "Galaxy Local"
|
||||||
resources = ["assets/onboarding"]
|
resources = ["assets/onboarding"]
|
||||||
short_description = "Galaxy - AI-powered terminal, local build."
|
short_description = "Galaxy - AI-powered terminal, local build."
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.samsung.GalaxyDockTilePlugin</string>
|
<string>samsung.galaxy.GalaxyDockTilePlugin</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>WarpDockTilePlugin</string>
|
<string>WarpDockTilePlugin</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use super::{
|
|||||||
view_impl::common::{
|
view_impl::common::{
|
||||||
render_switch_control_to_user_button, render_warping_indicator,
|
render_switch_control_to_user_button, render_warping_indicator,
|
||||||
render_warping_indicator_base, ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText,
|
render_warping_indicator_base, ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText,
|
||||||
WarpingIndicatorProps, WarpingProps, LOAD_OUTPUT_MESSAGE, WAITING_FOR_USER_INPUT_MESSAGE,
|
WarpingIndicatorProps, WarpingProps, random_load_output_message,
|
||||||
|
WAITING_FOR_USER_INPUT_MESSAGE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -812,7 +813,7 @@ impl BlocklistAIStatusBar {
|
|||||||
);
|
);
|
||||||
let default_warping_text = fallback_warping_text
|
let default_warping_text = fallback_warping_text
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or(LOAD_OUTPUT_MESSAGE)
|
.unwrap_or(random_load_output_message())
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let secondary_element = if fallback_warping_text.is_some() {
|
let secondary_element = if fallback_warping_text.is_some() {
|
||||||
Some(render_fallback_explanation(model.as_ref(), app))
|
Some(render_fallback_explanation(model.as_ref(), app))
|
||||||
|
|||||||
@@ -129,7 +129,54 @@ const IMAGE_SOURCE_LINK_LINE_INDEX: usize = 1;
|
|||||||
const ERROR_APOLOGY_TEXT: &str = "I'm sorry, I couldn't complete that request.";
|
const ERROR_APOLOGY_TEXT: &str = "I'm sorry, I couldn't complete that request.";
|
||||||
const INTERNAL_WARP_ERROR: &str = "Internal Warp error.";
|
const INTERNAL_WARP_ERROR: &str = "Internal Warp error.";
|
||||||
|
|
||||||
pub const LOAD_OUTPUT_MESSAGE: &str = "Warping...";
|
const GALAXY_STATUS_MESSAGES: &[&str] = &[
|
||||||
|
"Warping through the Galaxy...",
|
||||||
|
"Guarding the Galaxy...",
|
||||||
|
"Exploring the Galaxy...",
|
||||||
|
"Charting the Galaxy...",
|
||||||
|
"Traversing the Galaxy...",
|
||||||
|
"Navigating the Galaxy...",
|
||||||
|
"Scanning the Galaxy...",
|
||||||
|
"Hitchhiking across the Galaxy...",
|
||||||
|
"Assembling the Galaxy...",
|
||||||
|
"Aligning stars in the Galaxy...",
|
||||||
|
"Consulting the Galaxy...",
|
||||||
|
"Galaxy brain activated...",
|
||||||
|
"Orbiting the Galaxy...",
|
||||||
|
"Bending the Galaxy...",
|
||||||
|
"Summoning the Galaxy...",
|
||||||
|
"Decoding the Galaxy...",
|
||||||
|
"Harnessing Galaxy power...",
|
||||||
|
"Riding the Galaxy stream...",
|
||||||
|
"Tuning into the Galaxy...",
|
||||||
|
"Surfing the Galaxy waves...",
|
||||||
|
"Mapping the Galaxy...",
|
||||||
|
"Galaxy engines engaged...",
|
||||||
|
"Weaving through the Galaxy...",
|
||||||
|
"Channeling the Galaxy...",
|
||||||
|
"Phasing through the Galaxy...",
|
||||||
|
"Galaxy hyperdrive online...",
|
||||||
|
"Syncing with the Galaxy...",
|
||||||
|
"Galaxy core spinning up...",
|
||||||
|
"Drifting through the Galaxy...",
|
||||||
|
"Galaxy warp field stable...",
|
||||||
|
"Tapping into Galaxy energy...",
|
||||||
|
"Galaxy neurons firing...",
|
||||||
|
"Quantum leaping the Galaxy...",
|
||||||
|
"Galaxy thrusters ignited...",
|
||||||
|
"Calculating Galaxy trajectory...",
|
||||||
|
"Galaxy flux capacitor charged...",
|
||||||
|
"Piercing the Galaxy veil...",
|
||||||
|
"Galaxy signal acquired...",
|
||||||
|
"Folding Galaxy spacetime...",
|
||||||
|
"Galaxy coordinates locked...",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn random_load_output_message() -> &'static str {
|
||||||
|
use rand::Rng;
|
||||||
|
let idx = rand::thread_rng().gen_range(0..GALAXY_STATUS_MESSAGES.len());
|
||||||
|
GALAXY_STATUS_MESSAGES[idx]
|
||||||
|
}
|
||||||
pub const LOAD_OUTPUT_MESSAGE_FOR_ADJUSTING: &str = "Adjusting tasks...";
|
pub const LOAD_OUTPUT_MESSAGE_FOR_ADJUSTING: &str = "Adjusting tasks...";
|
||||||
pub const LOAD_OUTPUT_MESSAGE_FOR_PASSIVE_CODE_GEN: &str = "Generating fix...";
|
pub const LOAD_OUTPUT_MESSAGE_FOR_PASSIVE_CODE_GEN: &str = "Generating fix...";
|
||||||
pub const LOAD_OUTPUT_MESSAGE_FOR_CREATING_DIFF: &str = "Creating diff...";
|
pub const LOAD_OUTPUT_MESSAGE_FOR_CREATING_DIFF: &str = "Creating diff...";
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ embed_plist::embed_info_plist_bytes!(r#"
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>galaxy-local</string>
|
<string>galaxy-local</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.samsung.Galaxy-Local</string>
|
<string>samsung.galaxy.GalaxyLocal</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ embed_plist::embed_info_plist_bytes!(r#"
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>galaxy-oss</string>
|
<string>galaxy-oss</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.samsung.Galaxy</string>
|
<string>samsung.galaxy.GalaxyOss</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
|
|||||||
@@ -0,0 +1,513 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use futures::stream::AbortHandle;
|
||||||
|
use galaxy_core::features::FeatureFlag;
|
||||||
|
use galaxy_core::ui::appearance::Appearance;
|
||||||
|
use galaxy_core::ui::theme::color::internal_colors;
|
||||||
|
use galaxyui::elements::{
|
||||||
|
Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||||
|
MainAxisSize, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius,
|
||||||
|
Shrinkable, Text,
|
||||||
|
};
|
||||||
|
use galaxyui::{AppContext, Element, SingletonEntity, ViewContext};
|
||||||
|
use lsp::{CompletionItemData, CompletionKind, CompletionResult, CompletionTrigger};
|
||||||
|
use pathfinder_geometry::vector::Vector2F;
|
||||||
|
use string_offset::CharOffset;
|
||||||
|
use vec1::Vec1;
|
||||||
|
|
||||||
|
use super::local_code_editor::LocalCodeEditorView;
|
||||||
|
|
||||||
|
pub const COMPLETION_DEBOUNCE_PERIOD: Duration = Duration::from_millis(50);
|
||||||
|
const COMPLETION_MENU_MAX_HEIGHT: f32 = 220.;
|
||||||
|
const COMPLETION_MENU_WIDTH: f32 = 340.;
|
||||||
|
const MAX_VISIBLE_ITEMS: usize = 10;
|
||||||
|
|
||||||
|
/// State machine for the completion feature.
|
||||||
|
pub(super) enum CompletionState {
|
||||||
|
/// No completion session active.
|
||||||
|
Idle,
|
||||||
|
/// Waiting for the LSP response.
|
||||||
|
Requesting {
|
||||||
|
abort_handle: AbortHandle,
|
||||||
|
trigger_offset: CharOffset,
|
||||||
|
},
|
||||||
|
/// Completion menu is visible with results.
|
||||||
|
Showing {
|
||||||
|
items: Vec<CompletionItemData>,
|
||||||
|
filtered_indices: Vec<usize>,
|
||||||
|
selected_index: usize,
|
||||||
|
trigger_offset: CharOffset,
|
||||||
|
is_incomplete: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CompletionState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionState {
|
||||||
|
pub fn is_showing(&self) -> bool {
|
||||||
|
matches!(self, Self::Showing { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dismiss(&mut self) -> bool {
|
||||||
|
if matches!(self, Self::Idle) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Self::Requesting { abort_handle, .. } = self {
|
||||||
|
abort_handle.abort();
|
||||||
|
}
|
||||||
|
*self = Self::Idle;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_item(&self) -> Option<&CompletionItemData> {
|
||||||
|
match self {
|
||||||
|
Self::Showing {
|
||||||
|
items,
|
||||||
|
filtered_indices,
|
||||||
|
selected_index,
|
||||||
|
..
|
||||||
|
} => filtered_indices
|
||||||
|
.get(*selected_index)
|
||||||
|
.and_then(|&idx| items.get(idx)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_selection(&mut self, delta: i32) {
|
||||||
|
if let Self::Showing {
|
||||||
|
filtered_indices,
|
||||||
|
selected_index,
|
||||||
|
..
|
||||||
|
} = self
|
||||||
|
{
|
||||||
|
if filtered_indices.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let len = filtered_indices.len() as i32;
|
||||||
|
let new_idx = (*selected_index as i32 + delta).rem_euclid(len);
|
||||||
|
*selected_index = new_idx as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter(&mut self, query: &str) {
|
||||||
|
if let Self::Showing {
|
||||||
|
items,
|
||||||
|
filtered_indices,
|
||||||
|
selected_index,
|
||||||
|
..
|
||||||
|
} = self
|
||||||
|
{
|
||||||
|
if query.is_empty() {
|
||||||
|
*filtered_indices = (0..items.len()).collect();
|
||||||
|
} else {
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
*filtered_indices = items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, item)| {
|
||||||
|
let filter_text = item.effective_filter_text().to_lowercase();
|
||||||
|
fuzzy_match(&filter_text, &query_lower)
|
||||||
|
})
|
||||||
|
.map(|(idx, _)| idx)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
if *selected_index >= filtered_indices.len() {
|
||||||
|
*selected_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fuzzy_match(target: &str, query: &str) -> bool {
|
||||||
|
let mut target_chars = target.chars();
|
||||||
|
for query_char in query.chars() {
|
||||||
|
loop {
|
||||||
|
match target_chars.next() {
|
||||||
|
Some(tc) if tc == query_char => break,
|
||||||
|
Some(_) => continue,
|
||||||
|
None => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalCodeEditorView {
|
||||||
|
pub(super) fn is_completion_enabled() -> bool {
|
||||||
|
FeatureFlag::LspCompletion.is_enabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a user typing event — potentially trigger completion.
|
||||||
|
pub(super) fn on_content_changed_for_completion(&mut self, ctx: &mut ViewContext<Self>) {
|
||||||
|
if !Self::is_completion_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.lsp_server.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor_offset = self.editor().as_ref(ctx).cursor_head_offset(ctx);
|
||||||
|
if cursor_offset == CharOffset::from(0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let char_before = self
|
||||||
|
.editor()
|
||||||
|
.as_ref(ctx)
|
||||||
|
.char_at(cursor_offset - CharOffset::from(1), ctx);
|
||||||
|
|
||||||
|
let trigger = match char_before {
|
||||||
|
Some('.') => Some(CompletionTrigger::TriggerCharacter(".".into())),
|
||||||
|
Some(':') => {
|
||||||
|
if cursor_offset >= CharOffset::from(2) {
|
||||||
|
let prev_char = self
|
||||||
|
.editor()
|
||||||
|
.as_ref(ctx)
|
||||||
|
.char_at(cursor_offset - CharOffset::from(2), ctx);
|
||||||
|
if prev_char == Some(':') {
|
||||||
|
Some(CompletionTrigger::TriggerCharacter("::".into()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(c) if c.is_alphanumeric() || c == '_' => {
|
||||||
|
let _ = self.completion_debounce_tx.try_send(cursor_offset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if self.completion_state.dismiss() {
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(trigger) = trigger {
|
||||||
|
self.request_completion(cursor_offset, trigger, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request completions from the LSP server.
|
||||||
|
pub(super) fn request_completion(
|
||||||
|
&mut self,
|
||||||
|
trigger_offset: CharOffset,
|
||||||
|
trigger: CompletionTrigger,
|
||||||
|
ctx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if !Self::is_completion_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file_path) = self.file_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(lsp_server) = &self.lsp_server else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let lsp_position = self
|
||||||
|
.editor()
|
||||||
|
.as_ref(ctx)
|
||||||
|
.offset_to_lsp_position(trigger_offset, ctx);
|
||||||
|
|
||||||
|
let future = match lsp_server
|
||||||
|
.as_ref(ctx)
|
||||||
|
.completion(file_path.to_path_buf(), lsp_position, trigger)
|
||||||
|
{
|
||||||
|
Ok(future) => future,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to call lsp.completion: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.completion_state.dismiss();
|
||||||
|
|
||||||
|
let abort_handle = ctx
|
||||||
|
.spawn(future, move |me, result, ctx| {
|
||||||
|
me.handle_completion_response(result, trigger_offset, ctx);
|
||||||
|
})
|
||||||
|
.abort_handle();
|
||||||
|
|
||||||
|
self.completion_state = CompletionState::Requesting {
|
||||||
|
abort_handle,
|
||||||
|
trigger_offset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the debounced completion trigger (from typing).
|
||||||
|
pub(super) fn request_completion_debounced(
|
||||||
|
&mut self,
|
||||||
|
offset: CharOffset,
|
||||||
|
ctx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if let CompletionState::Showing { trigger_offset, .. } = &self.completion_state {
|
||||||
|
let query = self.get_completion_filter_query(*trigger_offset, ctx);
|
||||||
|
self.completion_state.filter(&query);
|
||||||
|
ctx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request_completion(offset, CompletionTrigger::Invoked, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_completion_response(
|
||||||
|
&mut self,
|
||||||
|
result: anyhow::Result<Option<CompletionResult>>,
|
||||||
|
trigger_offset: CharOffset,
|
||||||
|
ctx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let completion_result = match result {
|
||||||
|
Ok(Some(result)) if !result.items.is_empty() => result,
|
||||||
|
_ => {
|
||||||
|
self.completion_state = CompletionState::Idle;
|
||||||
|
ctx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let filtered_indices: Vec<usize> = (0..completion_result.items.len()).collect();
|
||||||
|
|
||||||
|
self.completion_state = CompletionState::Showing {
|
||||||
|
items: completion_result.items,
|
||||||
|
filtered_indices,
|
||||||
|
selected_index: 0,
|
||||||
|
trigger_offset,
|
||||||
|
is_incomplete: completion_result.is_incomplete,
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = self.get_completion_filter_query(trigger_offset, ctx);
|
||||||
|
self.completion_state.filter(&query);
|
||||||
|
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_completion_filter_query(
|
||||||
|
&self,
|
||||||
|
trigger_offset: CharOffset,
|
||||||
|
ctx: &ViewContext<Self>,
|
||||||
|
) -> String {
|
||||||
|
let editor = self.editor().as_ref(ctx);
|
||||||
|
let cursor_offset = editor.cursor_head_offset(ctx);
|
||||||
|
if cursor_offset <= trigger_offset {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access the buffer to get text in range
|
||||||
|
editor
|
||||||
|
.buffer_text_in_range(trigger_offset..cursor_offset, ctx)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm the currently selected completion item.
|
||||||
|
pub(super) fn confirm_completion(&mut self, ctx: &mut ViewContext<Self>) -> bool {
|
||||||
|
let (insert_text, text_edit_range, trigger_offset) = match &self.completion_state {
|
||||||
|
CompletionState::Showing {
|
||||||
|
items,
|
||||||
|
filtered_indices,
|
||||||
|
selected_index,
|
||||||
|
trigger_offset,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let Some(&idx) = filtered_indices.get(*selected_index) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let item = &items[idx];
|
||||||
|
(
|
||||||
|
item.insert_text.clone(),
|
||||||
|
item.text_edit_range.clone(),
|
||||||
|
*trigger_offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.completion_state = CompletionState::Idle;
|
||||||
|
|
||||||
|
self.editor.update(ctx, |editor, ctx| {
|
||||||
|
let edit_range = if let Some(range) = text_edit_range {
|
||||||
|
let start = editor.lsp_location_to_offset(&range.start, ctx);
|
||||||
|
let end = editor.lsp_location_to_offset(&range.end, ctx);
|
||||||
|
start..end
|
||||||
|
} else {
|
||||||
|
let cursor = editor.cursor_head_offset(ctx);
|
||||||
|
trigger_offset..cursor
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(edits) = Vec1::try_from_vec(vec![(insert_text, edit_range)]) {
|
||||||
|
editor.apply_edits(edits, ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.notify();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the completion menu overlay.
|
||||||
|
pub(super) fn render_completion_menu(&self, app: &AppContext) -> Option<Box<dyn Element>> {
|
||||||
|
let CompletionState::Showing {
|
||||||
|
items,
|
||||||
|
filtered_indices,
|
||||||
|
selected_index,
|
||||||
|
..
|
||||||
|
} = &self.completion_state
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if filtered_indices.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let appearance = Appearance::as_ref(app);
|
||||||
|
let theme = appearance.theme();
|
||||||
|
let visible_count = filtered_indices.len().min(MAX_VISIBLE_ITEMS);
|
||||||
|
|
||||||
|
let mut content_column = Flex::column();
|
||||||
|
|
||||||
|
for (display_idx, &item_idx) in
|
||||||
|
filtered_indices.iter().enumerate().take(visible_count)
|
||||||
|
{
|
||||||
|
let item = &items[item_idx];
|
||||||
|
let is_selected = display_idx == *selected_index;
|
||||||
|
content_column.add_child(render_completion_item(item, is_selected, appearance));
|
||||||
|
}
|
||||||
|
|
||||||
|
let constrained_content = ConstrainedBox::new(content_column.finish())
|
||||||
|
.with_width(COMPLETION_MENU_WIDTH)
|
||||||
|
.with_max_height(COMPLETION_MENU_MAX_HEIGHT)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let menu = Container::new(constrained_content)
|
||||||
|
.with_background(theme.background())
|
||||||
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
||||||
|
.with_border(Border::all(1.).with_border_fill(internal_colors::neutral_4(theme)))
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
Some(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the positioning for the completion menu (below cursor).
|
||||||
|
pub(super) fn completion_menu_positioning(
|
||||||
|
&self,
|
||||||
|
app: &AppContext,
|
||||||
|
) -> Option<OffsetPositioning> {
|
||||||
|
let trigger_offset = match &self.completion_state {
|
||||||
|
CompletionState::Showing { trigger_offset, .. } => *trigger_offset,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bounds = self
|
||||||
|
.editor()
|
||||||
|
.as_ref(app)
|
||||||
|
.character_bounds_in_viewport(trigger_offset, app)?;
|
||||||
|
|
||||||
|
Some(OffsetPositioning::offset_from_parent(
|
||||||
|
Vector2F::new(bounds.origin_x(), bounds.max_y()),
|
||||||
|
ParentOffsetBounds::ParentByPosition,
|
||||||
|
ParentAnchor::TopLeft,
|
||||||
|
ChildAnchor::TopLeft,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_completion_item(
|
||||||
|
item: &CompletionItemData,
|
||||||
|
is_selected: bool,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) -> Box<dyn Element> {
|
||||||
|
let theme = appearance.theme();
|
||||||
|
|
||||||
|
let icon_text: &'static str = match item.kind {
|
||||||
|
Some(CompletionKind::Function) | Some(CompletionKind::Method) => "fn",
|
||||||
|
Some(CompletionKind::Variable) => "var",
|
||||||
|
Some(CompletionKind::Field) | Some(CompletionKind::Property) => "fld",
|
||||||
|
Some(CompletionKind::Class) | Some(CompletionKind::Struct) => "str",
|
||||||
|
Some(CompletionKind::Interface) => "ifc",
|
||||||
|
Some(CompletionKind::Module) => "mod",
|
||||||
|
Some(CompletionKind::Enum) => "enm",
|
||||||
|
Some(CompletionKind::EnumMember) => "emb",
|
||||||
|
Some(CompletionKind::Constant) => "cst",
|
||||||
|
Some(CompletionKind::Keyword) => "kw",
|
||||||
|
Some(CompletionKind::Snippet) => "snp",
|
||||||
|
Some(CompletionKind::TypeParameter) => "typ",
|
||||||
|
_ => " ",
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = item.label.clone();
|
||||||
|
let detail = item.detail.clone();
|
||||||
|
|
||||||
|
let mut row = Flex::row()
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||||
|
.with_main_axis_size(MainAxisSize::Min);
|
||||||
|
|
||||||
|
row.add_child(
|
||||||
|
Container::new(
|
||||||
|
Text::new_inline(
|
||||||
|
icon_text,
|
||||||
|
appearance.monospace_font_family(),
|
||||||
|
appearance.monospace_font_size() * 0.85,
|
||||||
|
)
|
||||||
|
.with_color(theme.disabled_ui_text_color().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_padding_left(4.)
|
||||||
|
.with_padding_right(6.)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
|
||||||
|
row.add_child(
|
||||||
|
Shrinkable::new(
|
||||||
|
1.,
|
||||||
|
Text::new(
|
||||||
|
label,
|
||||||
|
appearance.monospace_font_family(),
|
||||||
|
appearance.monospace_font_size(),
|
||||||
|
)
|
||||||
|
.with_color(theme.active_ui_text_color().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(detail) = detail {
|
||||||
|
row.add_child(
|
||||||
|
Container::new(
|
||||||
|
Shrinkable::new(
|
||||||
|
2.,
|
||||||
|
Text::new(
|
||||||
|
detail,
|
||||||
|
appearance.monospace_font_family(),
|
||||||
|
appearance.monospace_font_size() * 0.85,
|
||||||
|
)
|
||||||
|
.with_color(theme.disabled_ui_text_color().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_padding_left(8.)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut container = Container::new(row.finish())
|
||||||
|
.with_vertical_padding(3.)
|
||||||
|
.with_horizontal_padding(4.);
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
container = container.with_background(internal_colors::neutral_2(theme));
|
||||||
|
} else {
|
||||||
|
container = container.with_background(theme.background());
|
||||||
|
}
|
||||||
|
|
||||||
|
container.finish()
|
||||||
|
}
|
||||||
@@ -1660,6 +1660,16 @@ impl CodeEditorView {
|
|||||||
self.model.as_ref(ctx).selections(ctx).first().head
|
self.model.as_ref(ctx).selections(ctx).first().head
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns text in the given character offset range from the buffer.
|
||||||
|
pub fn buffer_text_in_range(
|
||||||
|
&self,
|
||||||
|
range: Range<CharOffset>,
|
||||||
|
ctx: &AppContext,
|
||||||
|
) -> Option<String> {
|
||||||
|
let buffer = self.model.as_ref(ctx).buffer().as_ref(ctx);
|
||||||
|
Some(buffer.text_in_range(range).into_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hovered_symbol_range<'a>(
|
pub fn hovered_symbol_range<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
ctx: &'a AppContext,
|
ctx: &'a AppContext,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ use super::editor::{
|
|||||||
scroll::{ScrollPosition, ScrollTrigger},
|
scroll::{ScrollPosition, ScrollTrigger},
|
||||||
view::{CodeEditorEvent, CodeEditorView},
|
view::{CodeEditorEvent, CodeEditorView},
|
||||||
};
|
};
|
||||||
|
use super::completion::{CompletionState, COMPLETION_DEBOUNCE_PERIOD};
|
||||||
use super::find_references_view::{FindReferencesView, FindReferencesViewEvent};
|
use super::find_references_view::{FindReferencesView, FindReferencesViewEvent};
|
||||||
use super::language_server_extension::ProcessedDiagnostic;
|
use super::language_server_extension::ProcessedDiagnostic;
|
||||||
use super::lsp_telemetry::LspTelemetryEvent;
|
use super::lsp_telemetry::LspTelemetryEvent;
|
||||||
@@ -303,6 +304,10 @@ pub struct LocalCodeEditorView {
|
|||||||
pub(super) diagnostic_decorations: Vec<Decoration>,
|
pub(super) diagnostic_decorations: Vec<Decoration>,
|
||||||
/// View for the find references feature.
|
/// View for the find references feature.
|
||||||
find_references_view: Option<ViewHandle<FindReferencesView>>,
|
find_references_view: Option<ViewHandle<FindReferencesView>>,
|
||||||
|
/// State for the LSP completion menu.
|
||||||
|
pub(super) completion_state: CompletionState,
|
||||||
|
/// Channel for debouncing completion requests triggered by typing.
|
||||||
|
pub(super) completion_debounce_tx: async_channel::Sender<CharOffset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalCodeEditorView {
|
impl LocalCodeEditorView {
|
||||||
@@ -337,6 +342,9 @@ impl LocalCodeEditorView {
|
|||||||
if origin.from_user() {
|
if origin.from_user() {
|
||||||
me.was_edited = true;
|
me.was_edited = true;
|
||||||
ctx.emit(LocalCodeEditorEvent::UserEdited);
|
ctx.emit(LocalCodeEditorEvent::UserEdited);
|
||||||
|
|
||||||
|
// Trigger completion on user typing
|
||||||
|
me.on_content_changed_for_completion(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CodeEditorEvent::VimEscapeInNormalMode => {
|
CodeEditorEvent::VimEscapeInNormalMode => {
|
||||||
@@ -473,6 +481,14 @@ impl LocalCodeEditorView {
|
|||||||
|_, _| {},
|
|_, _| {},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set up debounce for completion requests (shorter period than hover)
|
||||||
|
let (completion_debounce_tx, completion_debounce_rx) = async_channel::unbounded();
|
||||||
|
ctx.spawn_stream_local(
|
||||||
|
debounce(COMPLETION_DEBOUNCE_PERIOD, completion_debounce_rx),
|
||||||
|
|me, offset, ctx| me.request_completion_debounced(offset, ctx),
|
||||||
|
|_, _| {},
|
||||||
|
);
|
||||||
|
|
||||||
let model = Self {
|
let model = Self {
|
||||||
editor,
|
editor,
|
||||||
diff_type,
|
diff_type,
|
||||||
@@ -495,6 +511,8 @@ impl LocalCodeEditorView {
|
|||||||
processed_diagnostics: Vec::new(),
|
processed_diagnostics: Vec::new(),
|
||||||
diagnostic_decorations: Vec::new(),
|
diagnostic_decorations: Vec::new(),
|
||||||
find_references_view: None,
|
find_references_view: None,
|
||||||
|
completion_state: CompletionState::default(),
|
||||||
|
completion_debounce_tx,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(display_mode) = display_mode {
|
if let Some(display_mode) = display_mode {
|
||||||
@@ -1915,7 +1933,8 @@ impl LocalCodeEditorView {
|
|||||||
fn dismiss_lsp_overlays(&mut self, ctx: &mut ViewContext<Self>) -> bool {
|
fn dismiss_lsp_overlays(&mut self, ctx: &mut ViewContext<Self>) -> bool {
|
||||||
let had_refs = self.close_find_references_card(ctx);
|
let had_refs = self.close_find_references_card(ctx);
|
||||||
let had_hover = self.lsp_hover_state.clear();
|
let had_hover = self.lsp_hover_state.clear();
|
||||||
had_refs || had_hover
|
let had_completion = self.completion_state.dismiss();
|
||||||
|
had_refs || had_hover || had_completion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform goto definition at the cursor position and navigate directly.
|
/// Perform goto definition at the cursor position and navigate directly.
|
||||||
@@ -2181,6 +2200,14 @@ impl View for LocalCodeEditorView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render completion menu if active
|
||||||
|
if let (Some(completion_menu), Some(positioning)) = (
|
||||||
|
self.render_completion_menu(app),
|
||||||
|
self.completion_menu_positioning(app),
|
||||||
|
) {
|
||||||
|
stack.add_positioned_overlay_child(completion_menu, positioning);
|
||||||
|
}
|
||||||
|
|
||||||
// Render LSP hover tooltip if available (render last so it appears on top)
|
// Render LSP hover tooltip if available (render last so it appears on top)
|
||||||
if let (Some(hover_tooltip), Some(positioning)) = (
|
if let (Some(hover_tooltip), Some(positioning)) = (
|
||||||
self.render_hover_tooltip(app),
|
self.render_hover_tooltip(app),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use std::any::Any;
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::ops::AddAssign;
|
use std::ops::AddAssign;
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub mod completion;
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
pub mod find_references_view;
|
pub mod find_references_view;
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub struct ChannelState {
|
|||||||
impl ChannelState {
|
impl ChannelState {
|
||||||
pub fn init() -> Self {
|
pub fn init() -> Self {
|
||||||
let channel = Channel::Oss;
|
let channel = Channel::Oss;
|
||||||
let app_id = AppId::new("dev", "warp", "WarpOss");
|
let app_id = AppId::new("samsung", "galaxy", "GalaxyOss");
|
||||||
Self {
|
Self {
|
||||||
channel,
|
channel,
|
||||||
additional_features: Default::default(),
|
additional_features: Default::default(),
|
||||||
@@ -379,13 +379,13 @@ impl ChannelState {
|
|||||||
|
|
||||||
pub fn url_scheme() -> &'static str {
|
pub fn url_scheme() -> &'static str {
|
||||||
match Self::channel() {
|
match Self::channel() {
|
||||||
Channel::Stable => "warp",
|
Channel::Stable => "galaxy",
|
||||||
Channel::Preview => "warppreview",
|
Channel::Preview => "galaxypreview",
|
||||||
Channel::Dev => "warpdev",
|
Channel::Dev => "galaxydev",
|
||||||
// Dummy value--integration tests shouldn't support URL schemes.
|
// Dummy value--integration tests shouldn't support URL schemes.
|
||||||
Channel::Integration => "warpintegration",
|
Channel::Integration => "galaxyintegration",
|
||||||
Channel::Local => "warplocal",
|
Channel::Local => "galaxylocal",
|
||||||
Channel::Oss => "warposs",
|
Channel::Oss => "galaxyoss",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,37 @@ impl CommandBuilder {
|
|||||||
}
|
}
|
||||||
cmd
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,25 +167,24 @@ impl LspServerConfig {
|
|||||||
/// and working on PATH, we use that. Otherwise, we fall back to our custom installation.
|
/// and working on PATH, we use that. Otherwise, we fall back to our custom installation.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) async fn command_and_params(self) -> Result<ResolvedLspCommand> {
|
pub(crate) async fn command_and_params(self) -> Result<ResolvedLspCommand> {
|
||||||
// PATH takes precedence - only use custom installation if not working on PATH
|
// Resolve the effective PATH for this server. We try multiple sources:
|
||||||
let executor = crate::CommandBuilder::new(self.path_env_var.clone());
|
// 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
|
let is_working_on_path = self
|
||||||
.server_type
|
.server_type
|
||||||
.is_working_on_path(&executor, self.client.clone())
|
.is_working_on_path(&executor, self.client.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let custom_binary_config = if is_working_on_path {
|
let custom_binary_config = if is_working_on_path {
|
||||||
// Binary works on PATH, don't use custom installation
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// Not working on PATH, check for custom installation
|
|
||||||
self.server_type
|
self.server_type
|
||||||
.find_installed_binary_config(executor.path_env_var())
|
.find_installed_binary_config(executor.path_env_var())
|
||||||
.await
|
.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() {
|
if !is_working_on_path && custom_binary_config.is_none() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"{} is not installed. Binary was not found on PATH and no custom installation exists",
|
"{} is not installed. Binary was not found on PATH and no custom installation exists",
|
||||||
@@ -212,6 +211,39 @@ impl LspServerConfig {
|
|||||||
Ok(ResolvedLspCommand { command, params })
|
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 {
|
pub(crate) fn server_type(&self) -> LSPServerType {
|
||||||
self.server_type
|
self.server_type
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ use async_trait::async_trait;
|
|||||||
|
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
#[cfg(feature = "local_fs")]
|
|
||||||
use command::r#async::Command;
|
|
||||||
|
|
||||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||||
pub struct TypeScriptLanguageServerCandidate {
|
pub struct TypeScriptLanguageServerCandidate {
|
||||||
@@ -18,88 +16,31 @@ pub struct TypeScriptLanguageServerCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn new(client: Arc<http_client::Client>) -> Self {
|
||||||
Self { client }
|
Self { client }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the configuration for running typescript-language-server from our custom installation.
|
/// Returns a CustomBinaryConfig that runs the server via npx.
|
||||||
///
|
/// npx handles downloading/caching the package automatically.
|
||||||
/// 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.
|
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
pub async fn find_installed_binary_config(
|
pub async fn find_installed_binary_config(
|
||||||
path_env_var: Option<&str>,
|
path_env_var: Option<&str>,
|
||||||
) -> Option<CustomBinaryConfig> {
|
) -> Option<CustomBinaryConfig> {
|
||||||
let install_dir = galaxy_core::paths::data_dir().join("typescript-language-server");
|
// npx is available wherever npm/node is — verify it exists
|
||||||
|
let path_env = path_env_var?;
|
||||||
// Check for the JS file - prefer new path (cli.mjs) over old path (cli.js)
|
let mut cmd = command::r#async::Command::new("npx");
|
||||||
let server_js = {
|
cmd.env("PATH", path_env);
|
||||||
let new_path = install_dir.join(Self::NEW_SERVER_PATH);
|
cmd.arg("--version");
|
||||||
if new_path.is_file() {
|
let output = cmd.output().await.ok()?;
|
||||||
new_path
|
if !output.status.success() {
|
||||||
} else {
|
return None;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use npx to run typescript-language-server
|
||||||
|
// --yes skips the install prompt, npx handles download/caching
|
||||||
Some(CustomBinaryConfig {
|
Some(CustomBinaryConfig {
|
||||||
binary_path: node_binary,
|
binary_path: "npx".into(),
|
||||||
prepend_args: vec![server_js.to_string_lossy().to_string()],
|
prepend_args: vec!["--yes".into(), "typescript-language-server".into()],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,13 +49,13 @@ impl TypeScriptLanguageServerCandidate {
|
|||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
|
impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
|
||||||
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
|
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("package.json").exists()
|
||||||
|| path.join("tsconfig.json").exists()
|
|| path.join("tsconfig.json").exists()
|
||||||
|| path.join("jsconfig.json").exists()
|
|| path.join("jsconfig.json").exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_installed_in_data_dir(&self, executor: &CommandBuilder) -> bool {
|
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())
|
Self::find_installed_binary_config(executor.path_env_var())
|
||||||
.await
|
.await
|
||||||
.is_some()
|
.is_some()
|
||||||
@@ -132,74 +73,23 @@ impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
|
|||||||
|
|
||||||
async fn install(
|
async fn install(
|
||||||
&self,
|
&self,
|
||||||
metadata: LanguageServerMetadata,
|
_metadata: LanguageServerMetadata,
|
||||||
executor: &CommandBuilder,
|
executor: &CommandBuilder,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
log::info!(
|
// With npx, there's nothing to install — npx downloads on first use.
|
||||||
"Installing typescript-language-server version {}",
|
// Just verify that npx is available.
|
||||||
metadata.version
|
let output = executor
|
||||||
);
|
.command("npx")
|
||||||
|
.arg("--version")
|
||||||
let install_dir = galaxy_core::paths::data_dir().join("typescript-language-server");
|
.output()
|
||||||
|
|
||||||
// Create the installation directory
|
|
||||||
async_fs::create_dir_all(&install_dir)
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to create typescript-language-server installation directory")?;
|
.context("npx not found — is Node.js installed?")?;
|
||||||
|
|
||||||
// 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")?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
anyhow::bail!("npx is not working. Ensure Node.js and npm are installed.");
|
||||||
anyhow::bail!(
|
|
||||||
"Failed to install typescript-language-server via npm: {}",
|
|
||||||
stderr
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("typescript-language-server installed successfully");
|
log::info!("typescript-language-server will run via npx (no pre-install needed)");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +101,7 @@ impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
|
|||||||
|
|
||||||
Ok(LanguageServerMetadata {
|
Ok(LanguageServerMetadata {
|
||||||
version,
|
version,
|
||||||
url: None, // npm packages don't have direct download URLs
|
url: None,
|
||||||
digest: None,
|
digest: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Resets all Galaxy app state to a fresh install.
|
||||||
|
# Run this before launching galaxy-oss if you want a clean slate.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Resetting Galaxy to fresh state..."
|
||||||
|
|
||||||
|
# Home config/data
|
||||||
|
rm -rf ~/.galaxy
|
||||||
|
|
||||||
|
# macOS Application Support (session history, chat history, workspace state)
|
||||||
|
rm -rf ~/Library/Application\ Support/com.samsung.Galaxy
|
||||||
|
rm -rf ~/Library/Application\ Support/com.samsung.Galaxy-Stable
|
||||||
|
rm -rf ~/Library/Application\ Support/com.samsung.GalaxyAI
|
||||||
|
rm -rf ~/Library/Application\ Support/samsung.galaxy.GalaxyOss
|
||||||
|
rm -rf ~/Library/Application\ Support/samsung.galaxy.GalaxyStable
|
||||||
|
rm -rf ~/Library/Application\ Support/samsung.galaxy.GalaxyDev
|
||||||
|
rm -rf ~/Library/Application\ Support/samsung.galaxy.GalaxyLocal
|
||||||
|
rm -rf ~/Library/Application\ Support/samsung.galaxy.GalaxyPreview
|
||||||
|
|
||||||
|
# macOS Saved Application State (window restoration)
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/com.samsung.Galaxy.savedState
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/com.samsung.Galaxy-Stable.savedState
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/com.samsung.GalaxyAI.savedState
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/samsung.galaxy.GalaxyOss.savedState
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/samsung.galaxy.GalaxyStable.savedState
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/samsung.galaxy.GalaxyDev.savedState
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/samsung.galaxy.GalaxyLocal.savedState
|
||||||
|
|
||||||
|
# macOS Caches
|
||||||
|
rm -rf ~/Library/Caches/com.samsung.Galaxy
|
||||||
|
rm -rf ~/Library/Caches/com.samsung.Galaxy-Stable
|
||||||
|
rm -rf ~/Library/Caches/com.samsung.GalaxyAI
|
||||||
|
rm -rf ~/Library/Caches/samsung.galaxy.GalaxyOss
|
||||||
|
rm -rf ~/Library/Caches/samsung.galaxy.GalaxyStable
|
||||||
|
rm -rf ~/Library/Caches/samsung.galaxy.GalaxyDev
|
||||||
|
rm -rf ~/Library/Caches/samsung.galaxy.GalaxyLocal
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
rm -f ~/Library/Logs/galaxy*.log*
|
||||||
|
|
||||||
|
# macOS Preferences
|
||||||
|
defaults delete com.samsung.Galaxy 2>/dev/null || true
|
||||||
|
defaults delete com.samsung.Galaxy-Stable 2>/dev/null || true
|
||||||
|
defaults delete com.samsung.GalaxyAI 2>/dev/null || true
|
||||||
|
defaults delete samsung.galaxy.GalaxyOss 2>/dev/null || true
|
||||||
|
defaults delete samsung.galaxy.GalaxyStable 2>/dev/null || true
|
||||||
|
defaults delete samsung.galaxy.GalaxyDev 2>/dev/null || true
|
||||||
|
defaults delete samsung.galaxy.GalaxyLocal 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Done. Galaxy will start fresh on next launch."
|
||||||
Reference in New Issue
Block a user