1298 lines
51 KiB
Rust
1298 lines
51 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::mpsc::SyncSender;
|
|
use std::sync::Arc;
|
|
|
|
use ai::index::full_source_code_embedding::manager::{
|
|
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
|
};
|
|
use ai::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
|
|
use ai::workspace::{WorkspaceMetadata, WorkspaceMetadataEvent};
|
|
use anyhow::Context;
|
|
use chrono::Utc;
|
|
#[cfg(feature = "local_fs")]
|
|
use galaxy_core::channel::ChannelState;
|
|
use galaxy_core::features::FeatureFlag;
|
|
#[cfg(feature = "local_fs")]
|
|
use galaxy_util::{local_or_remote_path::LocalOrRemotePath, standardized_path::StandardizedPath};
|
|
#[cfg(feature = "local_fs")]
|
|
use galaxyui::windowing::WindowManager;
|
|
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity};
|
|
use itertools::Itertools;
|
|
use lsp::supported_servers::LSPServerType;
|
|
use lsp::LanguageId;
|
|
#[cfg(feature = "local_fs")]
|
|
use lsp::LspEvent;
|
|
#[cfg(feature = "local_fs")]
|
|
use lsp::{LspManagerModel, LspServerConfig};
|
|
#[cfg(feature = "local_fs")]
|
|
use repo_metadata::repositories::{DetectedRepositories, DetectedRepositoriesEvent};
|
|
#[cfg(feature = "local_fs")]
|
|
use repo_metadata::RepoMetadataModel;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::ai::blocklist::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel};
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::ai::codebase_auto_indexing::{
|
|
auto_index_candidate_roots, should_auto_index_codebase, CodebaseAutoIndexingSurface,
|
|
};
|
|
use crate::ai::metadata_project_rules::read_project_rule_contents;
|
|
use crate::ai::AIRequestUsageModel;
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::code::language_server_shutdown_manager::LanguageServerShutdownManager;
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::code::lsp_telemetry::LspTelemetryEvent;
|
|
use crate::persistence::ModelEvent;
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::server::server_api::ServerApiProvider;
|
|
use crate::settings::CodeSettings;
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::terminal::local_shell::LocalShellState;
|
|
use crate::terminal::TerminalView;
|
|
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
|
|
use crate::{report_if_error, send_telemetry_from_ctx, TelemetryEvent};
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::{view_components::DismissibleToast, workspace::ToastStack};
|
|
|
|
/// Represents whether an LSP server is enabled or disabled for a workspace.
|
|
///
|
|
/// This is also used in underlying sqlite type persistence. We should be careful
|
|
/// not to rename an existing variant, as it will break persistence.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum EnablementState {
|
|
Yes,
|
|
No,
|
|
/// Server was detected as available for a repo but not yet explicitly
|
|
/// enabled/disabled by the user. Entries with this state live only in
|
|
/// memory and are never persisted to SQLite.
|
|
Suggested,
|
|
}
|
|
|
|
/// Describes an LSP operation to be executed after capturing the interactive shell PATH.
|
|
#[cfg(feature = "local_fs")]
|
|
pub enum LspTask {
|
|
/// Install and enable an LSP server for a file path.
|
|
Install {
|
|
file_path: PathBuf,
|
|
repo_root: PathBuf,
|
|
server_type: LSPServerType,
|
|
},
|
|
/// Spawn LSP servers for a file path.
|
|
Spawn { file_path: PathBuf },
|
|
}
|
|
|
|
pub enum LSPEnablementResultForFile {
|
|
Enabled,
|
|
UnsupportedLanguage,
|
|
LSPNotEnabled { root_name: Option<String> },
|
|
}
|
|
|
|
/// Tracks whether an LSP server is relevant/installed/enabled for a repo.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum LspRepoStatus {
|
|
/// LSP is enabled and running (view will set this when subscribed to a live server).
|
|
Ready,
|
|
/// LSP is enabled (we don't block on installation checks when enabled).
|
|
Enabled,
|
|
/// We are checking installation status (only for disabled case).
|
|
CheckingForInstallation,
|
|
/// LSP is disabled and globally installed.
|
|
DisabledAndInstalled { server_type: LSPServerType },
|
|
/// LSP is disabled and not installed.
|
|
DisabledAndNotInstalled { server_type: LSPServerType },
|
|
/// LSP is currently being installed.
|
|
Installing { server_type: LSPServerType },
|
|
}
|
|
|
|
/// Global installation status for an LSP server (across all projects).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum LSPInstallationStatus {
|
|
Installed,
|
|
NotInstalled,
|
|
Checking,
|
|
Installing,
|
|
}
|
|
|
|
impl LspRepoStatus {
|
|
/// Converts an [`LSPInstallationStatus`] (global, per-server-type) into an
|
|
/// [`LspRepoStatus`] (per-repo view of enablement/installation).
|
|
pub fn from_installation_status(
|
|
status: &LSPInstallationStatus,
|
|
server_type: LSPServerType,
|
|
) -> Self {
|
|
match status {
|
|
LSPInstallationStatus::Installed => Self::DisabledAndInstalled { server_type },
|
|
LSPInstallationStatus::NotInstalled => Self::DisabledAndNotInstalled { server_type },
|
|
LSPInstallationStatus::Checking => Self::CheckingForInstallation,
|
|
LSPInstallationStatus::Installing => Self::Installing { server_type },
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Workspace {
|
|
metadata: WorkspaceMetadata,
|
|
language_servers: HashMap<LSPServerType, EnablementState>,
|
|
}
|
|
|
|
impl Workspace {
|
|
/// Returns `true` if this workspace has been persisted to SQLite.
|
|
///
|
|
/// A workspace created solely from available-server detection will have
|
|
/// all metadata timestamps set to `None` and is considered non-persisted.
|
|
fn is_persisted(&self) -> bool {
|
|
let persisted = self.metadata.navigated_ts.is_some()
|
|
|| self.metadata.modified_ts.is_some()
|
|
|| self.metadata.queried_ts.is_some();
|
|
|
|
if !persisted {
|
|
debug_assert!(
|
|
self.language_servers
|
|
.values()
|
|
.all(|s| *s == EnablementState::Suggested),
|
|
"non-persisted workspace has Yes/No server state; persist metadata first"
|
|
);
|
|
}
|
|
|
|
persisted
|
|
}
|
|
}
|
|
|
|
/// Manages a set of code workspaces that the app recognizes. These workspaces define
|
|
/// the scope of various repo-based code features like codebase indexing, project rules and LSP.
|
|
pub struct PersistedWorkspace {
|
|
workspaces: HashMap<PathBuf, Workspace>,
|
|
model_event_sender: Option<SyncSender<ModelEvent>>,
|
|
/// Global installation status per LSP server type.
|
|
#[cfg(feature = "local_fs")]
|
|
lsp_installation_status: HashMap<LSPServerType, LSPInstallationStatus>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum PersistedWorkspaceEvent {
|
|
/// Emitted when LSP installation status changes.
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
InstallStatusUpdate {
|
|
server_type: LSPServerType,
|
|
status: LSPInstallationStatus,
|
|
},
|
|
/// Emitted when LSP installation completes successfully.
|
|
/// Toast notification is shown directly by PersistedWorkspace.
|
|
/// The server is also spawned automatically by PersistedWorkspace.
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
InstallationSucceeded,
|
|
/// Emitted when LSP installation fails.
|
|
/// Toast notification is shown directly by PersistedWorkspace.
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
InstallationFailed,
|
|
/// Emitted when async detection of available servers for a workspace completes.
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
AvailableServersDetected {
|
|
workspace_path: PathBuf,
|
|
servers: Vec<LSPServerType>,
|
|
},
|
|
/// Emitted when the user explicitly adds a repo via a picker (e.g. the tab-config
|
|
/// params modal's repo dropdown). Subscribers can use this to refresh their list.
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
WorkspaceAdded { path: PathBuf },
|
|
}
|
|
|
|
impl Entity for PersistedWorkspace {
|
|
type Event = PersistedWorkspaceEvent;
|
|
}
|
|
|
|
impl SingletonEntity for PersistedWorkspace {}
|
|
|
|
impl PersistedWorkspace {
|
|
#[cfg(test)]
|
|
pub fn new_for_test(_ctx: &mut ModelContext<Self>) -> Self {
|
|
Self {
|
|
workspaces: HashMap::new(),
|
|
model_event_sender: None,
|
|
#[cfg(feature = "local_fs")]
|
|
lsp_installation_status: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn new(
|
|
metadata: Vec<WorkspaceMetadata>,
|
|
workspace_language_servers: HashMap<PathBuf, HashMap<LSPServerType, EnablementState>>,
|
|
model_event_sender: Option<SyncSender<ModelEvent>>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Self {
|
|
let metadata: HashMap<PathBuf, Workspace> = metadata
|
|
.into_iter()
|
|
.map(|metadata| {
|
|
let path = metadata.path.clone();
|
|
let language_servers = workspace_language_servers
|
|
.get(&path)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
|
|
(
|
|
path,
|
|
Workspace {
|
|
metadata,
|
|
language_servers,
|
|
},
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
|
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
|
|
match event {
|
|
CodebaseIndexManagerEvent::IndexMetadataUpdated { root_path, event } => {
|
|
me.handle_index_metadata_event(root_path, *event);
|
|
}
|
|
CodebaseIndexManagerEvent::NewIndexCreated { .. } => {
|
|
send_active_indexed_repos_changed_telemetry(ctx);
|
|
}
|
|
CodebaseIndexManagerEvent::RemoveExpiredIndexMetadata { expired_metadata } => {
|
|
// TODO: Disable expired metadata removal once we have other consumers of the workspace metadata.
|
|
me.clean_up_expired_metadata(expired_metadata.clone(), ctx);
|
|
send_active_indexed_repos_changed_telemetry(ctx);
|
|
}
|
|
_ => {}
|
|
}
|
|
});
|
|
|
|
// Subscribe to AI conversation events to trigger incremental sync
|
|
ctx.subscribe_to_model(
|
|
&BlocklistAIHistoryModel::handle(ctx),
|
|
|me, _, event, ctx| {
|
|
if let BlocklistAIHistoryEvent::StartedNewConversation {
|
|
terminal_surface_id,
|
|
..
|
|
} = event
|
|
{
|
|
#[cfg(feature = "local_fs")]
|
|
me.clean_up_deleted_indices(ctx);
|
|
|
|
me.trigger_incremental_sync_for_conversation(*terminal_surface_id, ctx);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Subscribe to changes in workspace settings.
|
|
ctx.subscribe_to_model(
|
|
&UserWorkspaces::handle(ctx),
|
|
|me, _, user_workspaces_event, ctx| {
|
|
if let UserWorkspacesEvent::CodebaseContextEnablementChanged =
|
|
user_workspaces_event
|
|
{
|
|
me.on_settings_changed(ctx);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Subscribe to ProjectContextModel events to persist rule changes
|
|
ctx.subscribe_to_model(&ProjectContextModel::handle(ctx), |me, _, event, _ctx| {
|
|
if let ProjectContextModelEvent::KnownRulesChanged(delta) = event {
|
|
let mut events = vec![];
|
|
|
|
if !delta.discovered_rules.is_empty() {
|
|
events.push(ModelEvent::UpsertProjectRules {
|
|
project_rule_paths: delta.discovered_rules.clone(),
|
|
});
|
|
}
|
|
|
|
if !delta.deleted_rules.is_empty() {
|
|
events.push(ModelEvent::DeleteProjectRules {
|
|
path: delta.deleted_rules.clone(),
|
|
});
|
|
}
|
|
|
|
if !events.is_empty() {
|
|
me.save_to_db(events);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
if !cfg!(any(
|
|
test,
|
|
feature = "fast_dev",
|
|
feature = "integration_tests"
|
|
)) && CodebaseIndexManager::as_ref(ctx).is_indexing_enabled()
|
|
{
|
|
ctx.subscribe_to_model(&DetectedRepositories::handle(ctx), |me, _, event, ctx| {
|
|
let DetectedRepositoriesEvent::DetectedGitRepo { repository, .. } = event;
|
|
let repo_path = repository.as_ref(ctx).root_dir().to_local_path_lossy();
|
|
|
|
me.index_repo(repo_path, ctx);
|
|
});
|
|
}
|
|
|
|
// Collect workspace paths before metadata is moved into Self.
|
|
#[cfg(feature = "local_fs")]
|
|
let startup_workspace_paths: Vec<PathBuf> = metadata.keys().cloned().collect();
|
|
|
|
#[allow(unused_mut)]
|
|
let mut result = Self {
|
|
workspaces: metadata,
|
|
model_event_sender,
|
|
#[cfg(feature = "local_fs")]
|
|
lsp_installation_status: HashMap::new(),
|
|
};
|
|
|
|
// Kick off LSP suggestion scanning for all existing workspaces so that
|
|
// the available-server state is fresh by the time any footer is created.
|
|
// We pass skip_cached=true so workspaces with persisted entries are still
|
|
// re-scanned to discover newly relevant server types.
|
|
#[cfg(feature = "local_fs")]
|
|
if !cfg!(any(
|
|
test,
|
|
feature = "fast_dev",
|
|
feature = "integration_tests"
|
|
)) && !startup_workspace_paths.is_empty()
|
|
{
|
|
result.detect_available_servers_for_workspaces(startup_workspace_paths, true, ctx);
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Given a repo path, enables the specified LSP server. If the workspace doesn't exist, it will be created.
|
|
pub fn enable_lsp_server_for_path(&mut self, path: &Path, server_type: LSPServerType) {
|
|
self.set_lsp_server_for_path(path, server_type, EnablementState::Yes);
|
|
}
|
|
|
|
/// Given a repo path, disables the specified LSP server.
|
|
pub fn disable_lsp_server_for_path(&mut self, path: &Path, server_type: LSPServerType) {
|
|
self.set_lsp_server_for_path(path, server_type, EnablementState::No);
|
|
}
|
|
|
|
/// Returns the enabled LSP server type (if any) for this file path.
|
|
pub fn has_enabled_lsp_server_for_file_path(&self, path: &Path) -> LSPEnablementResultForFile {
|
|
let Some(language_id) = LanguageId::from_path(path) else {
|
|
return LSPEnablementResultForFile::UnsupportedLanguage;
|
|
};
|
|
let Some(root) = self.root_for_workspace(path) else {
|
|
return LSPEnablementResultForFile::LSPNotEnabled { root_name: None };
|
|
};
|
|
let Some(workspace) = self.workspaces.get(root) else {
|
|
return LSPEnablementResultForFile::LSPNotEnabled {
|
|
root_name: root
|
|
.file_name()
|
|
.and_then(|s| s.to_str())
|
|
.map(|s| s.to_string()),
|
|
};
|
|
};
|
|
|
|
for (language_server, enablement) in &workspace.language_servers {
|
|
if *enablement == EnablementState::Yes
|
|
&& language_server.languages().contains(&language_id)
|
|
{
|
|
return LSPEnablementResultForFile::Enabled;
|
|
}
|
|
}
|
|
|
|
LSPEnablementResultForFile::LSPNotEnabled {
|
|
root_name: root
|
|
.file_name()
|
|
.and_then(|s| s.to_str())
|
|
.map(|s| s.to_string()),
|
|
}
|
|
}
|
|
|
|
/// Internal method to set LSP server state for a path.
|
|
fn set_lsp_server_for_path(
|
|
&mut self,
|
|
path: &Path,
|
|
server_type: LSPServerType,
|
|
state: EnablementState,
|
|
) {
|
|
// Check if the workspace needs to be persisted before we take a
|
|
// mutable borrow, so we can call save_to_db without conflicting borrows.
|
|
let needs_persist = self
|
|
.workspaces
|
|
.get(path)
|
|
.is_some_and(|ws| !ws.is_persisted());
|
|
|
|
if needs_persist {
|
|
// Materialize the workspace: set a timestamp and persist metadata
|
|
// so the FK-dependent workspace_language_server row can be written.
|
|
let workspace = self.workspaces.get_mut(path).unwrap();
|
|
workspace.metadata.modified_ts = Some(Utc::now());
|
|
let metadata = workspace.metadata.clone();
|
|
self.save_to_db(vec![ModelEvent::UpsertCodebaseIndexMetadata {
|
|
index_metadata: Box::new(metadata),
|
|
}]);
|
|
}
|
|
|
|
match self.workspaces.get_mut(path) {
|
|
Some(workspace) => {
|
|
workspace.language_servers.insert(server_type, state);
|
|
}
|
|
None => {
|
|
let metadata = WorkspaceMetadata {
|
|
path: path.to_path_buf(),
|
|
navigated_ts: None,
|
|
// Consider creation as a modification event.
|
|
modified_ts: Some(Utc::now()),
|
|
queried_ts: None,
|
|
};
|
|
|
|
self.save_to_db(vec![ModelEvent::UpsertCodebaseIndexMetadata {
|
|
index_metadata: Box::new(metadata.clone()),
|
|
}]);
|
|
|
|
self.workspaces.insert(
|
|
path.to_path_buf(),
|
|
Workspace {
|
|
metadata,
|
|
language_servers: HashMap::from([(server_type, state)]),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// Persist the language server setting to database
|
|
self.save_to_db(vec![ModelEvent::UpsertWorkspaceLanguageServer {
|
|
workspace_path: path.to_path_buf(),
|
|
lsp_type: server_type,
|
|
enabled: state,
|
|
}]);
|
|
}
|
|
|
|
pub fn root_for_workspace<'a>(&self, path: &'a Path) -> Option<&'a Path> {
|
|
path.ancestors()
|
|
.find(|&path| self.workspaces.contains_key(path))
|
|
}
|
|
|
|
/// Returns the enabled lsp servers for a given repo path.
|
|
pub fn enabled_lsp_servers(
|
|
&self,
|
|
path: &Path,
|
|
) -> Option<impl Iterator<Item = LSPServerType> + use<'_>> {
|
|
let root = self.root_for_workspace(path)?;
|
|
|
|
self.workspaces.get(root).map(|workspace| {
|
|
workspace
|
|
.language_servers
|
|
.iter()
|
|
.filter_map(|(server_type, state)| {
|
|
if *state == EnablementState::Yes {
|
|
Some(*server_type)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Returns LSP servers for a given workspace path.
|
|
///
|
|
/// When `include_suggested` is `false`, only persisted entries (`Yes`/`No`)
|
|
/// are returned. When `true`, in-memory `Suggested` entries are included as
|
|
/// well (useful for showing available-for-download servers in the UI).
|
|
pub fn all_lsp_servers(
|
|
&self,
|
|
path: &Path,
|
|
include_suggested: bool,
|
|
) -> Option<impl Iterator<Item = (LSPServerType, EnablementState)> + use<'_>> {
|
|
let root = self.root_for_workspace(path)?;
|
|
|
|
self.workspaces.get(root).map(move |workspace| {
|
|
workspace
|
|
.language_servers
|
|
.iter()
|
|
.filter(move |(_, state)| {
|
|
include_suggested || **state != EnablementState::Suggested
|
|
})
|
|
.map(|(server_type, state)| (*server_type, *state))
|
|
})
|
|
}
|
|
|
|
/// Asynchronously detects which LSP server types are relevant for the given workspaces
|
|
/// by calling `should_suggest_for_repo` on each `LSPServerType`. Results are stored
|
|
/// as `Suggested` entries in the workspaces map and emitted via `AvailableServersDetected`.
|
|
///
|
|
/// Workspaces that already have language server entries are skipped (results emitted
|
|
/// immediately) unless `skip_cached` is true, in which case all workspaces are scanned
|
|
/// unconditionally. The workspaces to scan share a single background task and one
|
|
/// interactive PATH capture.
|
|
#[cfg(feature = "local_fs")]
|
|
pub fn detect_available_servers_for_workspaces(
|
|
&mut self,
|
|
workspace_paths: Vec<PathBuf>,
|
|
skip_cached: bool,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
// Workspaces that already have entries get an immediate emit; the rest need scanning.
|
|
// When skip_cached is true (initial startup), always scan to pick up new server types.
|
|
let mut paths_to_scan = Vec::new();
|
|
for workspace_path in workspace_paths {
|
|
if !skip_cached {
|
|
if let Some(workspace) = self.workspaces.get(&workspace_path) {
|
|
if !workspace.language_servers.is_empty() {
|
|
let servers: Vec<LSPServerType> =
|
|
workspace.language_servers.keys().copied().collect();
|
|
ctx.emit(PersistedWorkspaceEvent::AvailableServersDetected {
|
|
workspace_path,
|
|
servers,
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
paths_to_scan.push(workspace_path);
|
|
}
|
|
|
|
if paths_to_scan.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// Get interactive PATH for should_suggest_for_repo checks
|
|
let path_future = LocalShellState::handle(ctx).update(ctx, |shell_state, ctx| {
|
|
shell_state.get_interactive_path_env_var(ctx)
|
|
});
|
|
let http_client = ServerApiProvider::as_ref(ctx).get_http_client();
|
|
|
|
ctx.spawn(
|
|
async move {
|
|
let path_env_var = path_future.await;
|
|
let executor = lsp::CommandBuilder::new(path_env_var);
|
|
|
|
let mut results: Vec<(PathBuf, Vec<LSPServerType>)> = Vec::new();
|
|
for workspace_path in paths_to_scan {
|
|
let mut suggested = Vec::new();
|
|
for server_type in LSPServerType::all() {
|
|
let candidate = server_type.candidate(http_client.clone());
|
|
if candidate
|
|
.should_suggest_for_repo(&workspace_path, &executor)
|
|
.await
|
|
{
|
|
suggested.push(server_type);
|
|
}
|
|
}
|
|
if !suggested.is_empty() {
|
|
results.push((workspace_path, suggested));
|
|
}
|
|
}
|
|
results
|
|
},
|
|
move |me, results, ctx| {
|
|
for (workspace_path, servers) in results {
|
|
// Insert Suggested entries into the workspace, without
|
|
// overwriting existing Yes/No entries.
|
|
let workspace =
|
|
me.workspaces
|
|
.entry(workspace_path.clone())
|
|
.or_insert_with(|| Workspace {
|
|
metadata: WorkspaceMetadata {
|
|
path: workspace_path.clone(),
|
|
navigated_ts: None,
|
|
modified_ts: None,
|
|
queried_ts: None,
|
|
},
|
|
language_servers: HashMap::new(),
|
|
});
|
|
|
|
for &server_type in &servers {
|
|
workspace
|
|
.language_servers
|
|
.entry(server_type)
|
|
.or_insert(EnablementState::Suggested);
|
|
}
|
|
|
|
ctx.emit(PersistedWorkspaceEvent::AvailableServersDetected {
|
|
workspace_path,
|
|
servers,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Returns the total count of LSP servers across all workspaces.
|
|
///
|
|
/// When `include_suggested` is `false`, only persisted entries (`Yes`/`No`)
|
|
/// are counted. When `true`, in-memory `Suggested` entries are counted too.
|
|
pub fn total_lsp_server_count(&self, include_suggested: bool) -> usize {
|
|
self.workspaces
|
|
.values()
|
|
.map(|workspace| {
|
|
workspace
|
|
.language_servers
|
|
.values()
|
|
.filter(|state| include_suggested || **state != EnablementState::Suggested)
|
|
.count()
|
|
})
|
|
.sum()
|
|
}
|
|
|
|
fn on_settings_changed(&mut self, ctx: &mut ModelContext<Self>) {
|
|
Self::maybe_enable_codebase_indexing(ctx);
|
|
}
|
|
|
|
pub fn on_user_changed(&self, ctx: &mut ModelContext<Self>) {
|
|
Self::maybe_enable_codebase_indexing(ctx);
|
|
}
|
|
|
|
/// Enables or disables codebase indexing according to the setting.
|
|
fn maybe_enable_codebase_indexing(ctx: &mut ModelContext<Self>) {
|
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
|
if !manager.is_indexing_enabled() {
|
|
return;
|
|
}
|
|
let codebase_context_enabled =
|
|
UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx);
|
|
if codebase_context_enabled {
|
|
Self::enable_codebase_indexing(manager, ctx);
|
|
} else {
|
|
manager.reset_codebase_indexing(ctx);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn enable_codebase_indexing(
|
|
manager: &mut CodebaseIndexManager,
|
|
ctx: &mut ModelContext<CodebaseIndexManager>,
|
|
) {
|
|
let request_model = AIRequestUsageModel::handle(ctx);
|
|
let codebase_limits = request_model.as_ref(ctx).codebase_context_limits();
|
|
manager.update_max_limits(
|
|
codebase_limits.max_indices_allowed,
|
|
codebase_limits.max_files_per_repo,
|
|
codebase_limits.embedding_generation_batch_size,
|
|
ctx,
|
|
);
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
if should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx) {
|
|
let roots = all_working_directories(ctx).into_iter().filter_map(|dir| {
|
|
DetectedRepositories::as_ref(ctx)
|
|
.get_root_for_path(&LocalOrRemotePath::Local(dir))
|
|
.and_then(|root| root.to_local_path().map(Path::to_path_buf))
|
|
});
|
|
for root in auto_index_candidate_roots(roots, |_| true) {
|
|
manager.index_directory(root, ctx);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
|
fn index_repo(&self, directory_path: PathBuf, ctx: &mut ModelContext<Self>) {
|
|
ProjectContextModel::handle(ctx).update(ctx, |model, ctx| {
|
|
let _ = model.index_and_store_rules(
|
|
directory_path.clone(),
|
|
read_project_rule_contents,
|
|
ctx,
|
|
);
|
|
});
|
|
if FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
|
&& UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx)
|
|
&& *CodeSettings::as_ref(ctx).auto_indexing_enabled
|
|
{
|
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
|
manager.index_directory(directory_path, ctx);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Explicitly registers a directory as a workspace, as if the user had navigated there.
|
|
///
|
|
/// Creates or updates the entry with `navigated_ts = now`, persists to SQLite,
|
|
/// starts full repo-metadata indexing before triggering project-rules and codebase-index
|
|
/// scanning, and emits
|
|
/// [`PersistedWorkspaceEvent::WorkspaceAdded`] so subscribers can refresh their UI.
|
|
pub fn user_added_workspace(&mut self, path: PathBuf, ctx: &mut ModelContext<Self>) {
|
|
let now = Utc::now();
|
|
|
|
match self.workspaces.get_mut(&path) {
|
|
Some(workspace) => {
|
|
workspace.metadata.navigated_ts = Some(now);
|
|
}
|
|
None => {
|
|
self.workspaces.insert(
|
|
path.clone(),
|
|
Workspace {
|
|
metadata: WorkspaceMetadata {
|
|
path: path.clone(),
|
|
navigated_ts: Some(now),
|
|
modified_ts: None,
|
|
queried_ts: None,
|
|
},
|
|
language_servers: HashMap::new(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
self.persist_metadata_for_index(&path);
|
|
#[cfg(feature = "local_fs")]
|
|
match StandardizedPath::from_local_canonicalized(&path) {
|
|
Ok(path) => {
|
|
if let Err(error) = RepoMetadataModel::handle(ctx).update(ctx, |model, ctx| {
|
|
model.index_local_directory_path(&path, ctx)
|
|
}) {
|
|
log::warn!("Failed to start full repo metadata indexing for {path}: {error}");
|
|
}
|
|
}
|
|
Err(error) => {
|
|
log::warn!(
|
|
"Failed to canonicalize user-added workspace {} for full repo metadata indexing: {error}",
|
|
path.display()
|
|
);
|
|
}
|
|
}
|
|
self.index_repo(path.clone(), ctx);
|
|
ctx.emit(PersistedWorkspaceEvent::WorkspaceAdded { path });
|
|
}
|
|
|
|
pub fn workspaces<'a>(&'a self) -> impl Iterator<Item = WorkspaceMetadata> + use<'a> {
|
|
self.workspaces
|
|
.values()
|
|
.filter(|workspace| workspace.is_persisted())
|
|
.map(|workspace| workspace.metadata.clone())
|
|
.sorted_by(WorkspaceMetadata::most_recently_touched)
|
|
.dedup_by(|a, b| a.path == b.path)
|
|
}
|
|
|
|
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
|
pub fn navigated_to_path(&mut self, directory: &PathBuf) {
|
|
if let Some(workspace) = self.workspaces.get_mut(directory) {
|
|
workspace.metadata.navigated_ts = Some(Utc::now());
|
|
self.persist_metadata_for_index(directory);
|
|
}
|
|
}
|
|
|
|
fn handle_index_metadata_event(&mut self, root_path: &PathBuf, event: WorkspaceMetadataEvent) {
|
|
match event {
|
|
WorkspaceMetadataEvent::Queried => {
|
|
if let Some(workspace) = self.workspaces.get_mut(root_path) {
|
|
workspace.metadata.queried_ts = Some(Utc::now());
|
|
}
|
|
self.persist_metadata_for_index(root_path);
|
|
}
|
|
WorkspaceMetadataEvent::Modified => {
|
|
if let Some(workspace) = self.workspaces.get_mut(root_path) {
|
|
workspace.metadata.modified_ts = Some(Utc::now());
|
|
}
|
|
self.persist_metadata_for_index(root_path);
|
|
}
|
|
WorkspaceMetadataEvent::Created => {
|
|
let new_metadata = WorkspaceMetadata {
|
|
path: root_path.clone(),
|
|
navigated_ts: None,
|
|
// Count creation as a modification event.
|
|
modified_ts: Some(Utc::now()),
|
|
queried_ts: None,
|
|
};
|
|
|
|
if let Some(existing) = self.workspaces.get_mut(root_path) {
|
|
// Preserve existing language server settings when re-creating
|
|
// workspace metadata (e.g. after an expired index is cleaned up
|
|
// and the user navigates back to the same directory).
|
|
existing.metadata = new_metadata;
|
|
} else {
|
|
self.workspaces.insert(
|
|
root_path.clone(),
|
|
Workspace {
|
|
metadata: new_metadata,
|
|
language_servers: HashMap::new(),
|
|
},
|
|
);
|
|
}
|
|
self.persist_metadata_for_index(root_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn workspace_for_path(&self, root_path: &Path) -> Option<WorkspaceMetadata> {
|
|
self.workspaces
|
|
.get(root_path)
|
|
.map(|workspace| workspace.metadata.clone())
|
|
}
|
|
|
|
fn persist_metadata_for_index(&self, path: &PathBuf) {
|
|
log::info!("Saving workspace metadata for {path:?} to SQLite");
|
|
|
|
if let Some(single_metadata) = self.workspace_for_path(path) {
|
|
self.save_to_db(vec![ModelEvent::UpsertCodebaseIndexMetadata {
|
|
index_metadata: Box::new(single_metadata),
|
|
}]);
|
|
}
|
|
}
|
|
|
|
/// Triggers an incremental sync for the codebase context when a new conversation starts.
|
|
/// This ensures that the codebase index is up-to-date before the conversation begins.
|
|
fn trigger_incremental_sync_for_conversation(
|
|
&mut self,
|
|
terminal_view_id: galaxyui::EntityId,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
if !UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx) {
|
|
return;
|
|
}
|
|
|
|
// Get the current working directory for the terminal view that started the conversation
|
|
// Collect window IDs first to avoid borrowing conflicts
|
|
let window_ids: Vec<_> = ctx.window_ids().collect();
|
|
|
|
for window_id in window_ids {
|
|
let terminal_views = ctx.views_of_type::<TerminalView>(window_id);
|
|
|
|
for terminal_view in terminal_views.into_iter().flatten() {
|
|
let terminal_view_ref = terminal_view.as_ref(ctx);
|
|
if terminal_view_ref.view_id() == terminal_view_id {
|
|
if terminal_view_ref.active_session_is_local(ctx) != Some(true) {
|
|
log::info!(
|
|
"Skipping local codebase incremental sync for non-local agent conversation"
|
|
);
|
|
return;
|
|
}
|
|
|
|
let pwd = terminal_view_ref.pwd();
|
|
if let Some(pwd) = pwd {
|
|
let directory_path = PathBuf::from(pwd);
|
|
|
|
// Trigger an incremental sync through the CodebaseIndexManager
|
|
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
|
if let Err(e) = codebase_manager
|
|
.trigger_incremental_sync_for_path(&directory_path, ctx)
|
|
{
|
|
log::warn!("Failed to trigger incremental sync {e}");
|
|
}
|
|
});
|
|
}
|
|
return; // Found the terminal view, exit both loops
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn clean_up_expired_metadata(
|
|
&self,
|
|
indices_to_remove: Arc<Vec<PathBuf>>,
|
|
_ctx: &mut ModelContext<Self>,
|
|
) {
|
|
log::info!("Cleaning up index metadata from SQLite");
|
|
|
|
let indices_to_remove = indices_to_remove.as_ref();
|
|
self.save_to_db(indices_to_remove.iter().filter_map(|path| {
|
|
let Some(ws) = self.workspaces.get(path) else {
|
|
return Some(ModelEvent::DeleteCodebaseIndexMetadata {
|
|
repo_path: path.to_path_buf(),
|
|
});
|
|
};
|
|
|
|
// Skip non-persisted workspaces — they have no DB row to delete.
|
|
if !ws.is_persisted() {
|
|
return None;
|
|
}
|
|
|
|
// Don't delete workspace metadata rows for workspaces that have
|
|
// persisted LSP server settings (Yes/No).
|
|
//
|
|
// Deleting workspace_metadata rows would orphan corresponding
|
|
// workspace_language_server rows (FK'd without ON DELETE CASCADE).
|
|
// On next app load, the inner_join used to load workspace language
|
|
// servers will silently drop orphaned rows, making enabled
|
|
// language servers appear disabled.
|
|
let has_persisted_servers = ws
|
|
.language_servers
|
|
.values()
|
|
.any(|s| *s != EnablementState::Suggested);
|
|
if has_persisted_servers {
|
|
return None;
|
|
}
|
|
|
|
Some(ModelEvent::DeleteCodebaseIndexMetadata {
|
|
repo_path: path.to_path_buf(),
|
|
})
|
|
}));
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
fn clean_up_deleted_indices(&self, ctx: &mut ModelContext<Self>) {
|
|
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
|
codebase_manager.clean_up_deleted_indices(ctx);
|
|
});
|
|
}
|
|
|
|
fn save_to_db(&self, events: impl IntoIterator<Item = ModelEvent>) {
|
|
let model_event_sender = self.model_event_sender.clone();
|
|
if let Some(model_event_sender) = &model_event_sender {
|
|
for event in events {
|
|
report_if_error!(model_event_sender
|
|
.send(event)
|
|
.with_context(|| "Unable to save codebase index metadata to sqlite"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Installs the LSP server for the given file path and enables it.
|
|
/// This is used when the server is not yet installed.
|
|
#[cfg(feature = "local_fs")]
|
|
fn handle_install_lsp(
|
|
&mut self,
|
|
file_path: PathBuf,
|
|
repo_root: PathBuf,
|
|
server_type: LSPServerType,
|
|
path_env_var: Option<String>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
// Early return if already installing to prevent duplicate installations from repeated clicks
|
|
if self.lsp_installation_status.get(&server_type)
|
|
== Some(&LSPInstallationStatus::Installing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Set Installing state before spawning async installation
|
|
self.lsp_installation_status
|
|
.insert(server_type, LSPInstallationStatus::Installing);
|
|
ctx.emit(PersistedWorkspaceEvent::InstallStatusUpdate {
|
|
server_type,
|
|
status: LSPInstallationStatus::Installing,
|
|
});
|
|
|
|
let repo_root_clone = repo_root.clone();
|
|
let file_path_clone = file_path.clone();
|
|
let executor = lsp::CommandBuilder::new(path_env_var);
|
|
let http_client = ServerApiProvider::as_ref(ctx).get_http_client();
|
|
ctx.spawn(
|
|
async move {
|
|
let candidate = server_type.candidate(http_client);
|
|
let metadata = candidate.fetch_latest_server_metadata().await?;
|
|
candidate.install(metadata, &executor).await?;
|
|
Ok::<_, anyhow::Error>(())
|
|
},
|
|
move |me, result, ctx| match result {
|
|
Ok(()) => {
|
|
// Enable the LSP server
|
|
me.enable_lsp_server_for_path(&repo_root_clone, server_type);
|
|
|
|
// Update installation status cache
|
|
me.lsp_installation_status
|
|
.insert(server_type, LSPInstallationStatus::Installed);
|
|
|
|
// Show success toast
|
|
if let Some(window_id) = WindowManager::as_ref(ctx).active_window() {
|
|
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
|
toast_stack.add_ephemeral_toast(
|
|
DismissibleToast::success(format!(
|
|
"{} installed and enabled successfully.",
|
|
server_type.binary_name()
|
|
)),
|
|
window_id,
|
|
ctx,
|
|
);
|
|
});
|
|
}
|
|
|
|
ctx.emit(PersistedWorkspaceEvent::InstallationSucceeded);
|
|
|
|
// Also emit status update so listeners can update their UI
|
|
ctx.emit(PersistedWorkspaceEvent::InstallStatusUpdate {
|
|
server_type,
|
|
status: LSPInstallationStatus::Installed,
|
|
});
|
|
|
|
// Spawn the server now that it's installed and enabled.
|
|
// This is done here so it happens exactly once, rather
|
|
// than relying on each subscriber to spawn independently.
|
|
me.execute_lsp_task(
|
|
LspTask::Spawn {
|
|
file_path: file_path_clone,
|
|
},
|
|
ctx,
|
|
);
|
|
}
|
|
Err(e) => {
|
|
log::info!("Failed to install LSP server: {e}");
|
|
|
|
// Update installation status to NotInstalled
|
|
me.lsp_installation_status
|
|
.insert(server_type, LSPInstallationStatus::NotInstalled);
|
|
|
|
// Show error toast
|
|
if let Some(window_id) = WindowManager::as_ref(ctx).active_window() {
|
|
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
|
toast_stack.add_ephemeral_toast(
|
|
DismissibleToast::error(format!(
|
|
"Failed to install {}: {}",
|
|
server_type.binary_name(),
|
|
e
|
|
)),
|
|
window_id,
|
|
ctx,
|
|
);
|
|
});
|
|
}
|
|
|
|
ctx.emit(PersistedWorkspaceEvent::InstallationFailed);
|
|
|
|
// Also emit status update so listeners can update their UI
|
|
ctx.emit(PersistedWorkspaceEvent::InstallStatusUpdate {
|
|
server_type,
|
|
status: LSPInstallationStatus::NotInstalled,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Starts all enabled LSP servers for the given file path.
|
|
/// This looks up the workspace root and starts any servers that are enabled but not yet running.
|
|
#[cfg(feature = "local_fs")]
|
|
fn handle_spawn_lsp(
|
|
&self,
|
|
file_path: &Path,
|
|
path_env_var: Option<String>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
let Some(workspace_root) = self.root_for_workspace(file_path) else {
|
|
return;
|
|
};
|
|
|
|
let Some(servers) = self.enabled_lsp_servers(workspace_root) else {
|
|
return;
|
|
};
|
|
|
|
let supported_servers = servers.collect::<Vec<LSPServerType>>();
|
|
|
|
if supported_servers.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let mut new_servers_available_to_start = false;
|
|
let workspace_root = workspace_root.to_path_buf();
|
|
|
|
for server in supported_servers {
|
|
if LspManagerModel::as_ref(ctx).server_registered_and_started(
|
|
&workspace_root,
|
|
server,
|
|
ctx,
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
log::info!(
|
|
"Starting {} LSP server for {}",
|
|
server.binary_name(),
|
|
workspace_root.display()
|
|
);
|
|
let log_relative_path =
|
|
crate::code::lsp_logs::relative_log_path(server, &workspace_root);
|
|
let http_client = ServerApiProvider::as_ref(ctx).get_http_client();
|
|
let config = LspServerConfig::new(
|
|
server,
|
|
workspace_root.clone(),
|
|
path_env_var.clone(),
|
|
ChannelState::app_id().application_name().to_string(),
|
|
http_client,
|
|
)
|
|
.with_log_relative_path(log_relative_path);
|
|
|
|
LspManagerModel::handle(ctx).update(ctx, |manager, m_ctx| {
|
|
manager.register(workspace_root.clone(), config, m_ctx);
|
|
});
|
|
new_servers_available_to_start = true;
|
|
}
|
|
|
|
if !new_servers_available_to_start {
|
|
return;
|
|
}
|
|
|
|
let lsp_manager_handle = LspManagerModel::handle(ctx);
|
|
lsp_manager_handle.update(ctx, |manager, m_ctx| {
|
|
manager.start_all(workspace_root.clone(), m_ctx);
|
|
});
|
|
|
|
// Subscribe to LSP server events to show error toast on failure.
|
|
let workspace_root_display = workspace_root.display().to_string();
|
|
let servers = lsp_manager_handle
|
|
.as_ref(ctx)
|
|
.servers_for_workspace(&workspace_root)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
|
|
for server in servers {
|
|
let workspace_root_display = workspace_root_display.clone();
|
|
let server_type_name = server.as_ref(ctx).server_name();
|
|
ctx.subscribe_to_model(&server, move |_me, _, event, ctx| match event {
|
|
LspEvent::Started => {
|
|
send_telemetry_from_ctx!(
|
|
LspTelemetryEvent::ServerStarted {
|
|
server_type: server_type_name.clone(),
|
|
},
|
|
ctx
|
|
);
|
|
}
|
|
LspEvent::Failed(e) => {
|
|
send_telemetry_from_ctx!(
|
|
LspTelemetryEvent::ServerFailed {
|
|
server_type: server_type_name.clone(),
|
|
},
|
|
ctx
|
|
);
|
|
if let Some(window_id) = WindowManager::as_ref(ctx).active_window()
|
|
{
|
|
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
|
let toast = DismissibleToast::error(format!(
|
|
"Failed to start LSP server for {workspace_root_display} with error {e}",
|
|
));
|
|
toast_stack.add_ephemeral_toast(toast, window_id, ctx);
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
});
|
|
}
|
|
|
|
// Once we start a LSP server, also start the garbage collection process if it is not active.
|
|
LanguageServerShutdownManager::handle(ctx).update(ctx, |shutdown_manager, ctx| {
|
|
if !shutdown_manager.has_in_progress_scan() {
|
|
shutdown_manager.schedule_next_scan(ctx);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Executes an LSP task after capturing the interactive shell PATH.
|
|
/// This is the main entry point for LSP operations that need the full PATH.
|
|
#[cfg(feature = "local_fs")]
|
|
pub fn execute_lsp_task(&mut self, task: LspTask, ctx: &mut ModelContext<Self>) {
|
|
// For Spawn tasks, check synchronously whether there are any enabled LSP
|
|
// servers for this workspace before kicking off the expensive interactive
|
|
// shell PATH capture.
|
|
if let LspTask::Spawn { ref file_path } = task {
|
|
let has_servers = self
|
|
.root_for_workspace(file_path)
|
|
.and_then(|root| self.enabled_lsp_servers(root))
|
|
.is_some_and(|mut servers| servers.next().is_some());
|
|
if !has_servers {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get a future for the interactive PATH
|
|
let path_future = LocalShellState::handle(ctx).update(ctx, |shell_state, ctx| {
|
|
shell_state.get_interactive_path_env_var(ctx)
|
|
});
|
|
|
|
ctx.spawn(path_future, move |me, path_env_var, ctx| match task {
|
|
LspTask::Install {
|
|
file_path,
|
|
repo_root,
|
|
server_type,
|
|
} => {
|
|
me.handle_install_lsp(file_path, repo_root, server_type, path_env_var, ctx);
|
|
}
|
|
LspTask::Spawn { file_path } => {
|
|
me.handle_spawn_lsp(&file_path, path_env_var, ctx);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Kicks off detection (deduped via Checking) and returns the best immediate status.
|
|
/// Uses the interactive shell PATH for detection to ensure gopls and other tools
|
|
/// installed in user-specific locations (like ~/go/bin) are found.
|
|
///
|
|
/// Logic:
|
|
/// 1. If enabled for repo => Enabled
|
|
/// 2. If not enabled and Installed => DisabledAndInstalled
|
|
/// 3. If NotInstalled => DisabledAndNotInstalled
|
|
/// 4. If Installing => Installing
|
|
/// 5. If Checking or Unknown => set Checking, start detection, return CheckingForInstallation
|
|
#[cfg(feature = "local_fs")]
|
|
pub fn detect_lsp_workspace_status(
|
|
&mut self,
|
|
repo_root: PathBuf,
|
|
server_type: LSPServerType,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> LspRepoStatus {
|
|
// Determine enablement
|
|
let is_enabled = self
|
|
.enabled_lsp_servers(&repo_root)
|
|
.map(|mut it| it.any(|s| s == server_type))
|
|
.unwrap_or(false);
|
|
|
|
// If enabled, do not check installation.
|
|
if is_enabled {
|
|
return LspRepoStatus::Enabled;
|
|
}
|
|
|
|
match self.lsp_installation_status.get(&server_type).copied() {
|
|
Some(LSPInstallationStatus::Installed) => {
|
|
LspRepoStatus::DisabledAndInstalled { server_type }
|
|
}
|
|
Some(LSPInstallationStatus::NotInstalled) => {
|
|
LspRepoStatus::DisabledAndNotInstalled { server_type }
|
|
}
|
|
Some(LSPInstallationStatus::Checking) => LspRepoStatus::CheckingForInstallation,
|
|
Some(LSPInstallationStatus::Installing) => LspRepoStatus::Installing { server_type },
|
|
None => {
|
|
// Mark as checking and start async detection with interactive PATH
|
|
self.lsp_installation_status
|
|
.insert(server_type, LSPInstallationStatus::Checking);
|
|
|
|
// Get a future for the interactive PATH
|
|
let path_future = LocalShellState::handle(ctx).update(ctx, |shell_state, ctx| {
|
|
shell_state.get_interactive_path_env_var(ctx)
|
|
});
|
|
|
|
let http_client = ServerApiProvider::as_ref(ctx).get_http_client();
|
|
ctx.spawn(
|
|
async move {
|
|
// Wait for interactive PATH, then check installation
|
|
let path_env_var = path_future.await;
|
|
let executor = lsp::CommandBuilder::new(path_env_var);
|
|
let candidate = server_type.candidate(http_client);
|
|
candidate.is_installed(&executor).await
|
|
},
|
|
move |me, is_installed, ctx| {
|
|
let status = if is_installed {
|
|
LSPInstallationStatus::Installed
|
|
} else {
|
|
LSPInstallationStatus::NotInstalled
|
|
};
|
|
me.lsp_installation_status.insert(server_type, status);
|
|
ctx.emit(PersistedWorkspaceEvent::InstallStatusUpdate {
|
|
server_type,
|
|
status,
|
|
});
|
|
},
|
|
);
|
|
|
|
LspRepoStatus::CheckingForInstallation
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn send_active_indexed_repos_changed_telemetry<T: Entity>(ctx: &mut ModelContext<T>) {
|
|
let total = CodebaseIndexManager::as_ref(ctx).num_active_indices();
|
|
let hit_max = AIRequestUsageModel::as_ref(ctx).hit_codebase_index_limit(total);
|
|
send_telemetry_from_ctx!(
|
|
TelemetryEvent::ActiveIndexedReposChanged {
|
|
updated_number_of_codebase_indices: total,
|
|
hit_max_indices: hit_max
|
|
},
|
|
ctx
|
|
);
|
|
}
|
|
|
|
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
|
pub fn all_working_directories(app: &AppContext) -> HashSet<PathBuf> {
|
|
let mut working_directories = HashSet::new();
|
|
for window_id in app.window_ids() {
|
|
for terminal_view in app
|
|
.views_of_type::<TerminalView>(window_id)
|
|
.into_iter()
|
|
.flatten()
|
|
.map(|handle| handle.as_ref(app))
|
|
{
|
|
let working_directory = terminal_view.pwd();
|
|
if let Some(dir) = working_directory {
|
|
working_directories.insert(dir.into());
|
|
}
|
|
}
|
|
}
|
|
working_directories
|
|
}
|