From 69bf1c2709fc219d2ea9d00e3f4658865dd0acf5 Mon Sep 17 00:00:00 2001 From: GCWing Date: Sat, 22 Aug 2026 23:07:40 +0800 Subject: [PATCH 1/2] refactor: retire the LSP system --- AGENTS-CN.md | 6 +- AGENTS.md | 6 +- CONTRIBUTING.md | 2 +- CONTRIBUTING_CN.md | 2 +- Cargo.lock | 2 - Cargo.toml | 2 +- MiniApp/Skills/miniapp-dev/SKILL.md | 2 +- MiniApp/Skills/miniapp-dev/api-reference.md | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- SECURITY.md | 2 +- SECURITY_CN.md | 2 +- .../external-ai-work-sources-design.md | 15 +- .../opencode-config-assets-adapter-design.md | 16 +- .../opencode-extension-compatibility.md | 6 +- .../opencode-tui-plugin-adapter-design.md | 4 +- docs/architecture/peer-device-mode.md | 6 +- docs/architecture/product-architecture.md | 2 +- .../rust-build-dependency-boundaries.md | 2 +- docs/performance/01-compile-performance.md | 2 +- docs/performance/02-runtime-performance.md | 14 +- docs/plans/core-decomposition-completed.md | 6 +- docs/sdlc-harness/design.md | 2 +- .../research/external-research.md | 2 +- packages/dsh-acp/tests/tool-view.spec.ts | 13 + scripts/check-core-boundaries.test.mjs | 22 - .../cargo-dependency-boundaries.mjs | 4 - .../explicit-test-topology.mjs | 2 - scripts/core-boundaries/rules/crate-rules.mjs | 2 - .../core-boundaries/rules/feature-rules.mjs | 36 +- .../rules/source/forbidden-rules.mjs | 40 - .../rules/source/required-rules.mjs | 131 -- scripts/core-boundaries/self-test.mjs | 24 +- scripts/i18n-contract.test.mjs | 17 +- src/apps/cli/Cargo.toml | 1 - src/apps/cli/src/peer_host/deny.rs | 16 +- src/apps/cli/src/peer_host/dispatch.rs | 26 +- src/apps/desktop/src/api/lsp_api.rs | 429 ---- src/apps/desktop/src/api/lsp_workspace_api.rs | 623 ------ src/apps/desktop/src/api/mod.rs | 2 - src/apps/desktop/src/api/peer_host_invoke.rs | 27 + .../desktop/src/api/remote_connect_api.rs | 1 - .../src/api/remote_workspace_policy.rs | 170 -- src/apps/desktop/src/lib.rs | 49 - src/apps/sdk-host/Cargo.toml | 1 - src/apps/server/src/bootstrap.rs | 5 - src/crates/assembly/core/AGENTS-CN.md | 4 +- src/crates/assembly/core/AGENTS.md | 4 +- src/crates/assembly/core/Cargo.toml | 2 - .../references/appearance-registry.json | 1639 +++++++++++----- .../content/tips/en-US/020_lsp_diagnostics.md | 9 - .../content/tips/zh-CN/020_lsp_diagnostics.md | 9 - .../content/tips/zh-TW/020_lsp_diagnostics.md | 9 - .../core/src/service/lsp/config_watcher.rs | 5 - .../core/src/service/lsp/debouncer.rs | 5 - .../core/src/service/lsp/file_sync.rs | 448 ----- .../assembly/core/src/service/lsp/global.rs | 190 -- .../assembly/core/src/service/lsp/manager.rs | 5 - .../assembly/core/src/service/lsp/mod.rs | 29 - .../core/src/service/lsp/plugin_loader.rs | 6 - .../assembly/core/src/service/lsp/process.rs | 7 - .../core/src/service/lsp/project_detector.rs | 5 - .../assembly/core/src/service/lsp/protocol.rs | 7 - .../assembly/core/src/service/lsp/registry.rs | 8 - .../assembly/core/src/service/lsp/types.rs | 6 - .../core/src/service/lsp/workspace_manager.rs | 1423 -------------- src/crates/assembly/core/src/service/mod.rs | 4 - src/crates/contracts/AGENTS-CN.md | 2 +- src/crates/contracts/AGENTS.md | 2 +- src/crates/contracts/core-types/AGENTS.md | 6 +- src/crates/contracts/core-types/src/lib.rs | 1 - src/crates/contracts/core-types/src/lsp.rs | 424 ---- .../core-types/tests/core_type_contracts.rs | 2 - .../core_type_contracts/lsp_contracts.rs | 62 - src/crates/contracts/events/src/emitter.rs | 16 - src/crates/interfaces/acp/AGENTS-CN.md | 2 +- src/crates/interfaces/acp/AGENTS.md | 2 +- src/crates/interfaces/acp/Cargo.toml | 1 - src/crates/services/AGENTS-CN.md | 6 +- src/crates/services/AGENTS.md | 8 +- src/crates/services/services-core/AGENTS.md | 14 +- src/crates/services/services-core/Cargo.toml | 17 - src/crates/services/services-core/src/lib.rs | 2 - src/crates/services/services-core/src/lsp.rs | 262 --- .../services-core/src/lsp/config_watcher.rs | 127 -- .../services-core/src/lsp/debouncer.rs | 86 - .../services/services-core/src/lsp/manager.rs | 751 ------- .../services-core/src/lsp/plugin_loader.rs | 268 --- .../services/services-core/src/lsp/process.rs | 1139 ----------- .../services-core/src/lsp/project_detector.rs | 510 ----- .../services-core/src/lsp/protocol.rs | 220 --- .../tests/lsp_plugin_registry_contracts.rs | 186 -- src/web-ui/LOGGING.md | 2 +- .../components/panels/base/FlexiblePanel.tsx | 2 - .../components/CodeEditor/CodeEditor.tsx | 1 - .../styles/overlay-surfaces.contract.test.ts | 1 - .../api/adapters/peer-device-adapter.test.ts | 1 - .../api/adapters/peer-device-adapter.ts | 1 - .../registry/defaultAppearanceRegistry.ts | 6 - .../config/components/LspConfig.appearance.ts | 6 - .../config/components/LspConfig.scss | 13 - .../config/components/LspConfig.tsx | 180 -- .../infrastructure/i18n/core/I18nService.ts | 1 - .../i18n/presets/namespaceRegistry.ts | 2 - .../core/LanguageDetector.ts | 87 +- .../core/LanguageRegistry.ts | 29 - .../core/ProjectDetector.ts | 500 ----- .../language-detection/index.ts | 13 +- .../language-detection/types/index.ts | 146 +- .../language-detection/utils/helpers.ts | 5 - .../src/infrastructure/peer-device/README.md | 8 +- .../peer-device/deviceSurfaceRouting.ts | 1 - .../peer-device/peerDeviceSurfaceRuntime.ts | 9 - src/web-ui/src/locales/en-US/errors.json | 3 +- src/web-ui/src/locales/en-US/settings.json | 7 - .../src/locales/en-US/settings/editor.json | 2 +- .../src/locales/en-US/settings/lsp.json | 95 - src/web-ui/src/locales/en-US/tools.json | 20 - src/web-ui/src/locales/zh-CN/errors.json | 3 +- src/web-ui/src/locales/zh-CN/settings.json | 7 - .../src/locales/zh-CN/settings/editor.json | 2 +- .../src/locales/zh-CN/settings/lsp.json | 95 - src/web-ui/src/locales/zh-CN/tools.json | 20 - src/web-ui/src/locales/zh-TW/errors.json | 3 +- src/web-ui/src/locales/zh-TW/settings.json | 7 - .../src/locales/zh-TW/settings/editor.json | 2 +- .../src/locales/zh-TW/settings/lsp.json | 95 - src/web-ui/src/locales/zh-TW/tools.json | 20 - .../core/ContextResolver.ts | 48 +- .../providers/EditorMenuProvider.ts | 220 ++- .../types/context.types.ts | 3 + .../src/shared/services/EditorJumpService.ts | 2 +- .../tools/bitfun-canvas/BitfunCanvasPanel.tsx | 1 - .../tools/editor/components/CodeEditor.tsx | 271 +-- .../tools/editor/components/DiffEditor.tsx | 18 - .../components/EditorStatusBar.appearance.ts | 2 +- .../editor/components/EditorStatusBar.scss | 37 - .../editor/components/EditorStatusBar.tsx | 51 +- src/web-ui/src/tools/editor/config/presets.ts | 5 - src/web-ui/src/tools/editor/config/types.ts | 1 - .../src/tools/editor/core/MonacoDiffCore.tsx | 1 - .../tools/editor/core/MonacoEditorCore.tsx | 7 +- src/web-ui/src/tools/editor/core/types.ts | 5 +- .../tools/editor/extensions/LspExtension.ts | 108 -- .../src/tools/editor/extensions/index.ts | 3 - .../src/tools/editor/extensions/types.ts | 12 +- src/web-ui/src/tools/editor/hooks/index.ts | 1 - .../tools/editor/hooks/useLspIntegration.ts | 47 - src/web-ui/src/tools/editor/index.ts | 12 +- .../editor/services/EditorExtensionManager.ts | 1 - .../editor/services/EditorReadyManager.ts | 16 +- src/web-ui/src/tools/index.ts | 1 - src/web-ui/src/tools/initializeTools.ts | 2 - src/web-ui/src/tools/lsp/README.md | 114 -- src/web-ui/src/tools/lsp/appearance.ts | 17 - .../LspPluginList/LspPluginList.scss | 451 ----- .../LspPluginList/LspPluginList.tsx | 236 --- .../ReferencesPanel.appearance.ts | 14 - .../ReferencesPanel/ReferencesPanel.scss | 296 --- .../ReferencesPanel/ReferencesPanel.tsx | 308 --- .../lsp/components/ReferencesPanel/index.ts | 3 - src/web-ui/src/tools/lsp/hooks/useLsp.ts | 106 - .../src/tools/lsp/hooks/useMonacoLsp.ts | 83 - src/web-ui/src/tools/lsp/index.ts | 46 - .../src/tools/lsp/initializeLsp.test.ts | 50 - src/web-ui/src/tools/lsp/initializeLsp.ts | 218 --- .../lsp/services/HoverPositionCalculator.ts | 241 --- .../tools/lsp/services/LspAdapterManager.ts | 186 -- .../tools/lsp/services/LspConfigService.ts | 59 - .../src/tools/lsp/services/LspDiagnostics.ts | 99 - .../tools/lsp/services/LspDocumentService.ts | 326 ---- .../lsp/services/LspExtensionRegistry.ts | 162 -- .../tools/lsp/services/LspRefreshManager.ts | 283 --- .../src/tools/lsp/services/LspService.ts | 309 --- .../tools/lsp/services/MonacoLspAdapter.ts | 1718 ----------------- .../src/tools/lsp/services/MonacoLspBridge.ts | 1 - .../lsp/services/WorkspaceLspInitializer.ts | 178 -- .../lsp/services/WorkspaceLspManager.test.ts | 248 --- .../tools/lsp/services/WorkspaceLspManager.ts | 946 --------- src/web-ui/src/tools/lsp/types/index.ts | 131 -- 180 files changed, 1547 insertions(+), 17242 deletions(-) create mode 100644 packages/dsh-acp/tests/tool-view.spec.ts delete mode 100644 src/apps/desktop/src/api/lsp_api.rs delete mode 100644 src/apps/desktop/src/api/lsp_workspace_api.rs delete mode 100644 src/crates/assembly/core/src/service/announcement/content/tips/en-US/020_lsp_diagnostics.md delete mode 100644 src/crates/assembly/core/src/service/announcement/content/tips/zh-CN/020_lsp_diagnostics.md delete mode 100644 src/crates/assembly/core/src/service/announcement/content/tips/zh-TW/020_lsp_diagnostics.md delete mode 100644 src/crates/assembly/core/src/service/lsp/config_watcher.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/debouncer.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/file_sync.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/global.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/manager.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/mod.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/plugin_loader.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/process.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/project_detector.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/protocol.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/registry.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/types.rs delete mode 100644 src/crates/assembly/core/src/service/lsp/workspace_manager.rs delete mode 100644 src/crates/contracts/core-types/src/lsp.rs delete mode 100644 src/crates/contracts/core-types/tests/core_type_contracts/lsp_contracts.rs delete mode 100644 src/crates/services/services-core/src/lsp.rs delete mode 100644 src/crates/services/services-core/src/lsp/config_watcher.rs delete mode 100644 src/crates/services/services-core/src/lsp/debouncer.rs delete mode 100644 src/crates/services/services-core/src/lsp/manager.rs delete mode 100644 src/crates/services/services-core/src/lsp/plugin_loader.rs delete mode 100644 src/crates/services/services-core/src/lsp/process.rs delete mode 100644 src/crates/services/services-core/src/lsp/project_detector.rs delete mode 100644 src/crates/services/services-core/src/lsp/protocol.rs delete mode 100644 src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs delete mode 100644 src/web-ui/src/infrastructure/config/components/LspConfig.appearance.ts delete mode 100644 src/web-ui/src/infrastructure/config/components/LspConfig.scss delete mode 100644 src/web-ui/src/infrastructure/config/components/LspConfig.tsx delete mode 100644 src/web-ui/src/infrastructure/language-detection/core/ProjectDetector.ts delete mode 100644 src/web-ui/src/locales/en-US/settings/lsp.json delete mode 100644 src/web-ui/src/locales/zh-CN/settings/lsp.json delete mode 100644 src/web-ui/src/locales/zh-TW/settings/lsp.json delete mode 100644 src/web-ui/src/tools/editor/extensions/LspExtension.ts delete mode 100644 src/web-ui/src/tools/editor/hooks/useLspIntegration.ts delete mode 100644 src/web-ui/src/tools/lsp/README.md delete mode 100644 src/web-ui/src/tools/lsp/appearance.ts delete mode 100644 src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss delete mode 100644 src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.tsx delete mode 100644 src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.appearance.ts delete mode 100644 src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss delete mode 100644 src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.tsx delete mode 100644 src/web-ui/src/tools/lsp/components/ReferencesPanel/index.ts delete mode 100644 src/web-ui/src/tools/lsp/hooks/useLsp.ts delete mode 100644 src/web-ui/src/tools/lsp/hooks/useMonacoLsp.ts delete mode 100644 src/web-ui/src/tools/lsp/index.ts delete mode 100644 src/web-ui/src/tools/lsp/initializeLsp.test.ts delete mode 100644 src/web-ui/src/tools/lsp/initializeLsp.ts delete mode 100644 src/web-ui/src/tools/lsp/services/HoverPositionCalculator.ts delete mode 100644 src/web-ui/src/tools/lsp/services/LspAdapterManager.ts delete mode 100644 src/web-ui/src/tools/lsp/services/LspConfigService.ts delete mode 100644 src/web-ui/src/tools/lsp/services/LspDiagnostics.ts delete mode 100644 src/web-ui/src/tools/lsp/services/LspDocumentService.ts delete mode 100644 src/web-ui/src/tools/lsp/services/LspExtensionRegistry.ts delete mode 100644 src/web-ui/src/tools/lsp/services/LspRefreshManager.ts delete mode 100644 src/web-ui/src/tools/lsp/services/LspService.ts delete mode 100644 src/web-ui/src/tools/lsp/services/MonacoLspAdapter.ts delete mode 100644 src/web-ui/src/tools/lsp/services/MonacoLspBridge.ts delete mode 100644 src/web-ui/src/tools/lsp/services/WorkspaceLspInitializer.ts delete mode 100644 src/web-ui/src/tools/lsp/services/WorkspaceLspManager.test.ts delete mode 100644 src/web-ui/src/tools/lsp/services/WorkspaceLspManager.ts delete mode 100644 src/web-ui/src/tools/lsp/types/index.ts diff --git a/AGENTS-CN.md b/AGENTS-CN.md index 3bf286f4fc..df52d88c04 100644 --- a/AGENTS-CN.md +++ b/AGENTS-CN.md @@ -27,9 +27,9 @@ Stable Contracts and Security Control Plane 的边界以 | 1 | 接口与入口层 | `src/apps/*`, `src/web-ui`, `src/mobile-web`, `BitFun-Installer`, `tests/e2e`, `src/crates/interfaces` | 产品宿主、命令、UI 入口、协议接口和跨形态测试 | desktop、CLI、server、relay、Web UI、mobile web、installer、E2E、`acp`、`sdk-host` | 最近的本地 `AGENTS.md`;[interfaces](src/crates/interfaces/AGENTS.md) | | 2 | 产品组装层 | `src/crates/assembly` | 兼容导出、产品能力选择、product-full 接线、不可变内置 Agent 内容、adapter/service 注册和生态无关的来源协调 | `agent-content`, `core`, `external-sources`, `product-capabilities` | [AGENTS.md](src/crates/assembly/AGENTS.md) | | 3 | 适配层 | `src/crates/adapters` | AI/transport/WebDriver 协议 adapter、外部 AI work source adapter(OpenCode/Claude Code/Codex)和外部 provider 转换 | `agent-runtime-ipc`、`ai-adapters`, `opencode-adapter`, `claude-code-adapter`, `codex-adapter`, `static-hook-support`, `transport`, `webdriver` | [AGENTS.md](src/crates/adapters/AGENTS.md) | -| 4 | 服务实现层 | `src/crates/services` | 可复用 OS、filesystem、terminal、MCP、remote、git、watch、process、LSP plugin registry、session persistence primitives、network 和 MiniApp runtime IO 实现 | `services-core`, `services-integrations`, `relay-service`, `page-function-runtime`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | +| 4 | 服务实现层 | `src/crates/services` | 可复用 OS、filesystem、terminal、MCP、remote、git、watch、process、session persistence primitives、network 和 MiniApp runtime IO 实现 | `services-core`, `services-integrations`, `relay-service`, `page-function-runtime`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | | 5 | 执行原语层 | `src/crates/execution` | 可移植 agent、harness、stream、DeepReview policy/report、插件运行时客户端、typed-service、tool-contract、tool-group 和 tool-execution 构件 | `agent-runtime`, `agent-stream`, `tool-contracts`, `harness`, `plugin-runtime-client`, `runtime-services`, `tool-provider-groups`, `tool-execution`, `tool-call-jsonrepair` | [AGENTS.md](src/crates/execution/AGENTS.md) | -| 6 | 稳定契约与产品领域层 | `src/crates/contracts` | 跨层共享 DTO、事件形状、runtime port、LSP protocol/plugin DTO、产品领域契约和策略 | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | +| 6 | 稳定契约与产品领域层 | `src/crates/contracts` | 跨层共享 DTO、事件形状、runtime port、产品领域契约和策略 | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | 边界规则: @@ -37,7 +37,7 @@ Stable Contracts and Security Control Plane 的边界以 - 组装层只接线下层并选择产品能力事实,不实现具体 adapter、OS 或 service 细节。 - 产品特性只在内核能力之上组装用户侧命令、UI contribution、设置和默认策略;长程任务、scheduler、permission、session/workspace、memory、DFX、hook 和 event 事实属于 Agent Kernel owner。 - 适配层翻译协议和外部 provider 形状,不拥有产品能力选择或可复用 OS service 行为。 -- 服务实现层负责可复用的 OS、process、terminal、MCP、remote、git、filesystem、LSP plugin registry、session persistence primitives 和 MiniApp runtime IO 能力。 +- 服务实现层负责可复用的 OS、process、terminal、MCP、remote、git、filesystem、session persistence primitives 和 MiniApp runtime IO 能力。 - 外部系统是边界外资源,不是仓库内层级。只有已注册的 adapter、service 或 app-local provider 应调用它们;其他层消费 port 和稳定契约。 - 执行原语层只放可移植运行时构件,不拥有宿主或交付形态。 - 契约层保持轻行为,不得向上依赖。 diff --git a/AGENTS.md b/AGENTS.md index 048b60ca08..d539209c96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,9 +42,9 @@ Keep crate dependencies inside each layer to the smallest set needed. | 1 | Interfaces and entrypoints | `src/apps/*`, `src/web-ui`, `src/mobile-web`, `BitFun-Installer`, `tests/e2e`, `src/crates/interfaces` | Product hosts, commands, UI entrypoints, protocol interfaces, and cross-surface tests | desktop, CLI, server, relay, Web UI, mobile web, installer, E2E, `acp`, `app-server`, `sdk-host` | nearest local `AGENTS.md`; [interfaces](src/crates/interfaces/AGENTS.md) | | 2 | Product assembly | `src/crates/assembly` | Compatibility exports, product capability selection, product-full wiring, immutable built-in Agent content, adapter/service registration, and ecosystem-neutral source coordination | `agent-content`, `core`, `external-sources`, `product-capabilities` | [AGENTS.md](src/crates/assembly/AGENTS.md) | | 3 | Adapters | `src/crates/adapters` | AI/transport/WebDriver protocol adapters, external AI work source adapters (OpenCode/Claude Code/Codex), and external-provider translation | `agent-runtime-ipc`, `ai-adapters`, `opencode-adapter`, `claude-code-adapter`, `codex-adapter`, `static-hook-support`, `transport`, `webdriver` | [AGENTS.md](src/crates/adapters/AGENTS.md) | -| 4 | Services | `src/crates/services` | Reusable OS, filesystem, terminal, MCP, remote, git, watch, process, LSP plugin registry, session persistence primitives, MiniApp runtime IO, and network implementations | `services-core`, `services-integrations`, `miniapp-market-service`, `relay-service`, `page-function-runtime`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | +| 4 | Services | `src/crates/services` | Reusable OS, filesystem, terminal, MCP, remote, git, watch, process, session persistence primitives, MiniApp runtime IO, and network implementations | `services-core`, `services-integrations`, `miniapp-market-service`, `relay-service`, `page-function-runtime`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | | 5 | Execution primitives | `src/crates/execution` | Portable agent, harness, stream, DeepReview policy/report, plugin runtime client, typed-service, tool-contract, tool-group, and tool-execution building blocks | `agent-runtime`, `agent-stream`, `tool-contracts`, `harness`, `plugin-runtime-client`, `runtime-services`, `tool-provider-groups`, `tool-execution`, `tool-call-jsonrepair` | [AGENTS.md](src/crates/execution/AGENTS.md) | -| 6 | Stable contracts and product domains | `src/crates/contracts` | Shared DTOs, event shapes, runtime ports, LSP protocol/plugin DTOs, and product domain contracts/policies | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | +| 6 | Stable contracts and product domains | `src/crates/contracts` | Shared DTOs, event shapes, runtime ports, and product domain contracts/policies | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | Boundary rules: @@ -52,7 +52,7 @@ Boundary rules: - Assembly wires lower layers and selects product capability facts; it must not implement concrete adapter, OS, or service details. - Product features assemble user-facing commands, UI contributions, settings, and default policy on top of kernel capabilities; long-running task, scheduler, permission, session/workspace, memory, DFX, hook, and event facts stay in Agent Kernel owners. - Adapters translate protocols and external-provider shapes; they should not own product capability selection or reusable OS service behavior. -- Services implement reusable concrete OS, process, terminal, MCP, remote, git, filesystem, LSP plugin registry, and MiniApp runtime IO capabilities. +- Services implement reusable concrete OS, process, terminal, MCP, remote, git, filesystem, and MiniApp runtime IO capabilities. - External systems are boundary resources, not repository layers. Only registered adapters/services/app-local providers should call them; other layers consume ports and stable contracts. - Execution crates are portable runtime building blocks, not host-specific or delivery-profile owners. - Contracts stay behavior-light and must not depend upward. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cb107a01a..9b59315fc9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,7 +105,7 @@ terms: > Product managers and UI designers are welcome to submit ideas quickly via PI. We will help refine them for development. 2. Improve the Agent system and overall quality 3. Improve system stability and strengthen foundational capabilities -4. Expand the ecosystem (Skills, MCP, LSP plugins, or better support for domain-specific development scenarios) +4. Expand the ecosystem (Skills, MCP, or better support for domain-specific development scenarios) ## Contribution Workflow and PR Expectations diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index 444f59fb3b..12cdcfc3ee 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -95,7 +95,7 @@ DevTools;`Cmd/Ctrl + Shift + I` 切换 BitFun 元素检查器,`Cmd/Ctrl + Sh > 欢迎产品经理、UI 设计师通过 PI 快速提交创意,我们会帮助完善开发 2. 优化 Agent 系统和效果 3. 对提升系统稳定性和完善基础能力 -4. 扩展生态(Skills、MCP、LSP 插件,或者对某些垂域开发场景的更好支持) +4. 扩展生态(Skills、MCP,或者对某些垂域开发场景的更好支持) ## 贡献流程与 PR 约定 diff --git a/Cargo.lock b/Cargo.lock index 36b7a8277f..d9329d53a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1534,7 +1534,6 @@ dependencies = [ "ignore", "libc", "log", - "notify", "regex", "rusqlite", "serde", @@ -1548,7 +1547,6 @@ dependencies = [ "which 8.0.5", "win32job", "windows 0.61.3", - "zip 4.6.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 608df39c40..316cb95efe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ dark-light = "1.1" dunce = "1" filetime = "0.2" fs2 = "0.4" -zip = { version = "4.6", default-features = false, features = ["deflate"] } # plugin load +zip = { version = "4.6", default-features = false, features = ["deflate"] } # archive extraction flate2 = "1.0" tar = "0.4" bzip2 = "0.5" diff --git a/MiniApp/Skills/miniapp-dev/SKILL.md b/MiniApp/Skills/miniapp-dev/SKILL.md index 8e291f9b4f..296fa457d1 100644 --- a/MiniApp/Skills/miniapp-dev/SKILL.md +++ b/MiniApp/Skills/miniapp-dev/SKILL.md @@ -207,7 +207,7 @@ MiniApp 框架**只暴露下列能力**,没有任何"通用 BitFun 后端通 - GitService(结构化 status / diff / blame,区别于裸 `git` 命令) - TerminalService(创建/读写交互式终端) - Session / AgenticSystem 的**通用**会话管理(任意会话的创建、接管、读写)——小应用只能通过 `app.agent.*` 启动**自己的**隐藏会话并消费其事件,再通过 `app.chat.*` 借用悬浮会话气泡做输入与过程展示;不能访问其他会话 -- LSP / Snapshot / Mermaid / Skills / Browser API / Computer Use / Config 等 +- Snapshot / Mermaid / Skills / Browser API / Computer Use / Config 等 需要这类能力时的合规姿势: diff --git a/MiniApp/Skills/miniapp-dev/api-reference.md b/MiniApp/Skills/miniapp-dev/api-reference.md index b62f1c1e20..dcbb9e26c4 100644 --- a/MiniApp/Skills/miniapp-dev/api-reference.md +++ b/MiniApp/Skills/miniapp-dev/api-reference.md @@ -19,7 +19,7 @@ MiniApp **能且只能**用以下 API,没有任何"通用 BitFun 后端通道" - `app.call('xxx', ...)` + `worker.js` —— 自定义 Node 后端(仅 `node.enabled = true` 时) - `app.appearanceMode / locale / on*` —— 主题与 i18n -**框架不暴露**的 BitFun 后端能力(截至当前版本):WorkspaceService(结构化搜索 / 索引)、GitService(结构化 status/diff/blame)、TerminalService、Session/AgenticSystem、LSP / Snapshot / Mermaid / Skills / Browser / Computer Use / Config 等。需要这些能力时: +**框架不暴露**的 BitFun 后端能力(截至当前版本):WorkspaceService(结构化搜索 / 索引)、GitService(结构化 status/diff/blame)、TerminalService、Session/AgenticSystem、Snapshot / Mermaid / Skills / Browser / Computer Use / Config 等。需要这些能力时: 1. 能用裸命令行解决就用 `app.shell.exec`(如 git → 在 `permissions.shell.allow` 加 `"git"`,参考 `builtin-coding-selfie`); 2. 只是要读 BitFun 工作区里的文件就用 `app.fs.*`(把 `{workspace}` 加到 `permissions.fs.read`); diff --git a/README.md b/README.md index 7d3564a344..fa12cecbc4 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Stars, Issues, and PRs are welcome. We especially care about: 1. Code Agent, Deep Review, debugging, and long-task execution capabilities 2. Cowork, research, document, and desktop workflows -3. MCP, Skills, Mini App, LSP plugins, and new domain Agents +3. MCP, Skills, Mini App, and new domain Agents 4. Runtime stability, performance, context efficiency, and verifiability Please submit PRs directly to the `main` branch. For more details, see [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 53a4cb7ec1..5ea85dec84 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -159,7 +159,7 @@ BitFun 的扩展路径从轻到重连续展开: 1. Code Agent、Deep Review、调试和长任务执行能力 2. Cowork、调研、文档和桌面工作流 -3. MCP、Skills、Mini App、LSP 插件和新领域 Agent +3. MCP、Skills、Mini App 和新领域 Agent 4. Runtime 稳定性、性能、上下文效率和可验证性 请将 PR 直接提交至 `main` 分支。更多说明见 [CONTRIBUTING_CN.md](./CONTRIBUTING_CN.md)。 diff --git a/SECURITY.md b/SECURITY.md index b817b1b6e8..4a333d60c8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ [中文版](./SECURITY_CN.md) -BitFun is a desktop-grade Agent runtime (Rust core + Tauri shell) that runs on your own machine with broad capabilities—filesystem, terminal, Git, MCP/LSP, and remote control. Because of this reach, we take security reports seriously and appreciate the community's help in keeping users safe. +BitFun is a desktop-grade Agent runtime (Rust core + Tauri shell) that runs on your own machine with broad capabilities—filesystem, terminal, Git, MCP, and remote control. Because of this reach, we take security reports seriously and appreciate the community's help in keeping users safe. ## Supported Versions diff --git a/SECURITY_CN.md b/SECURITY_CN.md index 900186ea55..bbe4b02b88 100644 --- a/SECURITY_CN.md +++ b/SECURITY_CN.md @@ -2,7 +2,7 @@ [English](./SECURITY.md) -BitFun 是一个运行在你本机的桌面级 Agent 运行时(Rust 内核 + Tauri 外壳),具备文件系统、终端、Git、MCP/LSP 以及远程控制等广泛能力。正因为权限范围较大,我们非常重视安全问题,并感谢社区帮助我们保护用户安全。 +BitFun 是一个运行在你本机的桌面级 Agent 运行时(Rust 内核 + Tauri 外壳),具备文件系统、终端、Git、MCP 以及远程控制等广泛能力。正因为权限范围较大,我们非常重视安全问题,并感谢社区帮助我们保护用户安全。 ## 受支持的版本 diff --git a/docs/architecture/extensions/external-ai-work-sources-design.md b/docs/architecture/extensions/external-ai-work-sources-design.md index ecf60b044b..c2b3a66607 100644 --- a/docs/architecture/extensions/external-ai-work-sources-design.md +++ b/docs/architecture/extensions/external-ai-work-sources-design.md @@ -234,11 +234,15 @@ provider 失败事实,避免在空目录界面中伪装成“成功但没有 | 等级 | 示例 | 默认行为 | |---|---|---| -| L0 仅清单 | 尚未支持的字段、静态插件/工具名称、来源元数据 | 自动发现和展示,绝不宣称已经应用或可执行。 | +| L0 仅清单 | 尚未支持的字段、静态插件/工具名称、来源元数据、外部生态的 LSP 声明 | 自动发现和展示,绝不宣称已经应用或可执行。 | | L1 被动声明 | 本地 Rules、Instructions、纯声明配置、Skill 的说明和索引 | 校验后默认自动应用;显示一次可撤销摘要。不得启动进程、读取凭据或主动联网。 | -| L2 受归属模块保护的外部能力 | 可执行 Skill/Command、远程 Reference、MCP、LSP、Formatter、Provider 连接 | 发现后进入“需确认”;由真实归属模块展示命令、网络、凭据和使用范围后启用。 | +| L2 受归属模块保护的外部能力 | 可执行 Skill/Command、远程 Reference、MCP、Formatter、Provider 连接 | 发现后进入“需确认”;由真实归属模块展示命令、网络、凭据和使用范围后启用。 | | L3 任意第三方代码 | JS/TS Tool、服务插件、动态 Hook/TUI 入口、动态 import | 默认发现但不 import;只有能在执行前完整枚举命令与依赖、并由既有归属模块承担执行的窄切片可经独立设计和精确审阅启用。不能承诺动态 import 前已知全部贡献。 | +LSP 是明确的退役边界,不参与 L2 升级:Claude Code、OpenCode 或其他来源中的 LSP 字段只能作为 L0 来源事实或 +`unsupported` 诊断保留。BitFun LSP Runtime 已退役,adapter 不得导入、应用或执行这些声明,不得为其创建产品 DTO、 +启动进程,也不得在 Remote 不支持时回退到控制端本机。 + OpenCode Subagent 属于 L2:adapter 只读取声明,不执行外部代码;激活仍需确认实际模型、工具、执行域和来源关系。 仅 description 等 catalog 文案变化不会扩大运行权限,因此不重复询问;prompt 行为、来源、模型或工具变化必须重新确认。 当前实现中,生态 adapter 只提交类型明确的 `Default`、`Inherit` 或不透明 `Reference`,不能把 `provider/model` @@ -307,7 +311,8 @@ Desktop、交互式 TUI 以及未来通过 Host 能力访问该状态的界面 本切片明确不实现:内置厂商/型号别名表、按名称推断能力、模型质量评分、成本优化、自动跨 provider fallback、在线模型目录、 模型下载或安装、请求级配置覆盖、通用 options map、temperature/top_p/thinking budget/reasoning summary/verbosity、 -Plugin Host Runtime、LSP,以及通用动态模型路由器。现有 capability 标签只用于验证模型是否具备调用所需的已声明功能, +Plugin Host Runtime、已退役的 LSP Runtime,以及通用动态模型路由器。外部 LSP 声明只形成 L0/`unsupported` +事实,永不导入、应用或执行。现有 capability 标签只用于验证模型是否具备调用所需的已声明功能, 不能据此宣称两个模型在质量、成本、隐私或上下文行为上等价。 完成判定使用表驱动契约覆盖 Claude、GPT、GLM、DeepSeek 风格以及未知未来名称,证明生产解析不依赖任何厂商列表;同时 @@ -392,11 +397,13 @@ Plugin Host Runtime、LSP,以及通用动态模型路由器。现有 capabilit - 同一 provider 先完成原生覆盖,再把一个 effective candidate 交给产品管理模块;跨 provider 或 BitFun-native 同名才生成 用户冲突。选择绑定参与者与行为版本,仅展示变化不重问,候选删除或不可用不静默回退。 -该矩阵不是“除 Runtime 外全部兼容”的声明。Rules/Instructions、References、模型/Provider 配置、LSP、Formatter、 +该矩阵不是“除 Runtime 外全部兼容”的声明。Rules/Instructions、References、模型/Provider 配置、Formatter、 Theme、Keybind、完整插件清单,以及各生态新增的 managed/session/plugin 配置层仍需按具名消费场景逐项评估;其中能够 安全静态解析的部分也尚未全部实现。后续不得为了填满矩阵建立通用配置 DTO 或第二套归属模块,应继续以端到端收益和 真实消费方为前提扩展现有能力契约。 +LSP 不属于上述待评估能力:上游声明可以被发现并解释为不支持,但不得进入应用层、运行层或未来阶段计划。 + 声明式 adapter 共同遵守以下边界: - 仅扫描已知 root;配置文件、角色文件和符号链接在读取前 canonicalize,越出声明来源 root、循环或非普通文件即拒绝。 diff --git a/docs/architecture/extensions/opencode-config-assets-adapter-design.md b/docs/architecture/extensions/opencode-config-assets-adapter-design.md index 7aad29f849..8089ae61cc 100644 --- a/docs/architecture/extensions/opencode-config-assets-adapter-design.md +++ b/docs/architecture/extensions/opencode-config-assets-adapter-design.md @@ -8,6 +8,9 @@ 外部来源的统一产品体验、风险分级和变化提示见 [`external-ai-work-sources-design.md`](external-ai-work-sources-design.md)。 +其中 LSP 仅作为 OpenCode 上游配置事实进入清单:BitFun LSP Runtime 已退役,这类声明固定为 `unsupported`, +不得导入、应用或执行,也不进入后续兼容阶段。 + 配置字段与来源以 [OpenCode 配置文档](https://opencode.ai/docs/config/)和稳定提交中的主/TUI 配置实现为准。 本文同时记录当前可用切片与后续目标。BitFun 已实现本地用户全局/项目 Prompt Command 的来源发现、 @@ -21,7 +24,7 @@ JSON/JSONC/Markdown 解析、参数展开、运行时刷新和冲突选择,也 1. OpenCode 用户可以直接打开已有项目,常用配置和声明式资产无需手工转换即可生效。 2. 保留 OpenCode 的来源使用范围、合并顺序、冲突和相对路径语义,并能解释最终值来自哪里。 -3. 尽量复用 BitFun 已有配置、Agent、Skill、MCP、LSP、主题等归属模块,不复制第二套产品内核。 +3. 尽量复用 BitFun 已有配置、Agent、Skill、MCP、主题等归属模块,不复制第二套产品内核。 4. 未知字段和未支持资产局部降级,不导致整个配置、项目启动或界面卡死。 5. 低风险内容默认无感应用并可撤销;可执行、联网、凭据或外部进程能力在首次启用和能力扩大时等待非阻塞确认。 6. 本地激活后的运行语义以兼容优先;用户或组织可以收紧权限,策略差异必须与解析或插件错误分开显示。 @@ -163,7 +166,7 @@ OpenCode 来源顺序决定兼容输入如何合并;BitFun 产品能力上限 | OpenCode 来源发现器 | 在本地或 Remote 执行域寻找主配置、独立 TUI 配置、目录资产、环境指定来源和组织默认 | 合并配置、执行插件、保存最终产品状态 | | OpenCode 配置解析器 | JSON/JSONC、变量引用、字段版本、来源位置和未知字段保留 | 使用 BitFun 默认值猜测 OpenCode 语义 | | 来源合并器 | 按固定 OpenCode 版本合并并记录每个最终值的来源和覆盖关系 | 应用 BitFun 产品或组织策略 | -| 资产适配器 | 把 Rule、Agent、Skill、Command、MCP、LSP、Formatter、Theme、Keybind、Reference 和模型配置分别交给已存在或阶段内补齐的真实消费接口 | 因“看起来已有”而跳过基础能力或边界整理,或创建第二套 Agent、MCP、LSP、Formatter 或主题运行时 | +| 资产适配器 | 把 Rule、Agent、Skill、Command、MCP、Formatter、Theme、Keybind、Reference 和模型配置分别交给已存在或阶段内补齐的真实消费接口;LSP 只输出 L0 `unsupported` 诊断 | 因“看起来已有”而跳过基础能力或边界整理,创建第二套 Agent、MCP、Formatter 或主题运行时,或为已退役的 LSP 建立 DTO/进程/执行路径 | | 策略检查 | 在 OpenCode 合并结果上应用用户、产品和组织上限,生成可解释差异 | 改写原始 OpenCode 文件或伪装成解析错误 | | 状态与诊断服务 | 原子发布新结果、保留上一有效结果、聚合错误并区分已发现/已应用/需确认/暂时过期/已移除 | 在界面线程同步解析远程来源或安装依赖 | @@ -211,7 +214,7 @@ OpenCode adapter 在来源发现、解析和审批前不 import module、不读 | References | `references` / 旧 `reference`,本地 path 或 Git repository/branch/description/hidden | OpenCode adapter 输出来源无关的 Reference provider snapshot;Product Assembly 生命周期协调器与 BitFun 原生关联目录合成唯一有效引用目录;关联目录视图和既有目录选择器消费 | 当前支持本地声明路径、description/hidden、异步刷新和 `@alias` 展示;原生关联目录始终在 OpenCode 引用之前,外部引用只读、不自动进入 Prompt 且不改变权限 | Git 引用、Remote 发现和下载/缓存不实现;无效高优先级 entry 阻断同 alias 的旧值并给出诊断,不回退到更宽松来源。 | | Commands | JSON/JSONC、Markdown、`$ARGUMENTS`、位置参数、`@file`、`!shell`、agent/model/variant/subtask | Prompt Command 专属契约;adapter 提取静态文件引用与 shell 计划,Product Assembly 负责审批指纹和装配,Terminal owner 负责进程执行 | prompt 与静态 workspace 相对 UTF-8 `@file` 可发送;`!shell` 展示精确命令、工作目录与绝对 shell 路径,经重新校验后以不加载 profile 的隔离式 argv 执行,并仅把 stdout 按模板顺序加入 Prompt。为保持 OpenCode 语义,正常退出后的非零退出码仍使用 stdout。静态计划可记住,参数相关计划仅可单次运行;显式 agent 加缺省/true subtask 可走 approved fresh Subagent,其余 agent/model/variant/subtask 组合以及 shell 与委派的组合整体受限 | 任一文件读取、进程启动、超时或超限失败时不发送部分 Prompt;进程副作用不可回滚。最多 8 文件、单文件 64 KiB、文件总量 128 KiB;最多 8 条 shell 指令、单条 64 KiB、总计 128 KiB、每条 stdout 256 Ki 字符、30 秒;最终命令 1 MiB。安全模式禁用,Remote 不回退到本机。 | | MCP | local 的 command/environment/cwd/timeout,remote 的 URL/headers/oauth/timeout,Agent 选择 | MCP 归属模块创建兼容配置视图 | 当前支持 local stdio 和 HTTPS remote 的静态发现、首次/行为变化审批、冲突选择与 workspace 隔离的运行期接纳;显式 V1 `timeout` 作为毫秒值约束启动、目录读取和执行,并在 GUI/TUI 审阅详情中可见;C0a 快照导入只复制无 env/cwd/timeout 的 local command/args 或无 header/query/fragment/timeout 的 HTTPS remote,并保持 disabled | `{env:NAME}` 当前只允许用于运行期兼容来源的 environment/Header 值,不进入 C0a 快照;SSE、OpenCode OAuth client 配置、Agent 范围、Remote 执行域与 V2 分阶段 timeout 配置格式保持明确不支持;凭据、超时或网络失败只影响单个 Server。 | -| LSP | command、extensions、env、initialization | LSP 归属模块注册兼容实例 | 首次确认外部进程和使用范围后按文件类型启动 | 自定义 Server 缺少 extensions 或启动失败时只禁用该项。 | +| LSP | command、extensions、env、initialization | **Runtime 已退役**:adapter 只保留来源位置与字段存在事实,不创建运行时 DTO | 固定显示 `unsupported`;永不导入、应用或执行 | 不启动本地或 Remote 进程,不安装 Server,不回退到控制端本机;该项不进入任何 OC-R 阶段。 | | Formatters | command、environment、extensions、`$FILE` | **基础能力缺失**:先补文件写入后的 Formatter 执行消费点,再做格式转换 | 首次确认命令后执行匹配 Formatter | 超时后标记未格式化,文件写入结果保留。 | | Themes | builtin/user/project/cwd JSON | **部分已有**:GUI Theme 已有;TUI 主题消费边界在终端阶段补齐 | 保留覆盖顺序和语义角色 | 颜色能力不支持时做可见降级。 | | Keybinds | `tui.json` 的 leader、组合键、禁用和命令标识 | **已有行为、边界未抽取**:从现有 TUI 输入/命令路径提取最小接口 | 保留用户和项目覆盖 | 平台冲突时显示最终绑定与原因。 | @@ -394,7 +397,9 @@ OpenCode 生态内部仍按其规则覆盖同名内置命令,但跨独立 prov 和执行;缺省值继续使用 BitFun 既有行为。当前是每次请求的硬期限,不因 progress 重置;超时只停止 BitFun 的当前等待, 不承诺服务端工作已经取消,也不自动重放或重启。SSE、OpenCode `clientId/clientSecret/scope/callbackPort/redirectUri`、V2 分阶段 timeout 配置格式和 Agent 范围仍需后续接入,不能静默忽略。 -- LSP 必须覆盖 initialization、扩展名匹配、环境变量和工作区生命周期。 +- LSP 只解析到来源清单和 `unsupported` 诊断。BitFun LSP Runtime 已退役,因此 command、extensions、env、 + initialization 不进入产品 DTO,不触发审批、安装或进程启动,在本地、Remote Workspace、Peer Device 和 Detached + Dispatch 中都不得执行或回退。 - Formatter 必须覆盖写入后时机、`$FILE` 替换、`environment`、多个 Formatter 顺序和失败行为。 ### 5.5 其他稳定配置项 @@ -447,8 +452,9 @@ OpenCode 配置文档还包含下列不属于声明式目录资产、但会改 - 项目 `.opencode` 在实际工作区所在执行域发现、解析和监听;远程项目不回到本机扫描同名路径。 - 用户全局根必须明确属于本地用户还是远程用户,不能按路径字符串猜测。 -- 本地界面可以展示远程兼容结果和诊断,但依赖安装、LSP、Formatter、local MCP、Command shell 和插件 worker +- 本地界面可以展示远程兼容结果和诊断,但依赖安装、Formatter、local MCP、Command shell 和插件 worker 在远程执行域运行。 +- 远程来源中的 LSP 字段仍只显示 `unsupported`;不在远端启动,也不回退到本机。 - OC-R5 在远端实现同一窄访问接口,并路由到远端已有领域存储或执行环境;R5 之前远程插件路径返回明确 `unsupported`。不存在通用 Remote credential broker,也不从本机静默复制原值。 - 两端先交换兼容版本和能力;远端不支持的资产局部降级,不能把整个会话标记为失败。 diff --git a/docs/architecture/extensions/opencode-extension-compatibility.md b/docs/architecture/extensions/opencode-extension-compatibility.md index 5f17df5546..f6d529a732 100644 --- a/docs/architecture/extensions/opencode-extension-compatibility.md +++ b/docs/architecture/extensions/opencode-extension-compatibility.md @@ -9,7 +9,7 @@ | 主题 | 详细设计 | |---|---| | 外部 AI 工作内容的发现、非阻塞提示、风险分级、导入与持续更新 | [外部 AI 工作内容体验](external-ai-work-sources-design.md) | -| 配置来源、Rules、Agents、Skills、Commands、MCP、LSP、Formatter、Theme、Keybind | [配置与声明式资产适配](opencode-config-assets-adapter-design.md) | +| 配置来源、Rules、Agents、Skills、Commands、MCP、LSP(仅 `unsupported` 来源事实)、Formatter、Theme、Keybind | [配置与声明式资产适配](opencode-config-assets-adapter-design.md) | | JS/TS 工具、软件包插件、稳定 Hook、`client`、`serverUrl`、`$` | [服务插件运行时适配](opencode-plugin-runtime-adapter-design.md) | | TUI 插件入口、Route、Command、Keymap、Dialog、Slot、Theme、State、KV | [终端界面插件适配](opencode-tui-plugin-adapter-design.md) | | SDK、Server、ACP、IDE、Web、GitHub、GitLab、Slack | [外部集成适配](opencode-external-integration-adapter-design.md) | @@ -24,7 +24,7 @@ - 最新稳定版为 [`v1.18.9`](https://github.com/anomalyco/opencode/releases/tag/v1.18.9),提交为 [`4da7bb44c84e013fa53e9c5d02ac753d1435c81a`](https://github.com/anomalyco/opencode/commit/4da7bb44c84e013fa53e9c5d02ac753d1435c81a)。 - 开发分支前瞻检查记录为提交 [`7565e03536d19e850f9996c407f9bf5e932b5f7a`](https://github.com/anomalyco/opencode/commit/7565e03536d19e850f9996c407f9bf5e932b5f7a)。该值会持续变化,只用于发现差异,不计入稳定兼容承诺。 - `v2` 前瞻分支检查记录为提交 [`247f14f9556c31ee532cb4a79a83283e753adc62`](https://github.com/anomalyco/opencode/commit/247f14f9556c31ee532cb4a79a83283e753adc62):仓库仍以 Bun 作为 package manager、开发和默认编译路径,同时提供 Node 26 SEA 并行构建与 Node 启动器。它不是“已经完全切换 Node”的稳定承诺。 -- 配置、插件、工具、Agent、Skill、Command、Rule、MCP、LSP、Formatter、Theme、Keybind、开发工具包、Server 和 ACP 以 [OpenCode 官方文档](https://opencode.ai/docs/) 为准。 +- 配置、插件、工具、Agent、Skill、Command、Rule、MCP、Formatter、Theme、Keybind、开发工具包、Server 和 ACP 以 [OpenCode 官方文档](https://opencode.ai/docs/) 为准。 - 稳定服务插件接口以 [`packages/plugin/src/index.ts`](https://github.com/anomalyco/opencode/blob/4da7bb44c84e013fa53e9c5d02ac753d1435c81a/packages/plugin/src/index.ts) 为准; - custom tool 接口以 [`packages/plugin/src/tool.ts`](https://github.com/anomalyco/opencode/blob/4da7bb44c84e013fa53e9c5d02ac753d1435c81a/packages/plugin/src/tool.ts) 为准; - 终端插件接口以 [`packages/plugin/src/tui.ts`](https://github.com/anomalyco/opencode/blob/4da7bb44c84e013fa53e9c5d02ac753d1435c81a/packages/plugin/src/tui.ts) 为准; @@ -113,7 +113,7 @@ OpenCode,和 OpenCode 配置/插件进入 BitFun 是两个独立验收方向 | Commands | 补扩展接口 + 转换参数 | 部分实现:prompt、本地文本文件、经审阅的 shell 上下文与显式 Subagent 委派 | 可完整适配 | OC-R2 | 已支持全局/项目 JSON、JSONC、Markdown 命令、参数展开、动态目录、刷新和显式冲突选择;模板中的静态 workspace 相对 `@file` 可在调用时有界读取,`!shell` 经精确计划审阅后仅把 stdout 加入 Prompt,静态计划可记住、参数相关计划仅可单次运行。仅 `agent` 加缺省/`true` 的 `subtask` 可委派给同 workspace、同 OpenCode 生态、已审批且仍有效的精确 Subagent,并复用现有 fresh Task 生命周期;shell 与委派的组合、`model`、`variant`、`subtask: false`、隐式默认 Agent、Remote 与附件上下文保持受限,不回退到当前 Agent 或本机执行 | [Commands](opencode-config-assets-adapter-design.md#53-commands) | | Models / Providers 配置 | 融合现有能力 | 未实现 | 可主要适配 | OC-R1 | 静态字段进入模型归属模块;动态模型、鉴权和请求头交给插件运行时 | [声明式资产](opencode-config-assets-adapter-design.md#5-声明式资产映射) | | MCP | 转换参数 | 部分实现:local stdio 与 HTTPS remote | 可完整适配 | OC-R2 | 已接入发现、审批、冲突、workspace 隔离、更新和启动反馈;SSE、OAuth、完整 timeout/Agent 范围仍不支持;Remote 不回退本机实例 | [MCP、LSP 与 Formatter](opencode-config-assets-adapter-design.md#54-mcplsp-与-formatter) | -| LSP | 转换参数 | 未实现 | 可完整适配 | OC-R2 | R1 解析;R2 转换 command、extensions、env 和 initialization 并由 LSP 归属模块启动 | [MCP、LSP 与 Formatter](opencode-config-assets-adapter-design.md#54-mcplsp-与-formatter) | +| LSP | 明确退役 | Runtime 已删除;仅可保留上游来源事实 | 明确降级:不适配 | 不安排 | command、extensions、env、initialization 只能形成 L0 `unsupported` 诊断;不得导入、应用、执行、创建 DTO、启动进程或 Remote fallback | [MCP、LSP 与 Formatter](opencode-config-assets-adapter-design.md#54-mcplsp-与-formatter) | | Formatters | 补基础能力 + 转换参数 | 未实现 | 可主要适配 | OC-R2 | R1 解析;R2 补文件写入后的格式化执行能力,再映射 command/environment/extensions/`$FILE` | [MCP、LSP 与 Formatter](opencode-config-assets-adapter-design.md#54-mcplsp-与-formatter) | | Themes | 转换参数 | 未实现 | 可主要适配 | OC-R1 | 保留 builtin/user/project/cwd 覆盖顺序,分别映射 GUI 和 TUI 色彩能力 | [声明式资产](opencode-config-assets-adapter-design.md#5-声明式资产映射) | | Keybinds | 补扩展接口 + 转换参数 | 未实现 | 可主要适配 | OC-R1 | 为运行时 TUI 输入增加 `tui.json` 兼容入口,处理 leader、组合键、禁用和冲突 | [声明式资产](opencode-config-assets-adapter-design.md#5-声明式资产映射) | diff --git a/docs/architecture/extensions/opencode-tui-plugin-adapter-design.md b/docs/architecture/extensions/opencode-tui-plugin-adapter-design.md index 9de4c2505a..c5dddc55ce 100644 --- a/docs/architecture/extensions/opencode-tui-plugin-adapter-design.md +++ b/docs/architecture/extensions/opencode-tui-plugin-adapter-design.md @@ -252,7 +252,9 @@ Route 注册成功不代表原始组件可渲染。若只有 route id 可识别 ### 5.10 State、KV、Client 与 Events - `api.tuiConfig` 和 TUI synced state 通过实时兼容视图映射,不暴露 BitFun 内部 reducer 对象。状态至少覆盖 - config、provider、path、vcs、session count/diff/todo/messages/status/permission/question、part、LSP 和 MCP。 + config、provider、path、vcs、session count/diff/todo/messages/status/permission/question、part、上游 LSP 字段事实和 MCP。 + LSP 只用于版本化 schema 识别和明确的 `unsupported` 状态;BitFun 不消费或发布 LSP Runtime 状态,不创建 DTO、 + 启动进程,也不从 Remote 回退到本机。 - OpenCode v1 的 `api.kv` 是同一 TUI 应用共享的 KV,不按插件命名空间隔离。默认兼容策略必须保持共享键和 ready 语义;这意味着插件可能发生键冲突。用户选择受限策略时可以按插件隔离,但必须显示兼容降级。 - KV 读写在 Plugin Host 外由现有归属模块持久化,写入失败返回稳定错误且不阻塞渲染线程。 diff --git a/docs/architecture/peer-device-mode.md b/docs/architecture/peer-device-mode.md index 1e60d084c9..5b45355f18 100644 --- a/docs/architecture/peer-device-mode.md +++ b/docs/architecture/peer-device-mode.md @@ -32,8 +32,8 @@ lists this machine plus every online peer. Two rules follow, and both are load-bearing: - **A surface switch never mutates the device being left.** Everything in - `resetProductSurface()` is frontend-only. Sending `terminal_shutdown_all` or - `lsp_close_workspace` during a switch lands on the *previous* transport and + `resetProductSurface()` is frontend-only. Sending `terminal_shutdown_all` + during a switch lands on the *previous* transport and kills work an agent there still depends on. - **Product events are routed by their source device.** The controller re-emits peer DeviceEvents under their original event name, so with peers attached in @@ -184,7 +184,7 @@ FS) and must not be mixed with Peer Device Mode. `RemoteCommand::HostInvoke` over `account_device_rpc`. - HostInvoke on the controller is **priority-queued** with four requests in flight. Session restore / session-list / dialog / workspace-startup commands - outrank background `git_*` / `ssh_*` / `lsp_*` / `search_*` / FS / canvas / + outrank background `git_*` / `ssh_*` / `search_*` / FS / canvas / editor RPCs so hydrate is not starved into relay HTTP 504s. Terminal commands are always interactive priority, and one slot is kept free from normal and low-priority work so input cannot be trapped behind slow polling requests. diff --git a/docs/architecture/product-architecture.md b/docs/architecture/product-architecture.md index 78ada13378..7e12e1535c 100644 --- a/docs/architecture/product-architecture.md +++ b/docs/architecture/product-architecture.md @@ -846,7 +846,7 @@ flowchart LR | 当前入口 | 已有能力 | 明确边界 | |---|---|---| | Desktop | 使用 `product-full`;Settings 从现有来源目录和 integration policy 生成简短应用概览,具体审批与冲突仍进入 Tool、Agent、MCP 或 Hook owner | 可执行能力在事实所在 Host 运行;Safe Mode 只阻止新调用,不改来源、不取消正在运行的调用 | -| CLI / TUI | 使用显式 Core owner closure:`agent-runtime` 基线、实际 service owner(包括 Remote Connect、DeepResearch、LSP、external/plugin source 与 SSH)以及九组 `tools-*`;`/extensions` 只提供状态、启停和刷新,`/hooks`、`/tools`、`/agent` 和 `/mcp` 处理各自能力 | `agent-runtime` 不再隐式携带完整 MCP/Remote/Browser/Web/Git/LSP/模型目录闭包;非交互不等待权限输入,生态解析仍在适配器,远程能力未接入时不回退本机 | +| CLI / TUI | 使用显式 Core owner closure:`agent-runtime` 基线、实际 service owner(包括 Remote Connect、DeepResearch、external/plugin source 与 SSH)以及九组 `tools-*`;`/extensions` 只提供状态、启停和刷新,`/hooks`、`/tools`、`/agent` 和 `/mcp` 处理各自能力 | `agent-runtime` 不再隐式携带完整 MCP/Remote/Browser/Web/Git/模型目录闭包;非交互不等待权限输入,生态解析仍在适配器,远程能力未接入时不回退本机 | | ACP | 使用 `DeliveryProfile::Acp`、Runtime Parts、`agent-runtime` 基线、所需 service owner 与九组 `tools-*`,但不选择 CLI 的 plugin runtime 和 Remote Connect owner | load 成功后才发布活动状态;close 排空后再卸载;完整历史、Canvas 工具物化、兼容指令来源和配置仍由 Core/ACP 管理;未选择的能力不得借 Cargo feature union 偶然出现 | | SDK Host(preview) | 使用 `DeliveryProfile::Sdk`、Runtime Parts 和与当前本机协议能力一致的显式 Core owner closure;TLS provider 由 Host 进程入口安装 | 当前协议不暴露远程 workspace/SSH 执行,因此不选择 Remote Connect、SSH 或 Function Agent owner;未来远程 SDK 必须复用 Server/Remote 的认证和执行域,不能回退到本机执行 | | Peer / Server | Peer Host 执行真实工作区操作;通用 HTTP Server 未绑定可信 workspace owner 时明确返回不支持 | 控制端不替远端发现或执行;loopback 单用户边界不扩展到远程/多用户;SSH Remote 未接入时返回不支持 | diff --git a/docs/architecture/rust-build-dependency-boundaries.md b/docs/architecture/rust-build-dependency-boundaries.md index 6d30313b5a..57727b599c 100644 --- a/docs/architecture/rust-build-dependency-boundaries.md +++ b/docs/architecture/rust-build-dependency-boundaries.md @@ -59,7 +59,7 @@ Core library 的默认 feature 集合为空;完整产品必须显式选择 `pr `product-full`。每个角色必须独立编译,产品消费者还要显式关闭该 interface crate 的默认 feature 并选择 真实使用的角色,避免 workspace feature union 掩盖边界缺口。 -Core 的 `agent-runtime` 只承载 Agent 生命周期基线和明确的基线工具,不得再次把 MCP、Remote Connect、模型目录、Browser/Web、Git/LSP 或产品工具组藏成 capability union。具体 service 由同名 owner feature 选择,内置工具由 `tools-*` 选择;`product-full` 显式相加全部 owner,CLI/ACP 等窄入口则按真实命令与构造路径列出自己的闭包。 +Core 的 `agent-runtime` 只承载 Agent 生命周期基线和明确的基线工具,不得再次把 MCP、Remote Connect、模型目录、Browser/Web、Git 或产品工具组藏成 capability union。具体 service 由同名 owner feature 选择,内置工具由 `tools-*` 选择;`product-full` 显式相加全部 owner,CLI/ACP 等窄入口则按真实命令与构造路径列出自己的闭包。 执行层的 `bitfun-agent-runtime` 自身也保持空默认:完整生命周期由 `agent-runtime` 选择,DeepResearch 纯编号由 `deep-research` 选择,原生 Hook 配置解析与进程执行分别由 `native-hook-settings`、`native-hook-runtime` 选择。叶能力仍留在原 owner crate 内,不为依赖收敛新建 DTO/runtime crate;完整产品必须显式恢复真实 owner,不能依赖 workspace feature union 偶然补齐。 diff --git a/docs/performance/01-compile-performance.md b/docs/performance/01-compile-performance.md index c474dd3706..23658dcf49 100644 --- a/docs/performance/01-compile-performance.md +++ b/docs/performance/01-compile-performance.md @@ -225,7 +225,7 @@ Installer 自己生成的 `BitFun-Installer/src-tauri/Cargo.lock` 不提交。 | OpenCode Adapter | 141 → 140 | 140 → 139 | 140 → 139 | 既有 Markdown/Tokio owner 保留,仅未消费的 Similar 退出 | Core 的直接 Tokio capability 从 `fs/io-util/macros/net/rt/sync/time` 收敛为 `fs/sync`;异步宏、 -网络以及产品 runtime 能力由现有 `agent-runtime`、`browser-control`、`lsp` owner +网络以及产品 runtime 能力由现有 `agent-runtime`、`browser-control` owner 显式选择。feature-free Core 仍通过 `bitfun-services-core/json-io` 获得其原子 JSON 写入所需的 `rt/time`,测试使用的多线程运行时只留在 dev-dependency。`runtime-ports/permission` 没有被机械移出:`GlobalConfig` 与项目权限文件公开 DTO 确实在 feature-free facade 中使用它,继续保留比制造条件 API 更符合契约稳定性。 diff --git a/docs/performance/02-runtime-performance.md b/docs/performance/02-runtime-performance.md index 4e69d136bd..c5c9af9f71 100644 --- a/docs/performance/02-runtime-performance.md +++ b/docs/performance/02-runtime-performance.md @@ -5,6 +5,10 @@ - 方法:Grep 系统性扫描典型反模式(clone 密集区、async 中同步 IO、`Regex::new` 非静态、`block_on`、`JSON.parse(JSON.stringify)`、`setInterval`、`structuredClone`、未节流事件监听、高频 `invoke`/`emit`),对每个可疑点精读上下文并论证调用频率后才收录;所有明显但经确认无害的候选项在文末"误报排除记录"中留档,避免后续重复审查。 - 所有行号以当前 main 分支(48a003b73)为准。 +> 退役说明(2026-08):本文是提交 `48a003b73` 的固定历史审阅证据。此后 BitFun LSP Runtime 已被完整删除, +> 因此下文问题 #5 与任务 T3 已通过退役关闭,所述路径也不再存在。保留这些段落只为解释当时结论;不得据此恢复、 +> 优化或重新实现 LSP Runtime。 + --- ## 一、发现总览表 @@ -94,7 +98,7 @@ async fn emit(&self, event_name: &str, payload: serde_json::Value) -> anyhow::Re Ok(()) } ``` -`PeerAwareEmitter` 包裹的是**全局** emitter(`lib.rs:2008-2015`)和 LSP workspace emitter(`lsp_workspace_api.rs:189-192`),即所有 backend→frontend 事件(终端 chunk、文件变更、LSP 诊断、工具进度)每条都吃一次 `serde_json::Value` 深拷贝(逐节点堆分配,非 memcpy)—— 而绝大多数用户从不开 Peer Mode。同构问题:`terminal_api.rs:347-359` 对每个终端事件(64KB 峰值,~200 次/秒/终端)无条件 `serde_json::to_value` 后才进守卫。 +`PeerAwareEmitter` 包裹的是**全局** emitter(`lib.rs:2008-2015`)和当时存在的 LSP workspace emitter(`lsp_workspace_api.rs:189-192`),即所有 backend→frontend 事件(终端 chunk、文件变更、LSP 诊断、工具进度)每条都吃一次 `serde_json::Value` 深拷贝(逐节点堆分配,非 memcpy)—— 而绝大多数用户从不开 Peer Mode。同构问题:`terminal_api.rs:347-359` 对每个终端事件(64KB 峰值,~200 次/秒/终端)无条件 `serde_json::to_value` 后才进守卫。 **优化方案**:把 `should_fanout_peer_ui_event(event_name) && !attached_controllers().is_empty()` 判断提到 clone/to_value **之前**;不满足则 move payload 直接 emit,零拷贝。改动约 10 行,无语义变化,收益覆盖所有高频事件通道。 @@ -106,7 +110,9 @@ async fn emit(&self, event_name: &str, payload: serde_json::Value) -> anyhow::Re **优化方案**:`TranscriptStore.writers: HashMap` 已存在,补上常开的 `BufWriter` 句柄;去掉每 chunk 的 open/close 与 `flush()`(改定时/会话结束 flush);整个写路径移到专用写线程 + mpsc 或 `spawn_blocking`。 -#### 5. LSP 诊断/响应的五连深拷贝【R,高】 +#### 5. LSP 诊断/响应的五连深拷贝【R,高;Runtime 已退役】 + +> 历史项:以下描述只对应审阅提交 `48a003b73`。LSP Runtime 删除后,该优化任务已关闭,禁止实施或恢复相关代码。 **问题**:一次 `publishDiagnostics` 通知,同一份 `Vec` 被拷贝 5 次: 1. `process.rs:445` `diagnostics_arr.clone()`(`notif` 在 `:371` 已拥有,可 `Value::take` 移出); @@ -119,7 +125,7 @@ async fn emit(&self, event_name: &str, payload: serde_json::Value) -> anyhow::Re **频率**:工作区打开时 rust-analyzer/tsserver 为数百文件连发诊断;编辑期每次防抖后触发。200 条诊断 ≈ 100KB Value 树 × 5 = 每条通知 500KB 逐节点分配/释放。 -**优化方案**:`process.rs:164` 改 `match message` 按值解构消除响应 clone;`:445` 用 `Value::take`;`workspace_manager` 缓存与事件间用 `Arc>` 共享;`diagnostics_cache` 提为独立 `Arc>` 摆脱外层 `lsp_manager` 锁。 +**历史优化方案(已关闭,不得实施)**:`process.rs:164` 改 `match message` 按值解构消除响应 clone;`:445` 用 `Value::take`;`workspace_manager` 缓存与事件间用 `Arc>` 共享;`diagnostics_cache` 提为独立 `Arc>` 摆脱外层 `lsp_manager` 锁。 #### 6. 流式事件队列:每 delta 的分配与锁风暴【R,高】 @@ -221,7 +227,7 @@ async fn emit(&self, event_name: &str, payload: serde_json::Value) -> anyhow::Re |---|---|---|---| | T1 | PeerAwareEmitter/terminal 事件出口:把 `should_fanout_peer_ui_event && !attached_controllers().is_empty()` 提到 `payload.clone()`/`to_value` 之前,不满足则 move 零拷贝 | `apps/desktop/src/api/remote_connect_api.rs:558-561`、`terminal_api.rs:347-359` | 低 | | T2 | LS 工具:`list_directory_entries` 包 `spawn_blocking`(对齐 grep/glob);entry 增加 `is_symlink` 字段消除出队二次 stat;`Vec::with_capacity(limit)`;push 改 move | `crates/services/services-core/src/filesystem/listing.rs`、`assembly/core/src/agentic/tools/implementations/ls_tool.rs:292` | 低 | -| T3 | LSP 消息零拷贝:`process.rs:164` `match &message`→`match message` 消除响应 clone;`:445` `Value::take`;`workspace_manager.rs:889-906` 缓存/事件共享 `Arc>`;`diagnostics_cache` 独立锁 | `crates/services/services-core/src/lsp/process.rs`、`assembly/core/src/service/lsp/workspace_manager.rs` | 中 | +| T3(已关闭) | 历史 LSP 消息零拷贝方案;LSP Runtime 已退役,不得实施或恢复 | 已删除的 `crates/services/services-core/src/lsp/process.rs`、`assembly/core/src/service/lsp/workspace_manager.rs` | 不适用 | | T4 | 会话持久化:`sanitize_messages_for_persistence` 改 Cow 按需 clone;`to_string_pretty`→compact `to_writer`;加 200ms dirty-window 合批(保留 turn 结束强制落盘) | `assembly/core/src/agentic/persistence/manager.rs:646-654,1287-1298`、`services-core/src/json_store.rs:211` | 中(崩溃恢复语义需回归测试) | | T4b | (进阶)turn 快照改 JSONL 追加式,turn 结束/压缩时才全量重写 | 同上 + 读取端 | 高(格式迁移,需兼容旧快照) | | T5 | 文件树扫描:sort 前落地 `(entry, file_type, metadata)` 元组;Windows 权限复用 metadata;`canonicalize` 入 spawn_blocking;Vec 预分配;评估切换 `ignore::WalkBuilder` 并默认尊重 gitignore;合并 stats 复制版 | `services-core/src/filesystem/tree.rs:436-560,572-769,897` | 中(gitignore 默认值属行为变化,建议加开关) | diff --git a/docs/plans/core-decomposition-completed.md b/docs/plans/core-decomposition-completed.md index 0aeb438e76..75c50029cb 100644 --- a/docs/plans/core-decomposition-completed.md +++ b/docs/plans/core-decomposition-completed.md @@ -16,14 +16,14 @@ ## 1. 基础边界 - 已建立 `product-full` 作为兼容入口的完整产品能力保护开关,产品入口显式启用当前兼容能力集合;它不是未来按产品形态拆分能力的唯一事实源。 -- 已抽取 `bitfun-core-types`、`bitfun-events`、`bitfun-runtime-ports`、`bitfun-agent-stream` 等基础契约;LSP protocol DTO 和 plugin manifest DTO 已进入 `bitfun-core-types`。 +- 已抽取 `bitfun-core-types`、`bitfun-events`、`bitfun-runtime-ports`、`bitfun-agent-stream` 等基础契约。 - 已建立 `bitfun-services-core`、`bitfun-services-integrations`、`bitfun-agent-tools`、`tool-runtime`、`bitfun-tool-packs`、`bitfun-agent-runtime`、`bitfun-runtime-services`、`bitfun-harness`、`bitfun-product-domains`、`bitfun-product-capabilities` 等归属 crate。 - `src/crates` 已按 `interfaces / assembly / adapters / services / execution / contracts` 六层布局整理,DeepReview path classifier、边界规则、Cargo workspace 路径和根/层级 AGENTS 已同步。 - Cargo metadata 实际解析图检查已覆盖 workspace 与独立 manifest 的 normal、build、dev 依赖及 optional/target 变体;未知 crate 层级与反向依赖会直接失败。 ## 2. 已迁移归属 -- `services-core` 已承接 session layout、metadata store CRUD / index rebuild、metadata pagination、metadata construction / mutation、lineage / branch shaping、JSON file store、generic JSON persistence、storage cleanup、front-matter markdown、workspace instruction file IO/order、filesystem primitives、managed runtime command resolution / PATH merge、LSP plugin registry / extension matching / command-target mapping、diagnostic redaction、session usage/token usage 持久化与查询服务。 +- `services-core` 已承接 session layout、metadata store CRUD / index rebuild、metadata pagination、metadata construction / mutation、lineage / branch shaping、JSON file store、generic JSON persistence、storage cleanup、front-matter markdown、workspace instruction file IO/order、filesystem primitives、managed runtime command resolution / PATH merge、diagnostic redaction、session usage/token usage 持久化与查询服务。 - `services-core` 已承接 workspace-runtime legacy session-store merge、metadata 冲突选择、index rebuild 和 legacy path copy/move fallback;core workspace-runtime 只保留路径计算、runtime layout ensure 和错误兼容映射。 - `services-core` 已承接 LocalSystemAction 的稳定错误码转换;core Computer Use 系统动作路径只把这些 stable code 适配到既有 ControlHub 工具调用对象。 - `services-core` 已承接 memory workspace Git baseline、diff collection 和 diff file rendering;core memory workspace 只保留业务文件生成、Phase2 diff 清理、兼容 API 路径和错误类别映射。 @@ -42,8 +42,6 @@ - 插件运行时边界基础已建立:`runtime-ports` 持有 `PluginRuntimeClient`、binding、availability、插件调用与响应 DTO、disabled stub 和 `ProjectionOnlyPluginRuntimeClient`;产品组装输出扩展可用性事实与插件运行时绑定,并通过 Agent Runtime 内部 builder 注入该 binding;Agent Runtime SDK 接口不导出原始插件运行时 client 契约。默认产品启动不运行 JS/TS、worker 或子进程。 - OpenCode-compatible P0-C.1/P0-C.2 已建立受管包发现、完整性校验、工作区来源审核、精确内容哈希激活、CLI 管理与诊断,以及按需创建 OpenCode 适配器、`PluginRuntimeClient` 和 `PluginRuntimeBinding` 的唯一生产组装点。当前组装只返回需要权限的 custom tool 静态候选,不注册工具或执行插件代码。 - 插件停用已支持按工作区和包清理缺失或损坏包的残留激活记录;停用状态在扫描前提交,后续受限发现负责结果分类,并在稳定发现同 ID 不同来源时协调旧审核记录。包暂时缺失或损坏时保留来源审核记录;同一 `idempotency_key` 沿用已保存结果;请求携带的项目、信任、策略或工具目录版本过期时会被拒绝;持久化结果不确定时不报告成功。 -- LSP plugin runtime target 和命令占位符解析已从 `services-core` 收口到 `core-types`;`services-core` 保留兼容 re-export、registry、current-target detection 和 filesystem / runtime service 逻辑。 - - Agent session/workspace owner routing 已继续收敛:`AgentRuntime` 提供 port-backed session workspace resolution entrypoint;Cron、SessionControl、SessionMessage 和 SessionHistory 不再在工具实现中直接解析目标 session workspace,Cron 保留 target session 可见性验证,workspace identity 中的 `workspace_id` / remote connection / remote host 通过 runtime contract 传递。 - `/goal` model tool management 已继续收敛:`AgentRuntime` 提供 thread-goal management port,`get_goal` / `create_goal` / `update_goal` 经 `CoreServiceAgentRuntime` 路由到 core concrete adapter;goal lifecycle、metadata、tool response wire shape 和错误类别保持等价。 - `services-integrations` workspace search result mapping 已承接 flashgrep hit conversion 与 preview split owner,保持缺失 `line_text` 时的既有输出语义,并由 focused tests 覆盖有无 preview 两种路径。 diff --git a/docs/sdlc-harness/design.md b/docs/sdlc-harness/design.md index cd18fc79eb..e4a3fb2210 100644 --- a/docs/sdlc-harness/design.md +++ b/docs/sdlc-harness/design.md @@ -79,7 +79,7 @@ Git / PR provider / CI / Issue / 文档 / 发布 / 观测 / 知识库 智能体与工具运行时 - 会话 / 智能体 / 终端 / 文件系统 / MCP-LSP / Tool ABI / 适配器 + 会话 / 智能体 / 终端 / 文件系统 / MCP / Tool ABI / 适配器 ``` 关键边界: diff --git a/docs/sdlc-harness/research/external-research.md b/docs/sdlc-harness/research/external-research.md index 23c00bd21a..67636336d0 100644 --- a/docs/sdlc-harness/research/external-research.md +++ b/docs/sdlc-harness/research/external-research.md @@ -13,7 +13,7 @@ | [GitHub Copilot 仓库指令](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions) | 支持仓库指令、路径指令、AGENTS.md | 项目规则应优先读取现有资产,并按路径和上下文渐进加载 | | [GitHub Copilot 代码审查](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review) | AI 审查提供评论和建议 | AI 审查默认应是低摩擦建议态,不天然等同阻断审批 | | [Claude Code](https://github.com/anthropics/claude-code) / [权限](https://code.claude.com/docs/en/permissions) / [sandboxing](https://www.anthropic.com/engineering/claude-code-sandboxing) | 终端 Agent、权限配置、沙箱、allow/deny 规则 | 产品需要用技术隔离减少弹窗,同时保留用户可理解的放行路径 | -| [Claude Code plugins](https://code.claude.com/docs/en/plugins-reference) / [plugin marketplaces](https://code.claude.com/docs/en/plugin-marketplaces) | plugin marketplace、user/project/local scope、`.claude-plugin/plugin.json`、skills、agents、hooks、MCP/LSP servers | 插件分发单元、安装作用域和组件目录可以分离;BitFun 兼容时应读取 manifest 和组件事实,不继承外部启用状态或权限决定 | +| [Claude Code plugins](https://code.claude.com/docs/en/plugins-reference) / [plugin marketplaces](https://code.claude.com/docs/en/plugin-marketplaces) | plugin marketplace、user/project/local scope、`.claude-plugin/plugin.json`、skills、agents、hooks、MCP/LSP servers | 插件分发单元、安装作用域和组件目录可以分离;BitFun 兼容时应读取 manifest 和组件事实,不继承外部启用状态或权限决定;LSP 只保留为上游事实和 `unsupported` 诊断,不导入或执行 | | [CodeRabbit configuration](https://docs.coderabbit.ai/reference/configuration) / [路径指令](https://docs.coderabbit.ai/configuration/path-instructions) | 审查强度、路径级指令、规则 | 审查强度和路径规则可配置,默认模式降低噪音 | | [GitLab Duo 自定义指令](https://docs.gitlab.com/user/gitlab_duo/customize_duo/review_instructions/) / [警告模式](https://docs.gitlab.com/user/application_security/policies/merge_request_approval_policies/) | 审查指令、审批策略、警告模式 | 强策略应先建议态/警告校准,再进入强制要求/阻断 | | [Kiro Specs](https://kiro.dev/docs/specs/) / [Steering](https://kiro.dev/docs/steering/) / [Hooks](https://kiro.dev/docs/hooks/) | spec 驱动开发、工作区/全局/团队 steering、加载范围模式、智能体 hooks | 项目知识需要作用域、优先级和加载时机;复杂上下文按条件加载 | diff --git a/packages/dsh-acp/tests/tool-view.spec.ts b/packages/dsh-acp/tests/tool-view.spec.ts new file mode 100644 index 0000000000..ff2594a91f --- /dev/null +++ b/packages/dsh-acp/tests/tool-view.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' + +import { toolKind } from '../src/tool-view.ts' + +describe('harness tool presentation', () => { + it('renders the external harness lsp tool as a search card', () => { + expect(toolKind('lsp')).toBe('search') + }) + + it('keeps unknown tools honest instead of guessing by substring', () => { + expect(toolKind('custom_lsp_wrapper')).toBe('other') + }) +}) diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index 4661c0d1ac..7f95baa339 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -398,7 +398,6 @@ test('Services Core feature-free dependencies stay behind exact text and async I 'filesystem', 'json-io', 'local-storage', - 'lsp', 'permission', 'process-runtime', 'session-search', @@ -699,7 +698,6 @@ test('contract and AI adapter tests keep reviewed feature and failure-domain top name: 'core_type_contracts', path: 'tests/core_type_contracts.rs', leaves: [ - 'tests/core_type_contracts/lsp_contracts.rs', 'tests/core_type_contracts/session_contracts.rs', 'tests/core_type_contracts/session_usage_contracts.rs', 'tests/core_type_contracts/surface_contracts.rs', @@ -1463,7 +1461,6 @@ const SDK_HOST_REVIEWED_CORE_FEATURES = [ 'document-read', 'subscription-auth', 'deep-research', - 'lsp', 'external-sources', 'tools-basic', 'tools-git', @@ -3297,16 +3294,6 @@ test('services-core capability profiles keep heavy owners out of the empty profi 'tokio/io-util', 'tokio/rt', ]); - assert.deepEqual(profiles.get('lsp'), [ - 'dep:anyhow', - 'dep:bitfun-core-types', - 'dep:notify', - 'dep:zip', - 'process-runtime', - 'tokio/fs', - 'tokio/io-util', - 'tokio/sync', - ]); assert.deepEqual(profiles.get('workspace-runtime'), [ 'dep:anyhow', 'dep:async-trait', @@ -3391,7 +3378,6 @@ test('services-core Tokio capabilities stay owner-scoped', () => { 'local-storage': [], 'process-runtime': [], 'workspace-instructions': [], - lsp: [], 'workspace-runtime': [], }, }; @@ -3407,10 +3393,6 @@ test('services-core Tokio capabilities stay owner-scoped', () => { messages.some((message) => message.includes('filesystem missing effective Tokio capabilities: fs')), 'services-core must require filesystem to own tokio/fs', ); - assert.ok( - messages.some((message) => message.includes('lsp missing effective Tokio capabilities')), - 'services-core must require lsp to declare its complete effective Tokio profile', - ); }); test('Services Core accepts only the reviewed feature-owned Tokio runtime graph', () => { @@ -3447,7 +3429,6 @@ test('Services Core accepts only the reviewed feature-owned Tokio runtime graph' ], 'workspace-instructions': ['dep:tokio', 'tokio/fs', 'tokio/io-util', 'tokio/rt'], 'workspace-text-runtime': ['dep:tokio', 'tokio/rt'], - lsp: ['process-runtime', 'tokio/fs', 'tokio/io-util', 'tokio/sync'], 'workspace-runtime': [ 'process-runtime', 'tokio/fs', @@ -3494,7 +3475,6 @@ test('Services Core Tokio owners cannot be hidden behind an unreviewed alias', ( ], 'workspace-instructions': ['dep:tokio', 'tokio/fs', 'tokio/io-util', 'tokio/rt'], 'workspace-text-runtime': ['dep:tokio', 'tokio/rt'], - lsp: ['process-runtime', 'tokio/fs', 'tokio/io-util', 'tokio/sync'], 'workspace-runtime': [ 'process-runtime', 'tokio/fs', @@ -3557,7 +3537,6 @@ test('Core Tokio capabilities cannot hide behind an unreviewed owner feature', ( 'agent-runtime': ['tokio/io-util', 'tokio/macros', 'tokio/rt', 'tokio/time'], 'mcp-runtime': ['agent-runtime', 'tokio/rt-multi-thread'], 'browser-control': ['tokio/net', 'tokio/rt', 'tokio/time'], - lsp: ['tokio/macros'], sneaky: ['agent-runtime', 'browser-control'], }, }; @@ -3579,7 +3558,6 @@ test('reviewed Tokio aggregates cannot declare runtime capabilities directly', ( 'agent-runtime': ['tokio/io-util', 'tokio/macros', 'tokio/rt', 'tokio/time'], 'mcp-runtime': ['agent-runtime', 'tokio/rt-multi-thread'], 'browser-control': ['tokio/net', 'tokio/rt', 'tokio/time'], - lsp: ['tokio/macros'], 'product-full': ['agent-runtime', 'tokio/net'], }, }; diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index 594f88d33b..f1468a6ad2 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -164,7 +164,6 @@ const SERVICES_CORE_TOKIO_FEATURES = new Map([ ['session-search', ['rt']], ['workspace-instructions', ['fs', 'io-util', 'rt']], ['workspace-text-runtime', ['rt']], - ['lsp', ['fs', 'io-util', 'process', 'rt', 'sync', 'time']], ['workspace-runtime', ['fs', 'io-util', 'process', 'rt', 'sync', 'time']], ]); const SERVICES_CORE_BASE_TOKIO_FEATURES = []; @@ -174,7 +173,6 @@ const CORE_TOKIO_FEATURES = new Map([ ['agent-runtime', ['io-util', 'macros', 'rt', 'time']], ['mcp-runtime', ['io-util', 'macros', 'rt', 'rt-multi-thread', 'time']], ['browser-control', ['net', 'rt', 'time']], - ['lsp', ['macros']], ]); const CORE_TOKIO_AGGREGATES = new Set([ 'external-sources', @@ -896,7 +894,6 @@ export function findProductEntrypointCoreFeatureViolations( 'document-read', 'subscription-auth', 'deep-research', - 'lsp', 'external-sources', 'tools-basic', 'tools-git', @@ -938,7 +935,6 @@ export function findProductEntrypointCoreFeatureViolations( 'file-watch', 'filesystem', 'git', - 'lsp', 'local-storage', 'mcp-runtime', 'model-catalog', diff --git a/scripts/core-boundaries/explicit-test-topology.mjs b/scripts/core-boundaries/explicit-test-topology.mjs index 5f5735ea16..6ffe315f7e 100644 --- a/scripts/core-boundaries/explicit-test-topology.mjs +++ b/scripts/core-boundaries/explicit-test-topology.mjs @@ -32,7 +32,6 @@ export const cliIntegrationTestTargets = [ export const servicesCoreIntegrationTestTargets = [ { name: 'markdown_owner_contracts', path: 'tests/markdown_owner_contracts.rs' }, { name: 'declarative_workspace_instruction_contracts', path: 'tests/declarative_workspace_instruction_contracts.rs' }, - { name: 'lsp_plugin_registry_contracts', path: 'tests/lsp_plugin_registry_contracts.rs' }, { name: 'runtime_ownership_contracts', path: 'tests/runtime_ownership_contracts.rs' }, { name: 'local_runtime_ports', path: 'tests/local_runtime_ports.rs' }, { name: 'permission_store_contracts', path: 'tests/permission_store_contracts.rs' }, @@ -126,7 +125,6 @@ export const coreTypesIntegrationTestTargets = [ name: 'core_type_contracts', path: 'tests/core_type_contracts.rs', leaves: [ - 'tests/core_type_contracts/lsp_contracts.rs', 'tests/core_type_contracts/session_contracts.rs', 'tests/core_type_contracts/session_usage_contracts.rs', 'tests/core_type_contracts/surface_contracts.rs', diff --git a/scripts/core-boundaries/rules/crate-rules.mjs b/scripts/core-boundaries/rules/crate-rules.mjs index b46e630bc9..01af08b730 100644 --- a/scripts/core-boundaries/rules/crate-rules.mjs +++ b/scripts/core-boundaries/rules/crate-rules.mjs @@ -554,7 +554,6 @@ export const dependencyProfileRules = [ 'globset', 'ignore', 'libc', - 'notify', 'regex', 'rusqlite', 'serde_yaml', @@ -563,7 +562,6 @@ export const dependencyProfileRules = [ 'which', 'win32job', 'windows', - 'zip', ], }, { diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index d66a2e393f..2113dc6947 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -33,10 +33,10 @@ export const optionalDependencyFeatureOwnerRules = [ reason: 'services-core optional implementation dependencies must stay behind their exact owner capability', dependencies: [ - { depName: 'anyhow', ownerFeatures: ['dispatch-workspace', 'lsp', 'workspace-runtime'] }, + { depName: 'anyhow', ownerFeatures: ['dispatch-workspace', 'workspace-runtime'] }, { depName: 'async-trait', ownerFeatures: ['permission', 'workspace-runtime'] }, { depName: 'base64', ownerFeatures: ['filesystem'] }, - { depName: 'bitfun-core-types', ownerFeatures: ['local-storage', 'lsp'] }, + { depName: 'bitfun-core-types', ownerFeatures: ['local-storage'] }, { depName: 'bitfun-events', ownerFeatures: ['local-storage'] }, { depName: 'bitfun-runtime-ports', ownerFeatures: ['permission', 'workspace-runtime'] }, { depName: 'chrono', ownerFeatures: ['filesystem', 'local-storage'] }, @@ -47,7 +47,6 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'globset', ownerFeatures: ['workspace-instructions'] }, { depName: 'ignore', ownerFeatures: ['filesystem'] }, { depName: 'libc', ownerFeatures: ['local-storage', 'process-runtime'] }, - { depName: 'notify', ownerFeatures: ['lsp'] }, { depName: 'regex', ownerFeatures: [ @@ -74,7 +73,6 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'which', ownerFeatures: ['process-runtime'] }, { depName: 'win32job', ownerFeatures: ['process-runtime'] }, { depName: 'windows', ownerFeatures: ['json-io', 'local-storage', 'process-runtime'] }, - { depName: 'zip', ownerFeatures: ['lsp'] }, { depName: 'tokio', ownerFeatures: [ @@ -82,7 +80,6 @@ export const optionalDependencyFeatureOwnerRules = [ 'filesystem', 'json-io', 'local-storage', - 'lsp', 'permission', 'process-runtime', 'session-search', @@ -229,7 +226,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'serde_yaml', ownerFeatures: ['workspace-runtime'] }, { depName: 'similar', ownerFeatures: ['agent-runtime'] }, { depName: 'terminal-core', ownerFeatures: ['terminal'] }, - { depName: 'notify', ownerFeatures: ['lsp', 'workspace-watch'] }, + { depName: 'notify', ownerFeatures: ['workspace-watch'] }, { depName: 'tokio-tungstenite', ownerFeatures: ['browser-control'] }, { depName: 'tokio-util', ownerFeatures: ['agent-runtime'] }, { depName: 'unic-langid', ownerFeatures: ['i18n-runtime'] }, @@ -667,7 +664,6 @@ export const coreProductFullFeatureAssemblyRule = { 'file-watch', 'filesystem', 'git', - 'lsp', 'local-storage', 'process-runtime', 'external-sources', @@ -695,7 +691,6 @@ export const acpServerCoreFeatures = [ 'document-read', 'subscription-auth', 'deep-research', - 'lsp', 'external-sources', 'tools-basic', 'tools-git', @@ -917,7 +912,6 @@ export const coreClosedFeatureProfileRules = [ allowedTransitiveFeatureRefs: [ 'ai-adapter-runtime', 'filesystem', - 'lsp', 'local-storage', 'process-runtime', 'remote-workspace', @@ -955,7 +949,6 @@ export const coreClosedFeatureProfileRules = [ 'workspace-watch', 'ai-adapter-runtime', 'filesystem', - 'lsp', 'local-storage', 'process-runtime', 'remote-workspace', @@ -1409,22 +1402,6 @@ export const coreClosedFeatureProfileRules = [ exact: true, reason: 'services-core workspace-text-runtime must own only bounded asynchronous local workspace reads', }, - { - manifestPath: 'src/crates/services/services-core/Cargo.toml', - featureName: 'lsp', - requiredFeatureRefs: [ - 'dep:anyhow', - 'dep:bitfun-core-types', - 'dep:notify', - 'dep:zip', - 'process-runtime', - 'tokio/fs', - 'tokio/io-util', - 'tokio/sync', - ], - exact: true, - reason: 'services-core lsp must compose process supervision with only its protocol and watcher capabilities', - }, { manifestPath: 'src/crates/services/services-core/Cargo.toml', featureName: 'workspace-runtime', @@ -1489,13 +1466,6 @@ export const coreClosedFeatureProfileRules = [ exact: true, reason: 'bitfun-core process-runtime must select only managed process and local action owners', }, - { - manifestPath: 'src/crates/assembly/core/Cargo.toml', - featureName: 'lsp', - requiredFeatureRefs: ['dep:notify', 'bitfun-services-core/lsp', 'tokio/macros'], - exact: true, - reason: 'bitfun-core lsp must select only the LSP owner and its workspace watcher dependency', - }, { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'terminal', diff --git a/scripts/core-boundaries/rules/source/forbidden-rules.mjs b/scripts/core-boundaries/rules/source/forbidden-rules.mjs index d9783e11b8..d770787408 100644 --- a/scripts/core-boundaries/rules/source/forbidden-rules.mjs +++ b/scripts/core-boundaries/rules/source/forbidden-rules.mjs @@ -590,46 +590,6 @@ export const forbiddenContentRules = [ }, ], }, - { - path: 'src/crates/assembly/core/src/service/lsp/types.rs', - patterns: [ - { - regex: /\bpub struct LspPlugin\b/, - message: - 'LSP plugin manifest DTO belongs in bitfun-core-types; keep core LSP types as a compatibility facade', - }, - { - regex: /\bpub enum JsonRpcMessage\b/, - message: - 'LSP JSON-RPC DTOs belong in bitfun-core-types; keep core LSP types as a compatibility facade', - }, - { - regex: /\buse serde::\{Deserialize,\s*Serialize\}/, - message: - 'core LSP types should not own serialization DTOs after migration to bitfun-core-types', - }, - ], - }, - { - path: 'src/crates/assembly/core/src/service/lsp/registry.rs', - patterns: [ - { - regex: /\bpub struct PluginRegistry\b/, - message: - 'LSP plugin registry belongs in bitfun-services-core; keep core registry as a compatibility facade', - }, - { - regex: /\bHashMap\b/, - message: - 'core LSP registry must not own plugin index maps after migration to bitfun-services-core', - }, - { - regex: /\bPathBuf::from\(file_path\)/, - message: - 'LSP file-extension lookup belongs in bitfun-services-core registry rules', - }, - ], - }, { path: 'src/crates/assembly/core/src/product_assembly.rs', patterns: [ diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 593ba40e85..71ec5996aa 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -342,129 +342,6 @@ export const requiredContentRules = [ }, ], }, - { - path: 'src/crates/contracts/core-types/src/lsp.rs', - reason: - 'core-types must own shared LSP protocol DTOs, plugin manifest wire contracts, and plugin runtime target DTOs', - patterns: [ - { - regex: /\bpub struct LspPlugin\b/, - message: 'missing LSP plugin manifest contract owner', - }, - { - regex: /\bpub struct ServerConfig\b/, - message: 'missing LSP plugin server config contract', - }, - { - regex: /\bpub enum JsonRpcMessage\b/, - message: 'missing LSP JSON-RPC wire DTO contract', - }, - { - regex: /\bpub enum PluginSource\b/, - message: 'missing LSP plugin source contract', - }, - { - regex: /\bpub struct LspPluginRuntimeTarget\b/, - message: 'missing LSP plugin runtime target contract', - }, - { - regex: /\bpub fn resolve_lsp_plugin_command_for_target\b/, - message: 'missing pure LSP plugin command placeholder resolver', - }, - ], - }, - { - path: 'src/crates/services/services-core/src/lsp.rs', - reason: - 'services-core must own pure LSP plugin registry and current-target mapping rules', - patterns: [ - { - regex: /\bpub struct PluginRegistry\b/, - message: 'missing services-owned LSP plugin registry', - }, - { - regex: /\bpub struct LspSupportedExtensions\b/, - message: 'missing supported extension summary owner', - }, - { - regex: /\bpub fn resolve_plugin_command_for_current_target\b/, - message: 'missing current-target LSP plugin command resolver', - }, - ], - }, - { - path: 'src/crates/assembly/core/src/service/lsp/types.rs', - reason: - 'core LSP types path must remain a compatibility facade over core-types', - patterns: [ - { - regex: /\bpub use bitfun_core_types::lsp::\*/, - message: 'core LSP types must re-export bitfun-core-types contracts', - }, - ], - }, - { - path: 'src/crates/assembly/core/src/service/lsp/registry.rs', - reason: - 'core LSP registry path must remain a compatibility facade over services-core', - patterns: [ - { - regex: /\bpub use bitfun_services_core::lsp::\{/, - message: 'core LSP registry must re-export services-core registry', - }, - ], - }, - { - path: 'src/crates/contracts/core-types/tests/core_type_contracts/lsp_contracts.rs', - reason: - 'core-types must keep LSP manifest serialization, default-value, and placeholder regressions', - patterns: [ - { - regex: /\blsp_plugin_manifest_defaults_preserve_legacy_shape\b/, - message: 'missing LSP manifest default regression', - }, - { - regex: /\blsp_capability_config_missing_fields_default_to_false\b/, - message: 'missing LSP capability default regression', - }, - { - regex: /\blsp_plugin_command_placeholder_resolution_is_contract_owned\b/, - message: 'missing LSP command placeholder contract regression', - }, - ], - }, - { - path: 'src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs', - reason: - 'services-core must keep behavior-equivalence contracts for LSP plugin registry and command mapping', - patterns: [ - { - regex: /\bregistry_preserves_language_extension_and_file_path_lookup\b/, - message: 'missing LSP registry lookup regression', - }, - { - regex: /\bregistry_unregister_removes_plugin_indexes\b/, - message: 'missing LSP registry unregister regression', - }, - { - regex: /\bregistry_unregister_preserves_indexes_owned_by_newer_plugin\b/, - message: - 'missing LSP registry overlapping-plugin unregister regression', - }, - { - regex: /\bregistry_duplicate_and_missing_errors_keep_legacy_messages\b/, - message: 'missing LSP registry error-message regression', - }, - { - regex: /\bregistry_supported_extensions_matches_desktop_api_shape\b/, - message: 'missing LSP supported extension summary regression', - }, - { - regex: /\bplugin_command_placeholder_resolution_is_target_driven\b/, - message: 'missing LSP plugin command placeholder regression', - }, - ], - }, { path: 'src/crates/execution/runtime-services/src/lib.rs', reason: @@ -8651,10 +8528,6 @@ export const requiredContentRules = [ regex: /\bexport async function initializeAllTools\b/, message: 'missing narrow tool startup entry', }, - { - regex: /\binitializeLsp\b/, - message: 'missing LSP startup initializer call', - }, { regex: /\binitializeGit\b/, message: 'missing Git startup initializer call', @@ -10132,10 +10005,6 @@ export const requiredContentRules = [ regex: /#\[cfg\(feature = "dispatch-store"\)\]\s*pub mod dispatch\b/s, message: 'dispatch store facade must stay gated behind dispatch-store', }, - { - regex: /#\[cfg\(feature = "lsp"\)\]\s*pub mod lsp\b/s, - message: 'LSP facade must stay gated behind lsp', - }, { regex: /#\[cfg\(feature = "remote-workspace"\)\]\s*pub mod remote_ssh\b/s, message: 'remote workspace facade must stay gated behind remote-workspace', diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 46dd4966d8..b7cd74873e 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -379,7 +379,6 @@ export function runManifestParserSelfTest({ 'filesystem', 'git', 'i18n-runtime', - 'lsp', 'local-storage', 'process-runtime', 'external-sources', @@ -491,20 +490,6 @@ export function runManifestParserSelfTest({ 'workspace-text-runtime', ['dep:tokio', 'tokio/rt'], ], - [ - servicesCoreManifest, - 'lsp', - [ - 'dep:anyhow', - 'dep:bitfun-core-types', - 'dep:notify', - 'dep:zip', - 'process-runtime', - 'tokio/fs', - 'tokio/io-util', - 'tokio/sync', - ], - ], [ servicesCoreManifest, 'workspace-runtime', @@ -540,7 +525,6 @@ export function runManifestParserSelfTest({ ['dep:bitfun-agent-tools', 'bitfun-services-core/local-storage'], ], [coreManifest, 'process-runtime', ['bitfun-services-core/process-runtime']], - [coreManifest, 'lsp', ['dep:notify', 'bitfun-services-core/lsp', 'tokio/macros']], [coreManifest, 'terminal', ['dep:terminal-core']], [ coreManifest, @@ -638,7 +622,6 @@ export function runManifestParserSelfTest({ 'bitfun-core/document-read', 'bitfun-core/subscription-auth', 'bitfun-core/deep-research', - 'bitfun-core/lsp', 'bitfun-core/external-sources', 'bitfun-core/tools-basic', 'bitfun-core/tools-git', @@ -1158,7 +1141,7 @@ export function runManifestParserSelfTest({ } const expectedServicesCoreOwners = new Map([ ['base64', ['filesystem']], - ['bitfun-core-types', ['local-storage', 'lsp']], + ['bitfun-core-types', ['local-storage']], ['bitfun-events', ['local-storage']], ['chrono', ['filesystem', 'local-storage']], ['chrono-tz', ['token-usage-statistics']], @@ -1167,7 +1150,6 @@ export function runManifestParserSelfTest({ ['globset', ['workspace-instructions']], ['ignore', ['filesystem']], ['libc', ['local-storage', 'process-runtime']], - ['notify', ['lsp']], [ 'regex', ['diagnostics', 'filesystem', 'local-storage', 'markdown', 'workspace-instructions'], @@ -1188,7 +1170,6 @@ export function runManifestParserSelfTest({ ['which', ['process-runtime']], ['win32job', ['process-runtime']], ['windows', ['json-io', 'local-storage', 'process-runtime']], - ['zip', ['lsp']], [ 'tokio', [ @@ -1196,7 +1177,6 @@ export function runManifestParserSelfTest({ 'filesystem', 'json-io', 'local-storage', - 'lsp', 'permission', 'process-runtime', 'workspace-instructions', @@ -4575,7 +4555,7 @@ export function runManifestParserSelfTest({ }, { path: 'src/web-ui/src/tools/initializeTools.ts', - contracts: ['initializeAllTools', 'initializeLsp', 'initializeGit', 'does not import every tool'], + contracts: ['initializeAllTools', 'initializeGit', 'does not import every tool'], }, { path: 'src/web-ui/src/tools/editor/services/MonacoStartupWarmup.ts', diff --git a/scripts/i18n-contract.test.mjs b/scripts/i18n-contract.test.mjs index 58d54eabb5..ebf31ee0af 100644 --- a/scripts/i18n-contract.test.mjs +++ b/scripts/i18n-contract.test.mjs @@ -587,14 +587,14 @@ test('i18n audit can emit a machine-readable governance report', { concurrency: } }); -auditIntegrationTest('i18n audit reports same-text zh-TW copy with a l10n signal', { concurrency: false }, () => { - const localePath = 'src/web-ui/src/locales/zh-TW/settings/lsp.json'; +auditIntegrationTest('i18n audit reports same-text zh-TW copy with a terminology signal', { concurrency: false }, () => { + const localePath = 'src/web-ui/src/locales/zh-TW/settings/external-apps.json'; const reportPath = 'scripts/.tmp-i18n-l10n-signal-report.json'; const absoluteReportPath = path.join(root, reportPath); const source = readText(localePath); const fixture = source.replace( - '"suffix": "瞭解如何開發和貢獻 LSP 插件。"', - '"suffix": "了解如何开发和贡献 LSP 插件。"', + '"summary": "有 {{count}} 項內容需要注意,請查看詳細資料以瞭解影響範圍。"', + '"summary": "有 {{count}} 项内容需要注意,请查看详情了解影响范围。"', ); assert.notEqual(fixture, source, 'test fixture should introduce same-text zh-TW terminology debt'); @@ -609,8 +609,8 @@ auditIntegrationTest('i18n audit reports same-text zh-TW copy with a l10n signal assert.ok( report.l10nQualityCandidates.some((entry) => ( entry.surface === 'web-ui' && - entry.namespace === 'settings/lsp' && - entry.key === 'help.morePlugins.suffix' && + entry.namespace === 'settings/external-apps' && + entry.key === 'diagnostics.summary' && entry.locale === 'zh-TW' && entry.comparisonLocale === 'zh-CN' && entry.signal?.type === 'terminology' && @@ -621,8 +621,8 @@ auditIntegrationTest('i18n audit reports same-text zh-TW copy with a l10n signal assert.ok( report.sameTextLocaleInventory.some((entry) => ( entry.surface === 'web-ui' && - entry.namespace === 'settings/lsp' && - entry.key === 'help.morePlugins.suffix' && + entry.namespace === 'settings/external-apps' && + entry.key === 'diagnostics.summary' && entry.locale === 'zh-TW' && entry.comparisonLocale === 'zh-CN' && entry.signalType === 'terminology' && @@ -739,7 +739,6 @@ auditIntegrationTest('web-ui uses shared terms for stable navigation and feature 'scenes/skills:suite.modes.claw', 'settings:title', 'settings:workspace.title', - 'settings/lsp:tabs.settings', ]); const webUiDuplicates = report.sharedTermDuplicates .filter((entry) => entry.surface === 'web-ui' && migratedResourceKeys.has(entry.resourceKey)) diff --git a/src/apps/cli/Cargo.toml b/src/apps/cli/Cargo.toml index 35f75f58fa..7d9628332e 100644 --- a/src/apps/cli/Cargo.toml +++ b/src/apps/cli/Cargo.toml @@ -37,7 +37,6 @@ bitfun-core = { path = "../../crates/assembly/core", features = [ "subscription-auth", "remote-connect", "deep-research", - "lsp", "external-sources", "plugin-runtime", "ssh-remote", diff --git a/src/apps/cli/src/peer_host/deny.rs b/src/apps/cli/src/peer_host/deny.rs index 6e88539b7b..96b7359259 100644 --- a/src/apps/cli/src/peer_host/deny.rs +++ b/src/apps/cli/src/peer_host/deny.rs @@ -117,7 +117,7 @@ static LOCAL_ONLY_COMMANDS: &[&str] = &[ ]; /// Desktop IDE surfaces that CLI Peer Host does not implement. -/// Prefix match is applied for `lsp_`, `canvas_`, `editor_`, `ssh_`, +/// Prefix match is applied for `canvas_`, `editor_`, `ssh_`, /// `terminal_`, `search_` unless the command is explicitly allowlisted. /// /// `git_*` is intentionally not prefix-denied: `git_is_repository` and the @@ -136,6 +136,11 @@ pub(crate) fn is_local_only_command(command: &str) -> bool { LOCAL_ONLY_COMMANDS.contains(&command) } +/// Commands kept as protocol tombstones after their runtime owner was removed. +pub(crate) fn is_retired_command(command: &str) -> bool { + command.starts_with("lsp_") +} + pub(crate) fn is_cli_unsupported_command(command: &str) -> bool { if command == "search_session_content" { return false; @@ -144,7 +149,6 @@ pub(crate) fn is_cli_unsupported_command(command: &str) -> bool { return true; } let prefixes = [ - "lsp_", "canvas_", "editor_", "ssh_", @@ -159,7 +163,13 @@ pub(crate) fn is_cli_unsupported_command(command: &str) -> bool { #[cfg(test)] mod tests { - use super::{is_cli_unsupported_command, is_local_only_command}; + use super::{is_cli_unsupported_command, is_local_only_command, is_retired_command}; + + #[test] + fn retired_lsp_commands_are_matched_without_overmatching_external_names() { + assert!(is_retired_command("lsp_open_workspace")); + assert!(!is_retired_command("custom_lsp_wrapper")); + } #[test] fn session_content_search_executes_on_the_peer_host() { diff --git a/src/apps/cli/src/peer_host/dispatch.rs b/src/apps/cli/src/peer_host/dispatch.rs index 38435b0b3c..8e39d0e120 100644 --- a/src/apps/cli/src/peer_host/dispatch.rs +++ b/src/apps/cli/src/peer_host/dispatch.rs @@ -8,7 +8,7 @@ use super::commands; use super::control::{ attach_controller, detach_controller, parse_controller_device_id, peer_mode_ping_value, }; -use super::deny::{is_cli_unsupported_command, is_local_only_command}; +use super::deny::{is_cli_unsupported_command, is_local_only_command, is_retired_command}; use super::state::peer_host_state; #[derive(Debug, Clone)] @@ -56,6 +56,12 @@ async fn handle_host_invoke_inner(command: &str, args: Value) -> HostInvokeBridg return HostInvokeBridgeResult::err("HostInvoke command is empty"); } + if is_retired_command(command) { + return HostInvokeBridgeResult::err(format!( + "command '{command}' is unsupported because the BitFun LSP runtime has been retired" + )); + } + // Detached dispatch is target-owned and must not acquire a Peer controller // lease. Route the distinct target command family directly to the durable // CLI runner before applying the generic HostInvoke deny list. @@ -178,6 +184,24 @@ mod tests { } } + #[tokio::test] + async fn retired_lsp_commands_fail_before_peer_state_or_command_dispatch() { + let resp = handle_host_invoke("lsp_open_workspace", json!({})).await; + match resp { + RemoteResponse::HostInvokeResult { + ok: false, + value: None, + error: Some(err), + } => { + assert_eq!( + err, + "command 'lsp_open_workspace' is unsupported because the BitFun LSP runtime has been retired" + ); + } + other => panic!("unexpected response: {other:?}"), + } + } + #[test] fn detached_dispatch_uses_a_distinct_target_command_family() { assert_eq!( diff --git a/src/apps/desktop/src/api/lsp_api.rs b/src/apps/desktop/src/api/lsp_api.rs deleted file mode 100644 index 5b617f95ae..0000000000 --- a/src/apps/desktop/src/api/lsp_api.rs +++ /dev/null @@ -1,429 +0,0 @@ -//! LSP API - -use log::{error, info}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; - -use bitfun_core::service::lsp::types::{CompletionItem, LspPlugin}; -use bitfun_core::service::lsp::{get_global_lsp_manager, initialize_global_lsp_manager}; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StartServerForFileRequest { - pub file_path: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StopServerRequest { - pub language: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DidOpenRequest { - pub language: String, - pub uri: String, - pub text: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DidChangeRequest { - pub language: String, - pub uri: String, - pub version: i32, - pub text: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DidSaveRequest { - pub language: String, - pub uri: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DidCloseRequest { - pub language: String, - pub uri: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsRequest { - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetHoverRequest { - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GotoDefinitionRequest { - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FindReferencesRequest { - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FormatDocumentRequest { - pub language: String, - pub uri: String, - pub tab_size: Option, - pub insert_spaces: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InstallPluginRequest { - pub package_path: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UninstallPluginRequest { - pub plugin_id: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetPluginRequest { - pub plugin_id: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetServerCapabilitiesRequest { - pub language: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StartServerResponse { - pub success: bool, - pub message: String, -} - -#[tauri::command] -pub async fn lsp_initialize() -> Result<(), String> { - initialize_global_lsp_manager() - .await - .map_err(|e| format!("Failed to initialize LSP: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_start_server_for_file( - request: StartServerForFileRequest, -) -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - info!("Starting LSP server for file: {}", request.file_path); - - let guard = manager.read().await; - if let Some(plugin) = guard.find_plugin_by_file(&request.file_path).await { - let language = &plugin.languages[0]; - match guard - .start_server(language, None, None, None, None, None) - .await - { - Ok(_) => Ok(StartServerResponse { - success: true, - message: format!("LSP server started for {}", request.file_path), - }), - Err(e) => { - error!("Failed to start LSP server: {}", e); - Err(format!("Failed to start LSP server: {}", e)) - } - } - } else { - Err(format!( - "No LSP plugin found for file: {}", - request.file_path - )) - } -} - -#[tauri::command] -pub async fn lsp_stop_server(request: StopServerRequest) -> Result<(), String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - guard - .stop_server(&request.language) - .await - .map_err(|e| format!("Failed to stop LSP server: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_stop_all_servers() -> Result<(), String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - guard - .stop_all_servers() - .await - .map_err(|e| format!("Failed to stop all LSP servers: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_did_open(request: DidOpenRequest) -> Result<(), String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - guard - .did_open(&request.language, &request.uri, &request.text) - .await - .map_err(|e| format!("Failed to send didOpen: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_did_change(request: DidChangeRequest) -> Result<(), String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - guard - .did_change( - &request.language, - &request.uri, - request.version, - &request.text, - ) - .await - .map_err(|e| format!("Failed to send didChange: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_did_save(request: DidSaveRequest) -> Result<(), String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - guard - .did_save(&request.language, &request.uri) - .await - .map_err(|e| format!("Failed to send didSave: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_did_close(request: DidCloseRequest) -> Result<(), String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - guard - .did_close(&request.language, &request.uri) - .await - .map_err(|e| format!("Failed to send didClose: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_get_completions( - request: GetCompletionsRequest, -) -> Result, String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let items = guard - .get_completions( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to get completions: {}", e))?; - - Ok(items) -} - -#[tauri::command] -pub async fn lsp_get_hover(request: GetHoverRequest) -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let hover = guard - .get_hover( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to get hover: {}", e))?; - - Ok(hover) -} - -#[tauri::command] -pub async fn lsp_goto_definition( - request: GotoDefinitionRequest, -) -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let definition = guard - .goto_definition( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to goto definition: {}", e))?; - - Ok(definition) -} - -#[tauri::command] -pub async fn lsp_find_references( - request: FindReferencesRequest, -) -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let references = guard - .find_references( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to find references: {}", e))?; - - Ok(references) -} - -#[tauri::command] -pub async fn lsp_format_document( - request: FormatDocumentRequest, -) -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let tab_size = request.tab_size.unwrap_or(4); - let insert_spaces = request.insert_spaces.unwrap_or(true); - - let guard = manager.read().await; - let edits = guard - .format_document(&request.language, &request.uri, tab_size, insert_spaces) - .await - .map_err(|e| format!("Failed to format document: {}", e))?; - - Ok(edits) -} - -#[tauri::command] -pub async fn lsp_install_plugin(request: InstallPluginRequest) -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let package_path = PathBuf::from(request.package_path); - - let guard = manager.read().await; - let plugin_id = guard - .install_plugin(package_path) - .await - .map_err(|e| format!("Failed to install plugin: {}", e))?; - - Ok(plugin_id) -} - -#[tauri::command] -pub async fn lsp_uninstall_plugin(request: UninstallPluginRequest) -> Result<(), String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - guard - .uninstall_plugin(&request.plugin_id) - .await - .map_err(|e| format!("Failed to uninstall plugin: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_list_plugins() -> Result, String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let plugins = guard.list_plugins().await; - Ok(plugins) -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SupportedExtensionsResponse { - pub extension_to_language: HashMap, - pub supported_languages: Vec, -} - -#[tauri::command] -pub async fn lsp_get_supported_extensions() -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let summary = guard.supported_extensions().await; - - Ok(SupportedExtensionsResponse { - extension_to_language: summary.extension_to_language, - supported_languages: summary.supported_languages, - }) -} - -#[tauri::command] -pub async fn lsp_get_plugin(request: GetPluginRequest) -> Result, String> { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let plugin = guard.get_plugin(&request.plugin_id).await; - Ok(plugin) -} - -#[tauri::command] -pub async fn lsp_get_server_capabilities( - request: GetServerCapabilitiesRequest, -) -> Result { - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; - - let guard = manager.read().await; - let capabilities = guard - .get_server_capabilities(&request.language) - .await - .map_err(|e| format!("Failed to get server capabilities: {}", e))?; - - Ok(capabilities) -} diff --git a/src/apps/desktop/src/api/lsp_workspace_api.rs b/src/apps/desktop/src/api/lsp_workspace_api.rs deleted file mode 100644 index b16f154fb2..0000000000 --- a/src/apps/desktop/src/api/lsp_workspace_api.rs +++ /dev/null @@ -1,623 +0,0 @@ -//! Workspace-level LSP API - -use log::error; -use serde::Deserialize; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; - -use bitfun_core::service::lsp::types::CompletionItem; -use bitfun_core::service::lsp::{ - close_workspace, get_workspace_manager, open_workspace_with_emitter, ServerState, -}; -use bitfun_transport::{TauriTransportAdapter, TransportEmitter}; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OpenWorkspaceRequest { - pub workspace_path: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OpenDocumentRequest { - pub workspace_path: String, - pub uri: String, - pub language: String, - pub content: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChangeDocumentRequest { - pub workspace_path: String, - pub uri: String, - pub content: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SaveDocumentRequest { - pub workspace_path: String, - pub uri: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CloseDocumentRequest { - pub workspace_path: String, - pub uri: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetHoverRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GotoDefinitionRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FindReferencesRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCodeActionsRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub range: serde_json::Value, - pub context: serde_json::Value, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FormatDocumentRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub tab_size: Option, - pub insert_spaces: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetInlayHintsRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub start_line: u32, - pub start_character: u32, - pub end_line: u32, - pub end_character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RenameRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, - pub new_name: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetDocumentHighlightRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub line: u32, - pub character: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetServerStateRequest { - pub workspace_path: String, - pub language: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetDocumentSymbolsRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSemanticTokensRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSemanticTokensRangeRequest { - pub workspace_path: String, - pub language: String, - pub uri: String, - pub start_line: u32, - pub start_character: u32, - pub end_line: u32, - pub end_character: u32, -} - -#[tauri::command] -pub async fn lsp_open_workspace( - request: OpenWorkspaceRequest, - app_handle: tauri::AppHandle, -) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - - let transport = Arc::new(TauriTransportAdapter::new(app_handle)); - let emitter: Arc = - crate::api::remote_connect_api::wrap_peer_aware_emitter(Arc::new(TransportEmitter::new( - transport, - ))); - - open_workspace_with_emitter(workspace_path, Some(emitter)) - .await - .map_err(|e| format!("Failed to open workspace: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_close_workspace(request: OpenWorkspaceRequest) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - close_workspace(workspace_path) - .await - .map_err(|e| format!("Failed to close workspace: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_open_document(request: OpenDocumentRequest) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - - let manager = get_workspace_manager(workspace_path.clone()) - .await - .map_err(|e| { - let error_msg = format!("Workspace not found: {}", e); - error!( - "Workspace not found: workspace_path={:?}, error={}", - workspace_path, e - ); - error_msg - })?; - - manager - .open_document( - request.uri.clone(), - request.language.clone(), - request.content, - ) - .await - .map_err(|e| { - let error_msg = format!("Failed to open document: {}", e); - error!( - "Failed to open document: uri={}, language={}, error={}", - request.uri, request.language, e - ); - error_msg - })?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_change_document(request: ChangeDocumentRequest) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .change_document(request.uri, request.content) - .await - .map_err(|e| format!("Failed to change document: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_save_document(request: SaveDocumentRequest) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .save_document(request.uri) - .await - .map_err(|e| format!("Failed to save document: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_close_document(request: CloseDocumentRequest) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .close_document(request.uri) - .await - .map_err(|e| format!("Failed to close document: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_get_completions_workspace( - request: GetCompletionsRequest, -) -> Result, String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .get_completions( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to get completions: {}", e)) -} - -#[tauri::command] -pub async fn lsp_get_hover_workspace( - request: GetHoverRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .get_hover( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to get hover: {}", e)) -} - -#[tauri::command] -pub async fn lsp_goto_definition_workspace( - request: GotoDefinitionRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .goto_definition( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to goto definition: {}", e)) -} - -#[tauri::command] -pub async fn lsp_find_references_workspace( - request: FindReferencesRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .find_references( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to find references: {}", e)) -} - -#[tauri::command] -pub async fn lsp_get_code_actions_workspace( - request: GetCodeActionsRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .get_code_actions( - &request.language, - &request.uri, - request.range, - request.context, - ) - .await - .map_err(|e| format!("Failed to get code actions: {}", e)) -} - -#[tauri::command] -pub async fn lsp_format_document_workspace( - request: FormatDocumentRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .format_document( - &request.language, - &request.uri, - request.tab_size.unwrap_or(4), - request.insert_spaces.unwrap_or(true), - ) - .await - .map_err(|e| format!("Failed to format document: {}", e)) -} - -#[tauri::command] -pub async fn lsp_get_inlay_hints_workspace( - request: GetInlayHintsRequest, -) -> Result, String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .get_inlay_hints( - &request.language, - &request.uri, - request.start_line, - request.start_character, - request.end_line, - request.end_character, - ) - .await - .map_err(|e| format!("Failed to get inlay hints: {}", e)) -} - -#[tauri::command] -pub async fn lsp_rename_workspace(request: RenameRequest) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .rename( - &request.language, - &request.uri, - request.line, - request.character, - &request.new_name, - ) - .await - .map_err(|e| format!("Failed to rename: {}", e)) -} - -#[tauri::command] -pub async fn lsp_get_document_highlight_workspace( - request: GetDocumentHighlightRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .get_document_highlight( - &request.language, - &request.uri, - request.line, - request.character, - ) - .await - .map_err(|e| format!("Failed to get document highlight: {}", e)) -} - -#[tauri::command] -pub async fn lsp_get_server_state(request: GetServerStateRequest) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - let state = manager.get_server_state(&request.language).await; - Ok(state) -} - -#[tauri::command] -pub async fn lsp_get_all_server_states( - request: OpenWorkspaceRequest, -) -> Result, String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - let states = manager.get_all_server_states().await; - Ok(states) -} - -#[tauri::command] -pub async fn lsp_stop_server_workspace(request: GetServerStateRequest) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .stop_server(&request.language) - .await - .map_err(|e| format!("Failed to stop server: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_list_workspaces() -> Result, String> { - use bitfun_core::service::lsp::get_all_workspace_paths; - - let workspaces = get_all_workspace_paths() - .await - .map_err(|e| format!("Failed to get workspaces: {}", e))?; - - Ok(workspaces) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DetectProjectRequest { - pub workspace_path: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PrestartServerRequest { - pub workspace_path: String, - pub language: String, -} - -#[tauri::command] -pub async fn lsp_detect_project( - request: DetectProjectRequest, -) -> Result { - use bitfun_core::service::lsp::project_detector::ProjectDetector; - - let workspace_path = PathBuf::from(&request.workspace_path); - let project_info = ProjectDetector::detect(&workspace_path) - .await - .map_err(|e| format!("Failed to detect project: {}", e))?; - - serde_json::to_value(&project_info) - .map_err(|e| format!("Failed to serialize project info: {}", e)) -} - -#[tauri::command] -pub async fn lsp_prestart_server(request: PrestartServerRequest) -> Result<(), String> { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - manager - .prestart_server(&request.language) - .await - .map_err(|e| format!("Failed to pre-start server: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn lsp_get_document_symbols_workspace( - request: GetDocumentSymbolsRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - let symbols = manager - .get_document_symbols(&request.language, &request.uri) - .await - .map_err(|e| format!("Failed to get document symbols: {}", e))?; - - Ok(symbols) -} - -#[tauri::command] -pub async fn lsp_get_semantic_tokens_workspace( - request: GetSemanticTokensRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - let tokens = manager - .get_semantic_tokens(&request.language, &request.uri) - .await - .map_err(|e| format!("Failed to get semantic tokens: {}", e))?; - - Ok(tokens) -} - -#[tauri::command] -pub async fn lsp_get_semantic_tokens_range_workspace( - request: GetSemanticTokensRangeRequest, -) -> Result { - let workspace_path = PathBuf::from(&request.workspace_path); - let manager = get_workspace_manager(workspace_path) - .await - .map_err(|e| format!("Workspace not found: {}", e))?; - - let range = serde_json::json!({ - "start": { - "line": request.start_line, - "character": request.start_character - }, - "end": { - "line": request.end_line, - "character": request.end_character - } - }); - - let tokens = manager - .get_semantic_tokens_range(&request.language, &request.uri, range) - .await - .map_err(|e| format!("Failed to get semantic tokens range: {}", e))?; - - Ok(tokens) -} diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index f7adf9ce6f..85c9986458 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -29,8 +29,6 @@ pub mod git_agent_api; pub mod git_api; pub mod i18n_api; pub mod insights_api; -pub mod lsp_api; -pub mod lsp_workspace_api; pub mod mcp_api; pub mod miniapp_agent_api; pub mod miniapp_api; diff --git a/src/apps/desktop/src/api/peer_host_invoke.rs b/src/apps/desktop/src/api/peer_host_invoke.rs index cf64e056fb..07066729a1 100644 --- a/src/apps/desktop/src/api/peer_host_invoke.rs +++ b/src/apps/desktop/src/api/peer_host_invoke.rs @@ -204,6 +204,11 @@ pub fn is_local_only_command(command: &str) -> bool { LOCAL_ONLY_COMMANDS.contains(&command) } +/// Commands kept as protocol tombstones after their runtime owner was removed. +pub fn is_retired_command(command: &str) -> bool { + command.starts_with("lsp_") +} + /// Register a controller device id to receive peer UI events. pub fn attach_controller(device_id: String) { if let Ok(mut state) = peer_control_state().lock() { @@ -390,6 +395,15 @@ pub async fn dispatch(command: &str, args: Value) -> HostInvokeBridgeResult { error: Some("HostInvoke command is empty".to_string()), }; } + if is_retired_command(command) { + return HostInvokeBridgeResult { + ok: false, + value: None, + error: Some(format!( + "command '{command}' is unsupported because the BitFun LSP runtime has been retired" + )), + }; + } if is_local_only_command(command) { return HostInvokeBridgeResult { ok: false, @@ -548,6 +562,19 @@ mod tests { assert!(!is_local_only_command("git_get_repository_trust")); } + #[tokio::test] + async fn retired_lsp_commands_fail_before_the_webview_bridge() { + let result = dispatch("lsp_open_workspace", serde_json::json!({})).await; + assert!(!result.ok); + assert!(result.value.is_none()); + assert_eq!( + result.error.as_deref(), + Some( + "command 'lsp_open_workspace' is unsupported because the BitFun LSP runtime has been retired" + ) + ); + } + #[test] fn only_the_final_detach_drains_peer_permission_requests() { let mut state = control_state(&["controller-a", "controller-b"], &["request-1"]); diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 66881b866f..de6341d2f8 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -573,7 +573,6 @@ fn should_fanout_peer_ui_event(event: &str) -> bool { event, "terminal_event" | "file-system-changed" - | "lsp-event" | "backend-event-mcpinteractionrequest" | "backend-event-acppermissionrequest" | "backend-event-toolexecutionprogress" diff --git a/src/apps/desktop/src/api/remote_workspace_policy.rs b/src/apps/desktop/src/api/remote_workspace_policy.rs index 1e6ac055ad..c19599c7aa 100644 --- a/src/apps/desktop/src/api/remote_workspace_policy.rs +++ b/src/apps/desktop/src/api/remote_workspace_policy.rs @@ -1000,133 +1000,6 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] = "logout_subscription_account", RemoteWorkspacePolicy::LocalOnly, ), - ( - "lsp_change_document", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_close_document", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_close_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_detect_project", RemoteWorkspacePolicy::LegacyUnaudited), - ("lsp_did_change", RemoteWorkspacePolicy::LegacyUnaudited), - ("lsp_did_close", RemoteWorkspacePolicy::LegacyUnaudited), - ("lsp_did_open", RemoteWorkspacePolicy::LegacyUnaudited), - ("lsp_did_save", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_find_references", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_find_references_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_format_document", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_format_document_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_all_server_states", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_code_actions_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_completions", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_completions_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_document_highlight_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_document_symbols_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_get_hover", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_get_hover_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_inlay_hints_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_get_plugin", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_get_semantic_tokens_range_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_semantic_tokens_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_server_capabilities", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_server_state", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_get_supported_extensions", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_goto_definition", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_goto_definition_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_initialize", RemoteWorkspacePolicy::LegacyUnaudited), - ("lsp_install_plugin", RemoteWorkspacePolicy::LegacyUnaudited), - ("lsp_list_plugins", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_list_workspaces", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_open_document", RemoteWorkspacePolicy::LegacyUnaudited), - ("lsp_open_workspace", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_prestart_server", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_rename_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_save_document", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_start_server_for_file", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_stop_all_servers", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ("lsp_stop_server", RemoteWorkspacePolicy::LegacyUnaudited), - ( - "lsp_stop_server_workspace", - RemoteWorkspacePolicy::LegacyUnaudited, - ), - ( - "lsp_uninstall_plugin", - RemoteWorkspacePolicy::LegacyUnaudited, - ), ( "mark_announcement_seen", RemoteWorkspacePolicy::WorkspaceAgnostic, @@ -2452,49 +2325,6 @@ mod tests { "load_mcp_json_config", "load_persisted_session_metadata", "load_session_turns", - "lsp_change_document", - "lsp_close_document", - "lsp_close_workspace", - "lsp_detect_project", - "lsp_did_change", - "lsp_did_close", - "lsp_did_open", - "lsp_did_save", - "lsp_find_references", - "lsp_find_references_workspace", - "lsp_format_document", - "lsp_format_document_workspace", - "lsp_get_all_server_states", - "lsp_get_code_actions_workspace", - "lsp_get_completions", - "lsp_get_completions_workspace", - "lsp_get_document_highlight_workspace", - "lsp_get_document_symbols_workspace", - "lsp_get_hover", - "lsp_get_hover_workspace", - "lsp_get_inlay_hints_workspace", - "lsp_get_plugin", - "lsp_get_semantic_tokens_range_workspace", - "lsp_get_semantic_tokens_workspace", - "lsp_get_server_capabilities", - "lsp_get_server_state", - "lsp_get_supported_extensions", - "lsp_goto_definition", - "lsp_goto_definition_workspace", - "lsp_initialize", - "lsp_install_plugin", - "lsp_list_plugins", - "lsp_list_workspaces", - "lsp_open_document", - "lsp_open_workspace", - "lsp_prestart_server", - "lsp_rename_workspace", - "lsp_save_document", - "lsp_start_server_for_file", - "lsp_stop_all_servers", - "lsp_stop_server", - "lsp_stop_server_workspace", - "lsp_uninstall_plugin", "miniapp_agent_cancel", "miniapp_agent_cancel_stale_runs", "miniapp_agent_run", diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 601abe25d0..f32b5f7752 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -76,8 +76,6 @@ use api::external_sources_api::*; use api::git_agent_api::*; use api::git_api::*; use api::i18n_api::*; -use api::lsp_api::*; -use api::lsp_workspace_api::*; use api::mcp_api::*; use api::review_platform_api::*; use api::runtime_api::*; @@ -1600,49 +1598,6 @@ pub async fn run() { get_acp_session_commands, set_acp_session_model, set_acp_session_config_option, - lsp_initialize, - lsp_start_server_for_file, - lsp_stop_server, - lsp_stop_all_servers, - lsp_did_open, - lsp_did_change, - lsp_did_save, - lsp_did_close, - lsp_get_completions, - lsp_get_hover, - lsp_goto_definition, - lsp_find_references, - lsp_format_document, - lsp_install_plugin, - lsp_uninstall_plugin, - lsp_list_plugins, - lsp_get_plugin, - lsp_get_server_capabilities, - lsp_get_supported_extensions, - lsp_open_workspace, - lsp_close_workspace, - lsp_open_document, - lsp_change_document, - lsp_save_document, - lsp_close_document, - lsp_get_completions_workspace, - lsp_get_hover_workspace, - lsp_goto_definition_workspace, - lsp_find_references_workspace, - lsp_get_code_actions_workspace, - lsp_format_document_workspace, - lsp_get_inlay_hints_workspace, - lsp_rename_workspace, - lsp_get_document_highlight_workspace, - lsp_get_document_symbols_workspace, - lsp_get_semantic_tokens_workspace, - lsp_get_semantic_tokens_range_workspace, - lsp_get_server_state, - lsp_get_all_server_states, - lsp_stop_server_workspace, - lsp_list_workspaces, - lsp_detect_project, - lsp_prestart_server, reload_global_config, get_global_config_status, subscribe_config_updates, @@ -2540,10 +2495,6 @@ fn init_services(app_handle: tauri::AppHandle, default_log_level: log::LevelFilt ); } - if let Err(e) = service::lsp::initialize_global_lsp_manager().await { - log::error!("Failed to initialize LSP manager: {}", e); - } - let event_system = infrastructure::events::get_global_event_system(); event_system.set_emitter(emitter).await; }); diff --git a/src/apps/sdk-host/Cargo.toml b/src/apps/sdk-host/Cargo.toml index f513aaffab..220ba0f1d3 100644 --- a/src/apps/sdk-host/Cargo.toml +++ b/src/apps/sdk-host/Cargo.toml @@ -19,7 +19,6 @@ bitfun-core = { path = "../../crates/assembly/core", features = [ "document-read", "subscription-auth", "deep-research", - "lsp", "external-sources", "tools-basic", "tools-git", diff --git a/src/apps/server/src/bootstrap.rs b/src/apps/server/src/bootstrap.rs index c91fb625ec..e5eeb9643e 100644 --- a/src/apps/server/src/bootstrap.rs +++ b/src/apps/server/src/bootstrap.rs @@ -218,11 +218,6 @@ pub(crate) async fn initialize(workspace: Option) -> anyhow::Result Session -> DialogTurn -> ModelRound - CLI/ACP 的闭包检查遵循 Cargo resolver-v2,保持 normal 与 host(build/proc-macro)feature context 相互隔离; 但同一 context 内的所有 target-specific 声明都属于同一个已评审架构边界。平台确实需要不同 owner 时,应拆分清晰的 package/module 归属;不得用互斥 Cargo `cfg` 隐藏未评审的 Core 能力。 -- 保持轻量兼容 feature 可独立编译。本地服务 profile 为 `dispatch-store`、`lsp`、`terminal`、 +- 保持轻量兼容 feature 可独立编译。本地服务 profile 为 `dispatch-store`、`terminal`、 `workspace-runtime` 和 `workspace-watch`;`remote-workspace` 只增加远程工作区 facade, `ssh-remote` 才增加具体 SSH transport。`announcement`、`file-watch`、`git`、 `review-platform` 也保持独立,`service-integrations` 只是其兼容聚合。任何窄 feature 都不得直接或间接启用 `product-full`。 diff --git a/src/crates/assembly/core/AGENTS.md b/src/crates/assembly/core/AGENTS.md index 4531bece6d..36c2f0edb0 100644 --- a/src/crates/assembly/core/AGENTS.md +++ b/src/crates/assembly/core/AGENTS.md @@ -17,7 +17,7 @@ and `docs/architecture/agent-runtime-services-design.md`. Main areas: - `src/agentic/`: agents, prompts, tools, sessions, execution, persistence -- `src/service/`: config, filesystem, terminal, git, LSP, MCP, remote connect, AI memory +- `src/service/`: config, filesystem, terminal, git, MCP, remote connect, AI memory - `src/infrastructure/`: AI clients, app paths, event system, storage, debug log server - `src/product_runtime/`: Core Agent Runtime compatibility adapters and runtime service provider wiring @@ -113,7 +113,7 @@ SessionManager -> Session -> DialogTurn -> ModelRound not hide an unreviewed Core capability behind mutually exclusive Cargo `cfg` branches. - Keep the light compatibility features independently compilable. Local service - profiles are `dispatch-store`, `lsp`, `terminal`, `workspace-runtime`, and + profiles are `dispatch-store`, `terminal`, `workspace-runtime`, and `workspace-watch`; `remote-workspace` adds only the remote workspace facade, while `ssh-remote` adds concrete SSH transport. Integration facades `announcement`, `file-watch`, `git`, and `review-platform` remain independent, diff --git a/src/crates/assembly/core/Cargo.toml b/src/crates/assembly/core/Cargo.toml index 18e541e5bc..398d7b56db 100644 --- a/src/crates/assembly/core/Cargo.toml +++ b/src/crates/assembly/core/Cargo.toml @@ -176,7 +176,6 @@ product-full = [ "dispatch-store", "filesystem", "git", - "lsp", "local-storage", "process-runtime", "remote-workspace", @@ -404,7 +403,6 @@ dispatch-store = ["dep:base64", "local-storage", "bitfun-services-core/dispatch- filesystem = ["bitfun-services-core/filesystem"] local-storage = ["dep:bitfun-agent-tools", "bitfun-services-core/local-storage"] process-runtime = ["bitfun-services-core/process-runtime"] -lsp = ["dep:notify", "bitfun-services-core/lsp", "tokio/macros"] terminal = ["dep:terminal-core"] workspace-runtime = [ "dep:serde_yaml", diff --git a/src/crates/assembly/core/builtin_skills/create-bitfun-skin/references/appearance-registry.json b/src/crates/assembly/core/builtin_skills/create-bitfun-skin/references/appearance-registry.json index 23be63c8ba..0a6aff8aff 100644 --- a/src/crates/assembly/core/builtin_skills/create-bitfun-skin/references/appearance-registry.json +++ b/src/crates/assembly/core/builtin_skills/create-bitfun-skin/references/appearance-registry.json @@ -1,9 +1,9 @@ { "schema": "bitfun.appearance.registry", "schemaVersion": 1, - "generatedFrom": "bc77e2c28c7b847abc175b6307ac18f84a89265a", - "generatedAt": "2026-08-16T16:09:40.016459+00:00", - "sourceRevision": "bc77e2c28c7b847abc175b6307ac18f84a89265a", + "generatedFrom": "a47c21a1a5b0b28ef9d2c1e17b9e1cea1be6f94c", + "generatedAt": "2026-08-22T14:28:57.698830+00:00", + "sourceRevision": "a47c21a1a5b0b28ef9d2c1e17b9e1cea1be6f94c", "sourceDirty": false, "sourceTreeHash": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", "components": [ @@ -32,6 +32,7 @@ "attribute": "data-bf-variant", "values": [ "primary", + "inverse", "secondary", "ghost", "dashed", @@ -367,7 +368,8 @@ "small", "medium", "large", - "xlarge" + "xlarge", + "xxlarge" ] }, { @@ -575,6 +577,69 @@ } ] }, + { + "id": "popup-close-button", + "parts": [ + { + "id": "root", + "propertyProfile": "control", + "visualRole": "control" + } + ], + "facets": [ + { + "id": "variant", + "attribute": "data-bf-variant", + "values": [ + "ghost" + ] + }, + { + "id": "size", + "attribute": "data-bf-size", + "values": [ + "medium" + ] + }, + { + "id": "shape", + "attribute": "data-bf-shape", + "values": [ + "square" + ] + } + ], + "states": [ + { + "id": "hover", + "selector": { + "kind": "self", + "suffix": ":hover:not(:disabled)" + } + }, + { + "id": "active", + "selector": { + "kind": "self", + "suffix": ":active:not(:disabled)" + } + }, + { + "id": "focusVisible", + "selector": { + "kind": "self", + "suffix": ":focus-visible" + } + }, + { + "id": "disabled", + "selector": { + "kind": "self", + "suffix": ":disabled" + } + } + ] + }, { "id": "checkbox", "parts": [ @@ -2037,22 +2102,6 @@ "id": "blockquote", "propertyProfile": "container" }, - { - "id": "reproduction", - "propertyProfile": "container" - }, - { - "id": "reproductionHeader", - "propertyProfile": "container" - }, - { - "id": "reproductionContent", - "propertyProfile": "container" - }, - { - "id": "reproductionActions", - "propertyProfile": "container" - }, { "id": "math", "propertyProfile": "container" @@ -2430,6 +2479,7 @@ "attribute": "data-bf-boost-item-kind", "values": [ "mode", + "agent", "context", "skill", "manage" @@ -2504,6 +2554,76 @@ } ] }, + { + "id": "harness-selector", + "parts": [ + { + "id": "root", + "propertyProfile": "container" + }, + { + "id": "trigger", + "propertyProfile": "container" + }, + { + "id": "menu", + "propertyProfile": "container" + }, + { + "id": "profile", + "propertyProfile": "container" + } + ], + "facets": [ + { + "id": "profile", + "attribute": "data-bf-profile", + "values": [ + "minimal", + "balanced", + "ultimate", + "creative" + ] + } + ], + "states": [ + { + "id": "open", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"open\"]" + } + }, + { + "id": "current", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"current\"]" + } + }, + { + "id": "pending", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"pending\"]" + } + }, + { + "id": "newSession", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"new-session\"]" + } + }, + { + "id": "comingSoon", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"coming-soon\"]" + } + } + ] + }, { "id": "composer-voice-input", "parts": [ @@ -2519,6 +2639,14 @@ "id": "pill", "propertyProfile": "container" }, + { + "id": "setupPill", + "propertyProfile": "container" + }, + { + "id": "setupMessage", + "propertyProfile": "container" + }, { "id": "status", "propertyProfile": "container" @@ -2550,6 +2678,8 @@ "attribute": "data-bf-phase", "values": [ "idle", + "setup", + "downloading", "preparing", "recording", "transcribing" @@ -2559,6 +2689,8 @@ "id": "action", "attribute": "data-bf-action", "values": [ + "install", + "dismiss", "cancel", "transcribe", "send" @@ -3056,15 +3188,19 @@ "propertyProfile": "container" }, { - "id": "label", + "id": "menu", "propertyProfile": "container" }, { - "id": "menu", + "id": "header", "propertyProfile": "container" }, { - "id": "header", + "id": "auto", + "propertyProfile": "container" + }, + { + "id": "options", "propertyProfile": "container" }, { @@ -3113,12 +3249,32 @@ "id": "actions", "propertyProfile": "container" }, + { + "id": "sessionOverview", + "propertyProfile": "container" + }, + { + "id": "sessionOverviewTrigger", + "propertyProfile": "container" + }, + { + "id": "sessionOverviewPanel", + "propertyProfile": "container" + }, + { + "id": "sessionOverviewList", + "propertyProfile": "container" + }, + { + "id": "sessionOverviewItem", + "propertyProfile": "container" + }, { "id": "backgroundActivity", "propertyProfile": "container" }, { - "id": "activityPanel", + "id": "rightPanelTrigger", "propertyProfile": "container" }, { @@ -3165,6 +3321,10 @@ "id": "sessionTreeNode", "propertyProfile": "container" }, + { + "id": "sessionTreeNodeTitle", + "propertyProfile": "container" + }, { "id": "sessionTreeMenu", "propertyProfile": "container" @@ -3201,6 +3361,13 @@ "suffix": "[data-bf-state~=\"open\"]" } }, + { + "id": "collapsed", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"collapsed\"]" + } + }, { "id": "active", "selector": { @@ -3229,6 +3396,13 @@ "suffix": "[data-bf-state~=\"empty\"]" } }, + { + "id": "unavailable", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"unavailable\"]" + } + }, { "id": "error", "selector": { @@ -3496,6 +3670,10 @@ "id": "column", "propertyProfile": "container" }, + { + "id": "sectionHeading", + "propertyProfile": "container" + }, { "id": "field", "propertyProfile": "container" @@ -3520,6 +3698,14 @@ "id": "tool", "propertyProfile": "container" }, + { + "id": "promptEditor", + "propertyProfile": "container" + }, + { + "id": "contextPreview", + "propertyProfile": "container" + }, { "id": "prompt", "propertyProfile": "container" @@ -3665,6 +3851,10 @@ "id": "action", "propertyProfile": "container" }, + { + "id": "subagentName", + "propertyProfile": "container" + }, { "id": "meta", "propertyProfile": "container" @@ -3717,7 +3907,7 @@ ] }, { - "id": "basics-config", + "id": "application-settings", "parts": [ { "id": "root", @@ -4228,11 +4418,23 @@ "propertyProfile": "container" }, { - "id": "zones", + "id": "summary", "propertyProfile": "container" }, { - "id": "split", + "id": "tabs", + "propertyProfile": "container" + }, + { + "id": "filters", + "propertyProfile": "container" + }, + { + "id": "list", + "propertyProfile": "container" + }, + { + "id": "listHeader", "propertyProfile": "container" }, { @@ -4309,6 +4511,20 @@ "kind": "self", "suffix": "[data-bf-state~=\"collapsed\"]" } + }, + { + "id": "unavailable", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"unavailable\"]" + } + }, + { + "id": "changed", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"changed\"]" + } } ] }, @@ -5117,10 +5333,6 @@ "id": "image", "propertyProfile": "container" }, - { - "id": "blocks", - "propertyProfile": "container" - }, { "id": "lightbox", "propertyProfile": "container" @@ -5409,6 +5621,20 @@ ], "facets": [], "states": [ + { + "id": "active", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"active\"]" + } + }, + { + "id": "completed", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"completed\"]" + } + }, { "id": "cancelled", "selector": { @@ -5471,7 +5697,7 @@ ] }, { - "id": "appearance-config", + "id": "appearance-settings", "parts": [ { "id": "root", @@ -5509,14 +5735,6 @@ "id": "packageSection", "propertyProfile": "container" }, - { - "id": "packageGrid", - "propertyProfile": "container" - }, - { - "id": "packageCard", - "propertyProfile": "container" - }, { "id": "packageSelect", "propertyProfile": "container" @@ -5525,22 +5743,10 @@ "id": "packagePreview", "propertyProfile": "container" }, - { - "id": "packageCardBody", - "propertyProfile": "container" - }, { "id": "packageActions", "propertyProfile": "container" }, - { - "id": "packageActiveIndicator", - "propertyProfile": "container" - }, - { - "id": "packageEmpty", - "propertyProfile": "container" - }, { "id": "packageDiagnostics", "propertyProfile": "container" @@ -5716,26 +5922,7 @@ ] }, { - "id": "lsp-config", - "parts": [ - { - "id": "root", - "propertyProfile": "container" - }, - { - "id": "message", - "propertyProfile": "container" - }, - { - "id": "plugins", - "propertyProfile": "container" - } - ], - "facets": [], - "states": [] - }, - { - "id": "config", + "id": "config", "parts": [ { "id": "root", @@ -5837,10 +6024,6 @@ "id": "collectionList", "propertyProfile": "container" }, - { - "id": "subagentModel", - "propertyProfile": "container" - }, { "id": "modelOption", "propertyProfile": "container" @@ -6537,6 +6720,223 @@ } ] }, + { + "id": "icon-catalog", + "parts": [ + { + "id": "root", + "propertyProfile": "container" + }, + { + "id": "item", + "propertyProfile": "container" + } + ], + "facets": [], + "states": [] + }, + { + "id": "assistant-avatar", + "parts": [ + { + "id": "root", + "propertyProfile": "container" + } + ], + "facets": [], + "states": [ + { + "id": "active", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"active\"]" + } + }, + { + "id": "running", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"running\"]" + } + }, + { + "id": "attention", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"attention\"]" + } + }, + { + "id": "unread", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"unread\"]" + } + }, + { + "id": "error", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"error\"]" + } + } + ] + }, + { + "id": "device-overview", + "parts": [ + { + "id": "root", + "propertyProfile": "overlay", + "visualRole": "popup" + } + ], + "facets": [ + { + "id": "placement", + "attribute": "data-bf-placement", + "values": [ + "top", + "bottom" + ] + } + ], + "states": [ + { + "id": "local", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"local\"]" + } + }, + { + "id": "connected", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"connected\"]" + } + } + ] + }, + { + "id": "session-navigation", + "parts": [ + { + "id": "viewToggle", + "propertyProfile": "control", + "visualRole": "control" + } + ], + "facets": [ + { + "id": "action", + "attribute": "data-bf-action", + "values": [ + "toggle-session-view" + ] + } + ], + "states": [ + { + "id": "grouped", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"grouped\"]" + } + }, + { + "id": "all", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"all\"]" + } + } + ] + }, + { + "id": "harness-profile-step", + "parts": [ + { + "id": "root", + "propertyProfile": "container", + "visualRole": "content" + } + ], + "facets": [ + { + "id": "profile", + "attribute": "data-bf-profile", + "values": [ + "minimal", + "balanced", + "ultimate", + "creative" + ] + } + ], + "states": [] + }, + { + "id": "subagent-avatar", + "parts": [ + { + "id": "root", + "propertyProfile": "container" + } + ], + "facets": [], + "states": [ + { + "id": "running", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"running\"]" + } + }, + { + "id": "finishing", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"finishing\"]" + } + }, + { + "id": "waiting", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"waiting\"]" + } + }, + { + "id": "completed", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"completed\"]" + } + }, + { + "id": "cancelled", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"cancelled\"]" + } + }, + { + "id": "error", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"error\"]" + } + }, + { + "id": "idle", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"idle\"]" + } + } + ] + }, { "id": "announcement", "parts": [ @@ -6805,6 +7205,10 @@ "id": "centerHeader", "propertyProfile": "container" }, + { + "id": "centerClose", + "propertyProfile": "container" + }, { "id": "centerList", "propertyProfile": "container" @@ -7128,52 +7532,6 @@ } ] }, - { - "id": "lsp", - "parts": [ - { - "id": "pluginList", - "propertyProfile": "container" - }, - { - "id": "empty", - "propertyProfile": "container" - }, - { - "id": "items", - "propertyProfile": "container" - }, - { - "id": "plugin", - "propertyProfile": "container" - }, - { - "id": "pluginHeader", - "propertyProfile": "container" - }, - { - "id": "pluginDetails", - "propertyProfile": "container" - } - ], - "facets": [], - "states": [ - { - "id": "empty", - "selector": { - "kind": "self", - "suffix": "[data-bf-state~=\"empty\"]" - } - }, - { - "id": "expanded", - "selector": { - "kind": "self", - "suffix": "[data-bf-state~=\"expanded\"]" - } - } - ] - }, { "id": "terminal-tool", "parts": [ @@ -7591,21 +7949,80 @@ "suffix": "[data-bf-state~=\"disabled\"]" } } - ] + ] + }, + { + "id": "mini-app-card", + "parts": [ + { + "id": "root", + "propertyProfile": "container" + }, + { + "id": "header", + "propertyProfile": "container" + }, + { + "id": "iconArea", + "propertyProfile": "container" + }, + { + "id": "icon", + "propertyProfile": "container" + }, + { + "id": "title", + "propertyProfile": "container" + }, + { + "id": "name", + "propertyProfile": "container" + }, + { + "id": "version", + "propertyProfile": "container" + }, + { + "id": "status", + "propertyProfile": "container" + }, + { + "id": "body", + "propertyProfile": "container" + }, + { + "id": "description", + "propertyProfile": "container" + }, + { + "id": "tags", + "propertyProfile": "container" + }, + { + "id": "footer", + "propertyProfile": "container" + }, + { + "id": "actions", + "propertyProfile": "container" + } + ], + "facets": [], + "states": [] }, { - "id": "mini-app-card", + "id": "mini-app-detail-modal", "parts": [ { "id": "root", "propertyProfile": "container" }, { - "id": "header", + "id": "hero", "propertyProfile": "container" }, { - "id": "iconArea", + "id": "iconStage", "propertyProfile": "container" }, { @@ -7613,35 +8030,27 @@ "propertyProfile": "container" }, { - "id": "title", - "propertyProfile": "container" - }, - { - "id": "name", - "propertyProfile": "container" - }, - { - "id": "version", + "id": "summary", "propertyProfile": "container" }, { - "id": "status", + "id": "tags", "propertyProfile": "container" }, { - "id": "body", + "id": "highlights", "propertyProfile": "container" }, { - "id": "description", + "id": "highlight", "propertyProfile": "container" }, { - "id": "tags", + "id": "footer", "propertyProfile": "container" }, { - "id": "footer", + "id": "status", "propertyProfile": "container" }, { @@ -7650,7 +8059,24 @@ } ], "facets": [], - "states": [] + "states": [ + { + "id": "running", + "selector": { + "kind": "ancestorPart", + "part": "root", + "suffix": "[data-bf-state~=\"running\"]" + } + }, + { + "id": "customizing", + "selector": { + "kind": "ancestorPart", + "part": "root", + "suffix": "[data-bf-state~=\"customizing\"]" + } + } + ] }, { "id": "agent-card", @@ -7739,19 +8165,19 @@ "propertyProfile": "container" }, { - "id": "body", + "id": "status", "propertyProfile": "container" }, { - "id": "description", + "id": "body", "propertyProfile": "container" }, { - "id": "footer", + "id": "description", "propertyProfile": "container" }, { - "id": "tags", + "id": "footer", "propertyProfile": "container" }, { @@ -7760,7 +8186,22 @@ } ], "facets": [], - "states": [] + "states": [ + { + "id": "connected", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"connected\"]" + } + }, + { + "id": "disabled", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"disabled\"]" + } + } + ] }, { "id": "agent-capability-tooltip", @@ -7893,6 +8334,14 @@ "id": "summary", "propertyProfile": "container" }, + { + "id": "title", + "propertyProfile": "container" + }, + { + "id": "heroActions", + "propertyProfile": "container" + }, { "id": "badges", "propertyProfile": "container" @@ -7915,7 +8364,22 @@ } ], "facets": [], - "states": [] + "states": [ + { + "id": "heroTitle", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"heroTitle\"]" + } + }, + { + "id": "stableHeight", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"stableHeight\"]" + } + } + ] }, { "id": "mcp-interaction-dialog", @@ -8029,6 +8493,10 @@ "id": "root", "propertyProfile": "container" }, + { + "id": "menuItem", + "propertyProfile": "container" + }, { "id": "progress", "propertyProfile": "container" @@ -8301,6 +8769,10 @@ "id": "browserHeader", "propertyProfile": "container" }, + { + "id": "browserClose", + "propertyProfile": "container" + }, { "id": "breadcrumb", "propertyProfile": "container" @@ -8348,6 +8820,26 @@ { "id": "confirmActions", "propertyProfile": "container" + }, + { + "id": "portForward", + "propertyProfile": "container" + }, + { + "id": "portForwardIntro", + "propertyProfile": "container" + }, + { + "id": "portForwardForm", + "propertyProfile": "container" + }, + { + "id": "portForwardError", + "propertyProfile": "container" + }, + { + "id": "portForwardTable", + "propertyProfile": "container" } ], "facets": [], @@ -8359,7 +8851,7 @@ { "id": "root", "propertyProfile": "container", - "visualRole": "dialog", + "visualRole": "content", "continuityGroup": "about-dialog" }, { @@ -8373,16 +8865,6 @@ "propertyProfile": "paint", "visualRole": "content" }, - { - "id": "version", - "propertyProfile": "paint", - "visualRole": "content" - }, - { - "id": "decoration", - "propertyProfile": "paint", - "visualRole": "decoration" - }, { "id": "content", "propertyProfile": "container", @@ -8390,14 +8872,14 @@ "continuityGroup": "about-dialog" }, { - "id": "updateCard", - "propertyProfile": "container", - "visualRole": "card" + "id": "channelBadge", + "propertyProfile": "paint", + "visualRole": "content" }, { - "id": "updateHeader", + "id": "updateCard", "propertyProfile": "container", - "visualRole": "toolbar" + "visualRole": "content" }, { "id": "updateFeedback", @@ -8419,11 +8901,6 @@ "propertyProfile": "paint", "visualRole": "decoration" }, - { - "id": "infoCard", - "propertyProfile": "container", - "visualRole": "card" - }, { "id": "infoRow", "propertyProfile": "container", @@ -8444,6 +8921,12 @@ "propertyProfile": "control", "visualRole": "control" }, + { + "id": "starCallout", + "propertyProfile": "container", + "visualRole": "toolbar", + "continuityGroup": "about-dialog" + }, { "id": "footer", "propertyProfile": "container", @@ -8544,6 +9027,12 @@ "visualRole": "toolbar", "continuityGroup": "nav-panel" }, + { + "id": "utilityRow", + "propertyProfile": "container", + "visualRole": "toolbar", + "continuityGroup": "nav-panel" + }, { "id": "search", "propertyProfile": "control", @@ -8554,6 +9043,11 @@ "propertyProfile": "control", "visualRole": "control" }, + { + "id": "todoEntry", + "propertyProfile": "control", + "visualRole": "control" + }, { "id": "topActions", "propertyProfile": "container", @@ -8564,6 +9058,11 @@ "propertyProfile": "control", "visualRole": "control" }, + { + "id": "topActionUnseen", + "propertyProfile": "paint", + "visualRole": "decoration" + }, { "id": "extensionGroup", "propertyProfile": "container", @@ -8580,6 +9079,12 @@ "propertyProfile": "container", "visualRole": "content" }, + { + "id": "stickySectionHeader", + "propertyProfile": "container", + "visualRole": "toolbar", + "continuityGroup": "nav-panel" + }, { "id": "sectionContent", "propertyProfile": "container", @@ -8595,12 +9100,6 @@ "propertyProfile": "overlay", "visualRole": "popup" }, - { - "id": "bottomBar", - "propertyProfile": "container", - "visualRole": "toolbar", - "continuityGroup": "nav-panel" - }, { "id": "miniAppFooter", "propertyProfile": "container", @@ -8613,6 +9112,16 @@ "visualRole": "toolbar", "continuityGroup": "nav-panel" }, + { + "id": "deviceStatus", + "propertyProfile": "control", + "visualRole": "control" + }, + { + "id": "settingsEntry", + "propertyProfile": "control", + "visualRole": "control" + }, { "id": "footerButton", "propertyProfile": "control", @@ -8672,21 +9181,29 @@ "id": "action", "attribute": "data-bf-action", "values": [ - "code", - "cowork", - "assistant", + "new-session", + "smart-members", + "long-term-tracking", "todos", "extensions", "agents", - "skills" + "skills", + "ecosystem-compatibility", + "session-filter", + "assistant-manager", + "floating-window", + "appearance-configuration", + "open-settings", + "about" ] }, { "id": "section", "attribute": "data-bf-section", "values": [ - "assistant-sessions", - "workspace" + "smart-members", + "workspace", + "sessions" ] } ], @@ -8732,6 +9249,27 @@ "kind": "self", "suffix": "[data-bf-state~=\"empty\"]" } + }, + { + "id": "stuck", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"stuck\"]" + } + }, + { + "id": "local", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"local\"]" + } + }, + { + "id": "connected", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"connected\"]" + } } ] }, @@ -8750,6 +9288,14 @@ "id": "retry", "propertyProfile": "container" }, + { + "id": "aggregateLoading", + "propertyProfile": "container" + }, + { + "id": "aggregateRetry", + "propertyProfile": "container" + }, { "id": "row", "propertyProfile": "container" @@ -8758,6 +9304,10 @@ "id": "rowMain", "propertyProfile": "container" }, + { + "id": "assistantAvatar", + "propertyProfile": "container" + }, { "id": "edit", "propertyProfile": "container" @@ -8804,6 +9354,13 @@ "kind": "self", "suffix": "[data-bf-state~=\"loading\"]" } + }, + { + "id": "partial", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"partial\"]" + } } ] }, @@ -9779,6 +10336,10 @@ "id": "headerMain", "propertyProfile": "container" }, + { + "id": "subagentName", + "propertyProfile": "container" + }, { "id": "badge", "propertyProfile": "container" @@ -9939,7 +10500,7 @@ ] }, { - "id": "ai-model-config", + "id": "model-settings", "parts": [ { "id": "root", @@ -10370,14 +10931,6 @@ { "id": "applicationToggle", "propertyProfile": "container" - }, - { - "id": "hooksSection", - "propertyProfile": "container" - }, - { - "id": "hooksSummary", - "propertyProfile": "container" } ], "facets": [], @@ -10476,7 +11029,7 @@ "states": [] }, { - "id": "session-config", + "id": "runtime-settings", "parts": [ { "id": "root", @@ -10503,63 +11056,27 @@ "propertyProfile": "container" }, { - "id": "petList", - "propertyProfile": "container" - }, - { - "id": "petGroup", - "propertyProfile": "container" - }, - { - "id": "petOption", - "propertyProfile": "container" - }, - { - "id": "petOptionMain", - "propertyProfile": "container" - }, - { - "id": "petActions", - "propertyProfile": "container" - }, - { - "id": "platformNote", - "propertyProfile": "container" - }, - { - "id": "debugInputs", - "propertyProfile": "container" - }, - { - "id": "debugActions", - "propertyProfile": "container" - }, - { - "id": "templateModal", - "propertyProfile": "container" - }, - { - "id": "templateCard", + "id": "petList", "propertyProfile": "container" }, { - "id": "templateHeader", + "id": "petGroup", "propertyProfile": "container" }, { - "id": "templateInfo", + "id": "petOption", "propertyProfile": "container" }, { - "id": "templateContent", + "id": "petOptionMain", "propertyProfile": "container" }, { - "id": "templateField", + "id": "petActions", "propertyProfile": "container" }, { - "id": "templateNotes", + "id": "platformNote", "propertyProfile": "container" }, { @@ -10576,8 +11093,11 @@ "id": "view", "attribute": "data-bf-view", "values": [ - "personalization", - "permissions" + "pet", + "session-workspace", + "execution", + "execution-control", + "device-control" ] } ], @@ -10843,9 +11363,7 @@ "attribute": "data-bf-phase", "values": [ "idle", - "preparing-microphone", - "checking-microphone", - "preparing-recognition", + "preparing", "recording", "transcribing" ] @@ -10861,14 +11379,6 @@ } ], "states": [ - { - "id": "testingMicrophone", - "selector": { - "kind": "ancestorPart", - "part": "root", - "suffix": "[data-bf-state~=\"testing-microphone\"]" - } - }, { "id": "testingRecognition", "selector": { @@ -10922,10 +11432,6 @@ "id": "badges", "propertyProfile": "container" }, - { - "id": "body", - "propertyProfile": "container" - }, { "id": "vibe", "propertyProfile": "container" @@ -11351,6 +11857,41 @@ } ] }, + { + "id": "usage-statistics-config", + "parts": [ + { + "id": "root", + "propertyProfile": "container" + }, + { + "id": "filters", + "propertyProfile": "container" + }, + { + "id": "summary", + "propertyProfile": "container" + }, + { + "id": "distributions", + "propertyProfile": "container" + }, + { + "id": "modelHitRate", + "propertyProfile": "container" + }, + { + "id": "trendPanel", + "propertyProfile": "container" + }, + { + "id": "empty", + "propertyProfile": "container" + } + ], + "facets": [], + "states": [] + }, { "id": "settings-nav", "parts": [ @@ -11412,11 +11953,6 @@ "propertyProfile": "control", "visualRole": "control" }, - { - "id": "itemUnseen", - "propertyProfile": "paint", - "visualRole": "decoration" - }, { "id": "highlight", "propertyProfile": "paint", @@ -11441,6 +11977,112 @@ } ] }, + { + "id": "settings-view-page", + "parts": [ + { + "id": "root", + "propertyProfile": "layout", + "visualRole": "content" + }, + { + "id": "loading", + "propertyProfile": "overlay", + "visualRole": "content" + }, + { + "id": "loadingLine", + "propertyProfile": "paint", + "visualRole": "decoration" + }, + { + "id": "loadingBlock", + "propertyProfile": "paint", + "visualRole": "decoration" + } + ], + "facets": [ + { + "id": "view", + "attribute": "data-bf-view", + "values": [ + "voice", + "shortcuts", + "editor", + "terminal", + "quick-actions", + "hooks" + ] + } + ], + "states": [] + }, + { + "id": "automation-settings-page", + "parts": [ + { + "id": "root", + "propertyProfile": "layout", + "visualRole": "content" + }, + { + "id": "quickActions", + "propertyProfile": "container", + "visualRole": "content" + }, + { + "id": "hooks", + "propertyProfile": "container", + "visualRole": "content" + } + ], + "facets": [], + "states": [] + }, + { + "id": "development-settings-page", + "parts": [ + { + "id": "root", + "propertyProfile": "layout", + "visualRole": "content" + }, + { + "id": "terminal", + "propertyProfile": "container", + "visualRole": "content" + }, + { + "id": "editor", + "propertyProfile": "container", + "visualRole": "content" + } + ], + "facets": [], + "states": [] + }, + { + "id": "input-settings-page", + "parts": [ + { + "id": "root", + "propertyProfile": "layout", + "visualRole": "content" + }, + { + "id": "voice", + "propertyProfile": "container", + "visualRole": "content" + }, + { + "id": "shortcuts", + "propertyProfile": "container", + "visualRole": "content" + } + ], + "facets": [], + "states": [] + }, { "id": "background-command-output-panel", "parts": [ @@ -12205,201 +12847,102 @@ "propertyProfile": "container" } ], - "facets": [ - { - "id": "kind", - "attribute": "data-bf-popover", - "values": [ - "line", - "indent", - "encoding", - "language" - ] - } - ], - "states": [ - { - "id": "selected", - "selector": { - "kind": "self", - "suffix": "[data-bf-state~=\"selected\"]" - } - } - ] - }, - { - "id": "m-editor", - "parts": [ - { - "id": "root", - "propertyProfile": "container" - }, - { - "id": "toolbar", - "propertyProfile": "container" - }, - { - "id": "content", - "propertyProfile": "container" - }, - { - "id": "notice", - "propertyProfile": "container" - }, - { - "id": "editPanel", - "propertyProfile": "container" - }, - { - "id": "previewPanel", - "propertyProfile": "container" - }, - { - "id": "irPanel", - "propertyProfile": "container" - } - ], - "facets": [], - "states": [] - }, - { - "id": "references-panel", - "parts": [ - { - "id": "root", - "propertyProfile": "container" - }, - { - "id": "header", - "propertyProfile": "container" - }, - { - "id": "title", - "propertyProfile": "container" - }, - { - "id": "close", - "propertyProfile": "container" - }, - { - "id": "empty", - "propertyProfile": "container" - }, - { - "id": "content", - "propertyProfile": "container" - }, - { - "id": "fileGroup", - "propertyProfile": "container" - }, - { - "id": "fileHeader", - "propertyProfile": "container" - }, - { - "id": "filePath", - "propertyProfile": "container" - }, - { - "id": "fileCount", - "propertyProfile": "container" - }, - { - "id": "list", - "propertyProfile": "container" - }, - { - "id": "item", - "propertyProfile": "container" - }, - { - "id": "line", - "propertyProfile": "container" - }, + "facets": [ { - "id": "preview", - "propertyProfile": "container" + "id": "kind", + "attribute": "data-bf-popover", + "values": [ + "line", + "indent", + "encoding", + "language" + ] } ], - "facets": [], "states": [ { - "id": "empty", + "id": "selected", "selector": { "kind": "self", - "suffix": "[data-bf-state~=\"empty\"]" + "suffix": "[data-bf-state~=\"selected\"]" } } ] }, { - "id": "nav-search-dialog", + "id": "m-editor", "parts": [ { "id": "root", "propertyProfile": "container" }, { - "id": "card", + "id": "toolbar", "propertyProfile": "container" }, { - "id": "inputRow", + "id": "content", "propertyProfile": "container" }, { - "id": "search", + "id": "notice", "propertyProfile": "container" }, { - "id": "results", + "id": "editPanel", "propertyProfile": "container" }, { - "id": "empty", + "id": "previewPanel", "propertyProfile": "container" }, { - "id": "group", + "id": "irPanel", "propertyProfile": "container" - }, + } + ], + "facets": [], + "states": [] + }, + { + "id": "global-search", + "parts": [ { - "id": "groupLabel", + "id": "root", "propertyProfile": "container" }, { - "id": "item", + "id": "query", "propertyProfile": "container" }, { - "id": "itemIcon", + "id": "scopeBar", "propertyProfile": "container" }, { - "id": "itemContent", + "id": "results", "propertyProfile": "container" }, { - "id": "itemLabel", + "id": "group", "propertyProfile": "container" }, { - "id": "itemSublabel", + "id": "result", "propertyProfile": "container" }, { - "id": "sessionHint", + "id": "footer", "propertyProfile": "container" } ], "facets": [], "states": [ { - "id": "active", + "id": "selected", "selector": { "kind": "self", - "suffix": "[data-bf-state~=\"active\"]" + "suffix": "[data-bf-state~=\"selected\"]" } } ] @@ -12812,10 +13355,6 @@ { "id": "categoryFilter", "propertyProfile": "container" - }, - { - "id": "detailTags", - "propertyProfile": "container" } ], "facets": [], @@ -13161,19 +13700,31 @@ "propertyProfile": "container" }, { - "id": "main", + "id": "context", + "propertyProfile": "container" + }, + { + "id": "next", "propertyProfile": "container" }, { "id": "workspace", "propertyProfile": "container" }, + { + "id": "workspaceMenu", + "propertyProfile": "container" + }, + { + "id": "workspaceOption", + "propertyProfile": "container" + }, { "id": "branch", "propertyProfile": "container" }, { - "id": "actions", + "id": "divider", "propertyProfile": "container" }, { @@ -13225,6 +13776,13 @@ "suffix": "[data-bf-state~=\"selected\"]" } }, + { + "id": "active", + "selector": { + "kind": "self", + "suffix": "[data-bf-state~=\"active\"]" + } + }, { "id": "armed", "selector": { @@ -13555,11 +14113,6 @@ "propertyProfile": "container", "visualRole": "content" }, - { - "id": "mascot", - "propertyProfile": "paint", - "visualRole": "decoration" - }, { "id": "heading", "propertyProfile": "paint", @@ -14085,10 +14638,6 @@ { "id": "separator", "propertyProfile": "container" - }, - { - "id": "lsp", - "propertyProfile": "container" } ], "facets": [], @@ -14737,6 +15286,10 @@ "id": "avatar", "propertyProfile": "container" }, + { + "id": "artwork", + "propertyProfile": "container" + }, { "id": "defaultsContent", "propertyProfile": "container" @@ -14744,6 +15297,10 @@ { "id": "stats", "propertyProfile": "container" + }, + { + "id": "companion", + "propertyProfile": "container" } ], "facets": [], @@ -14847,42 +15404,10 @@ "id": "root", "propertyProfile": "container" }, - { - "id": "collapsedTrigger", - "propertyProfile": "container" - }, - { - "id": "collapsedBadge", - "propertyProfile": "container" - }, - { - "id": "panel", - "propertyProfile": "container" - }, - { - "id": "heading", - "propertyProfile": "container" - }, - { - "id": "headingActions", - "propertyProfile": "container" - }, - { - "id": "requests", - "propertyProfile": "container" - }, { "id": "request", "propertyProfile": "container" }, - { - "id": "requestHeading", - "propertyProfile": "container" - }, - { - "id": "requestDetails", - "propertyProfile": "container" - }, { "id": "risk", "propertyProfile": "container" @@ -14891,32 +15416,17 @@ "id": "error", "propertyProfile": "container" }, - { - "id": "feedback", - "propertyProfile": "container" - }, { "id": "actions", "propertyProfile": "container" }, { - "id": "singleActions", - "propertyProfile": "container" - }, - { - "id": "batchActions", + "id": "scope", "propertyProfile": "container" } ], "facets": [], "states": [ - { - "id": "collapsed", - "selector": { - "kind": "self", - "suffix": "[data-bf-state~=\"collapsed\"]" - } - }, { "id": "responding", "selector": { @@ -15839,37 +16349,10 @@ { "id": "root", "propertyProfile": "container" - }, - { - "id": "content", - "propertyProfile": "container" - }, - { - "id": "expanded", - "propertyProfile": "container" - }, - { - "id": "section", - "propertyProfile": "container" - }, - { - "id": "label", - "propertyProfile": "container" - }, - { - "id": "description", - "propertyProfile": "container" } ], "facets": [], "states": [ - { - "id": "expanded", - "selector": { - "kind": "self", - "suffix": "[data-bf-state~=\"expanded\"]" - } - }, { "id": "failed", "selector": { @@ -16613,7 +17096,8 @@ "attribute": "data-bf-session-kind", "values": [ "code", - "cowork" + "cowork", + "unified" ] } ], @@ -16711,6 +17195,18 @@ "id": "root", "propertyProfile": "container" }, + { + "id": "statusCard", + "propertyProfile": "container" + }, + { + "id": "statusActions", + "propertyProfile": "container" + }, + { + "id": "modelDialog", + "propertyProfile": "container" + }, { "id": "modelList", "propertyProfile": "container" @@ -16722,13 +17218,17 @@ { "id": "modelActions", "propertyProfile": "container" + }, + { + "id": "modelMore", + "propertyProfile": "container" } ], "facets": [], "states": [] }, { - "id": "worktrees-config", + "id": "worktree-settings", "parts": [ { "id": "root", @@ -17168,19 +17668,19 @@ "propertyProfile": "container" }, { - "id": "anchorBar", + "id": "zones", "propertyProfile": "container" }, { - "id": "zones", + "id": "harnessPresentation", "propertyProfile": "container" }, { - "id": "coreGrid", + "id": "filters", "propertyProfile": "container" }, { - "id": "filters", + "id": "catalogGrid", "propertyProfile": "container" }, { @@ -17514,28 +18014,67 @@ ], "facets": [ { - "id": "tab", - "attribute": "data-bf-tab", + "id": "page", + "attribute": "data-bf-page", "values": [ - "basics", - "appearance", - "models", - "archived-sessions", - "session-personalization", - "session-permissions", - "quick-actions", - "review", - "memories", - "mcp-tools", - "external-sources", - "acp-agents", - "editor", - "keyboard" + "application.general", + "application.appearance", + "application.pet", + "application.input", + "application.development", + "ai.models", + "ai.memory", + "workspace.session", + "workspace.worktrees", + "tools.execution", + "tools.automation", + "tools.mcp", + "tools.acp", + "data.usage", + "data.archived", + "data.diagnostics" ] } ], "states": [] }, + { + "id": "ecosystem-compatibility", + "parts": [ + { + "id": "root", + "propertyProfile": "layout", + "visualRole": "workspace" + }, + { + "id": "sidebar", + "propertyProfile": "layout", + "visualRole": "panel" + }, + { + "id": "productList", + "propertyProfile": "container", + "visualRole": "content" + }, + { + "id": "main", + "propertyProfile": "layout", + "visualRole": "continuous-surface" + }, + { + "id": "header", + "propertyProfile": "container", + "visualRole": "toolbar" + }, + { + "id": "content", + "propertyProfile": "container", + "visualRole": "content" + } + ], + "facets": [], + "states": [] + }, { "id": "skills", "parts": [ @@ -17623,6 +18162,18 @@ "id": "addAction", "propertyProfile": "container" }, + { + "id": "installedListHeader", + "propertyProfile": "container" + }, + { + "id": "installedListTitle", + "propertyProfile": "container" + }, + { + "id": "installedListCount", + "propertyProfile": "container" + }, { "id": "loading", "propertyProfile": "container" @@ -17671,10 +18222,26 @@ "id": "installedCardMeta", "propertyProfile": "container" }, + { + "id": "installedCardSource", + "propertyProfile": "container" + }, + { + "id": "installedCardLevel", + "propertyProfile": "container" + }, + { + "id": "installedCardStatus", + "propertyProfile": "container" + }, { "id": "installedCardActions", "propertyProfile": "container" }, + { + "id": "installedCardDetails", + "propertyProfile": "container" + }, { "id": "installedCardDelete", "propertyProfile": "container" @@ -17970,6 +18537,10 @@ "id": "headerActions", "propertyProfile": "container" }, + { + "id": "monthNavigation", + "propertyProfile": "container" + }, { "id": "panes", "propertyProfile": "container" @@ -17978,6 +18549,10 @@ "id": "listPane", "propertyProfile": "container" }, + { + "id": "overview", + "propertyProfile": "container" + }, { "id": "rows", "propertyProfile": "container" @@ -17987,7 +18562,7 @@ "propertyProfile": "container" }, { - "id": "rowTime", + "id": "rowIcon", "propertyProfile": "container" }, { @@ -18026,10 +18601,6 @@ "id": "calendarHead", "propertyProfile": "container" }, - { - "id": "calendarNav", - "propertyProfile": "container" - }, { "id": "calendarGrid", "propertyProfile": "container" @@ -18047,13 +18618,29 @@ "propertyProfile": "container" }, { - "id": "editorHead", + "id": "editorIntro", + "propertyProfile": "container" + }, + { + "id": "editorBody", + "propertyProfile": "container" + }, + { + "id": "editorSection", "propertyProfile": "container" }, { "id": "editorGrid", "propertyProfile": "container" }, + { + "id": "editorRuntime", + "propertyProfile": "container" + }, + { + "id": "editorAdvanced", + "propertyProfile": "container" + }, { "id": "editorActions", "propertyProfile": "container" @@ -18211,6 +18798,40 @@ "--bf-appearance-token-btn-primary-hover-transform", "--bf-appearance-token-btn-primary-shadow", "--bf-appearance-token-button-bg-hover", + "--bf-appearance-token-chrome-accent-100", + "--bf-appearance-token-chrome-accent-200", + "--bf-appearance-token-chrome-accent-300", + "--bf-appearance-token-chrome-accent-400", + "--bf-appearance-token-chrome-accent-50", + "--bf-appearance-token-chrome-accent-500", + "--bf-appearance-token-chrome-accent-600", + "--bf-appearance-token-chrome-accent-700", + "--bf-appearance-token-chrome-bg-elevated", + "--bf-appearance-token-chrome-bg-primary", + "--bf-appearance-token-chrome-bg-scene", + "--bf-appearance-token-chrome-bg-secondary", + "--bf-appearance-token-chrome-bg-tertiary", + "--bf-appearance-token-chrome-bg-workbench", + "--bf-appearance-token-chrome-border-accent", + "--bf-appearance-token-chrome-border-accent-soft", + "--bf-appearance-token-chrome-border-accent-strong", + "--bf-appearance-token-chrome-border-accent-subtle", + "--bf-appearance-token-chrome-border-base", + "--bf-appearance-token-chrome-border-medium", + "--bf-appearance-token-chrome-border-prominent", + "--bf-appearance-token-chrome-border-strong", + "--bf-appearance-token-chrome-border-subtle", + "--bf-appearance-token-chrome-element-bg-base", + "--bf-appearance-token-chrome-element-bg-medium", + "--bf-appearance-token-chrome-element-bg-soft", + "--bf-appearance-token-chrome-element-bg-strong", + "--bf-appearance-token-chrome-element-bg-subtle", + "--bf-appearance-token-chrome-scrollbar-thumb", + "--bf-appearance-token-chrome-scrollbar-thumb-hover", + "--bf-appearance-token-chrome-text-disabled", + "--bf-appearance-token-chrome-text-muted", + "--bf-appearance-token-chrome-text-primary", + "--bf-appearance-token-chrome-text-secondary", "--bf-appearance-token-color-accent-100", "--bf-appearance-token-color-accent-200", "--bf-appearance-token-color-accent-300", @@ -18275,6 +18896,12 @@ "--bf-appearance-token-color-warning", "--bf-appearance-token-color-warning-bg", "--bf-appearance-token-color-warning-border", + "--bf-appearance-token-config-page-divider", + "--bf-appearance-token-config-page-row-hover-bg", + "--bf-appearance-token-config-page-section-bg", + "--bf-appearance-token-config-page-section-border", + "--bf-appearance-token-config-page-section-border-width", + "--bf-appearance-token-config-page-section-shadow", "--bf-appearance-token-domain-capability-creative", "--bf-appearance-token-domain-capability-docs", "--bf-appearance-token-domain-capability-ops", diff --git a/src/crates/assembly/core/src/service/announcement/content/tips/en-US/020_lsp_diagnostics.md b/src/crates/assembly/core/src/service/announcement/content/tips/en-US/020_lsp_diagnostics.md deleted file mode 100644 index e457a1e555..0000000000 --- a/src/crates/assembly/core/src/service/announcement/content/tips/en-US/020_lsp_diagnostics.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: lsp_diagnostics -nth_open: 35 -auto_dismiss_secs: 10 ---- - -# Language service diagnostics - -The editor has built-in LSP support with real-time syntax errors and type hints diff --git a/src/crates/assembly/core/src/service/announcement/content/tips/zh-CN/020_lsp_diagnostics.md b/src/crates/assembly/core/src/service/announcement/content/tips/zh-CN/020_lsp_diagnostics.md deleted file mode 100644 index c3235a3f2a..0000000000 --- a/src/crates/assembly/core/src/service/announcement/content/tips/zh-CN/020_lsp_diagnostics.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: lsp_diagnostics -nth_open: 35 -auto_dismiss_secs: 10 ---- - -# 语言服务诊断 - -代码编辑器内置 LSP 支持,实时显示语法错误和类型提示 diff --git a/src/crates/assembly/core/src/service/announcement/content/tips/zh-TW/020_lsp_diagnostics.md b/src/crates/assembly/core/src/service/announcement/content/tips/zh-TW/020_lsp_diagnostics.md deleted file mode 100644 index f3ef366656..0000000000 --- a/src/crates/assembly/core/src/service/announcement/content/tips/zh-TW/020_lsp_diagnostics.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: lsp_diagnostics -nth_open: 35 -auto_dismiss_secs: 10 ---- - -# 語言服務診斷 - -代碼編輯器內置 LSP 支持,實時顯示語法錯誤和類型提示 diff --git a/src/crates/assembly/core/src/service/lsp/config_watcher.rs b/src/crates/assembly/core/src/service/lsp/config_watcher.rs deleted file mode 100644 index 3768d6137f..0000000000 --- a/src/crates/assembly/core/src/service/lsp/config_watcher.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Compatibility re-exports for LSP configuration watching. -//! -//! The reusable watcher lives in `bitfun-services-core`. - -pub use bitfun_services_core::lsp::config_watcher::ConfigWatcher; diff --git a/src/crates/assembly/core/src/service/lsp/debouncer.rs b/src/crates/assembly/core/src/service/lsp/debouncer.rs deleted file mode 100644 index 20f257f1de..0000000000 --- a/src/crates/assembly/core/src/service/lsp/debouncer.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Compatibility re-exports for LSP request debouncing. -//! -//! The reusable debouncer lives in `bitfun-services-core`. - -pub use bitfun_services_core::lsp::debouncer::RequestDebouncer; diff --git a/src/crates/assembly/core/src/service/lsp/file_sync.rs b/src/crates/assembly/core/src/service/lsp/file_sync.rs deleted file mode 100644 index fc37a0458c..0000000000 --- a/src/crates/assembly/core/src/service/lsp/file_sync.rs +++ /dev/null @@ -1,448 +0,0 @@ -//! LSP file sync service -//! -//! Responsibilities: -//! - Watch filesystem changes in the workspace -//! - Sync file edits to the LSP server (didOpen/didChange) -//! - Batch + debounce to reduce LSP calls -//! - Support graceful degradation - -use anyhow::{anyhow, Result}; -use log::{debug, info, warn}; -use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; -use tokio::sync::RwLock; -use tokio::task::JoinHandle; - -use super::workspace_manager::WorkspaceLspManager; - -/// File sync configuration. -#[derive(Debug, Clone)] -pub struct FileSyncConfig { - /// Whether file sync is enabled (global downgrade switch). - pub enabled: bool, - - /// Debounce delay (milliseconds). - pub debounce_ms: u64, - - /// File extensions to watch (allowlist). - pub watch_extensions: Vec, - - /// Path patterns to ignore (denylist). - pub ignore_patterns: Vec, - - /// Max file size (bytes). Files larger than this will not be synced. - pub max_file_size: u64, -} - -impl Default for FileSyncConfig { - fn default() -> Self { - Self { - enabled: true, - debounce_ms: 300, - watch_extensions: vec![ - ".rs".to_string(), - ".ts".to_string(), - ".tsx".to_string(), - ".js".to_string(), - ".jsx".to_string(), - ".py".to_string(), - ".go".to_string(), - ".java".to_string(), - ".cpp".to_string(), - ".c".to_string(), - ".h".to_string(), - ".hpp".to_string(), - ".cs".to_string(), - ".rb".to_string(), - ".php".to_string(), - ".swift".to_string(), - ".kt".to_string(), - ".scala".to_string(), - ], - ignore_patterns: vec![ - "node_modules".to_string(), - "target".to_string(), - ".git".to_string(), - "dist".to_string(), - "build".to_string(), - ".next".to_string(), - ".vscode".to_string(), - ".idea".to_string(), - ], - max_file_size: 10 * 1024 * 1024, - } - } -} - -/// File change record. -#[derive(Debug, Clone)] -struct FileChange { - #[allow(dead_code)] - path: PathBuf, - #[allow(dead_code)] - last_modified: SystemTime, - change_count: u32, -} - -/// LSP file sync manager. -pub struct LspFileSync { - /// Configuration. - config: FileSyncConfig, - - /// File watcher. - watcher: Arc>>, - - /// Workspace LSP manager mapping (`workspace_path -> manager`). - workspace_managers: Arc>>>, - - /// Pending file change queue (debounced). - pending_changes: Arc>>, - - /// Debounce worker task handle. - debounce_worker: Arc>>>, -} - -impl LspFileSync { - /// Creates a new file sync manager. - pub fn new(config: FileSyncConfig) -> Arc { - Arc::new(Self { - config, - watcher: Arc::new(RwLock::new(None)), - workspace_managers: Arc::new(RwLock::new(HashMap::new())), - pending_changes: Arc::new(RwLock::new(HashMap::new())), - debounce_worker: Arc::new(RwLock::new(None)), - }) - } - - /// Starts watching a workspace. - pub async fn watch_workspace( - self: &Arc, - workspace: PathBuf, - manager: Arc, - ) -> Result<()> { - if !self.config.enabled { - info!("LSP file sync is disabled, skipping workspace watch"); - return Ok(()); - } - - info!("Starting file sync for workspace: {:?}", workspace); - - { - let mut managers = self.workspace_managers.write().await; - managers.insert(workspace.clone(), manager); - } - - { - let mut watcher = self.watcher.write().await; - if watcher.is_none() { - *watcher = Some(self.start_watcher().await?); - } - } - - { - let mut watcher = self.watcher.write().await; - if let Some(w) = watcher.as_mut() { - w.watch(&workspace, RecursiveMode::Recursive) - .map_err(|e| anyhow!("Failed to watch workspace: {}", e))?; - } - } - - { - let mut worker = self.debounce_worker.write().await; - if worker.is_none() { - *worker = Some(self.start_debounce_worker()); - } - } - - Ok(()) - } - - /// Stops watching a workspace. - pub async fn unwatch_workspace(&self, workspace: &Path) -> Result<()> { - info!("Stopping file sync for workspace: {:?}", workspace); - - { - let mut watcher = self.watcher.write().await; - if let Some(w) = watcher.as_mut() { - w.unwatch(workspace) - .map_err(|e| anyhow!("Failed to unwatch workspace: {}", e))?; - } - } - - { - let mut managers = self.workspace_managers.write().await; - managers.remove(workspace); - } - - Ok(()) - } - - /// Starts the filesystem watcher. - async fn start_watcher(self: &Arc) -> Result { - let sync_clone = self.clone(); - - let handle = tokio::runtime::Handle::current(); - - let watcher = - notify::recommended_watcher(move |event: notify::Result| match event { - Ok(event) => { - let sync = sync_clone.clone(); - - handle.spawn(async move { - if let Err(e) = sync.handle_fs_event(event).await { - debug!("Failed to handle fs event: {}", e); - } - }); - } - Err(e) => { - warn!("File watcher error: {}", e); - } - }) - .map_err(|e| anyhow!("Failed to create watcher: {}", e))?; - - Ok(watcher) - } - - /// Handles filesystem events. - async fn handle_fs_event(&self, event: Event) -> Result<()> { - match event.kind { - EventKind::Modify(_) | EventKind::Create(_) => { - for path in event.paths { - if self.should_sync(&path).await { - self.queue_sync(path).await; - } - } - } - _ => {} - } - - Ok(()) - } - - /// Returns whether a file should be synced. - async fn should_sync(&self, path: &Path) -> bool { - if !path.is_file() { - return false; - } - - if let Ok(metadata) = tokio::fs::metadata(path).await { - if metadata.len() > self.config.max_file_size { - return false; - } - } - - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - let ext_with_dot = format!(".{}", ext); - if !self.config.watch_extensions.contains(&ext_with_dot) { - return false; - } - } else { - return false; - } - - if let Some(path_str) = path.to_str() { - for pattern in &self.config.ignore_patterns { - if path_str.contains(pattern) { - return false; - } - } - } - - true - } - - /// Queues a file change for syncing (debounced). - async fn queue_sync(&self, path: PathBuf) { - let mut pending = self.pending_changes.write().await; - - let change = FileChange { - path: path.clone(), - last_modified: SystemTime::now(), - change_count: pending.get(&path).map(|c| c.change_count + 1).unwrap_or(1), - }; - - pending.insert(path.clone(), change); - } - - /// Flushes all pending syncs immediately (skips debounce). - pub async fn flush_pending_syncs(self: &Arc) -> Result<()> { - let changes = { - let mut pending = self.pending_changes.write().await; - pending.drain().collect::>() - }; - - if !changes.is_empty() { - info!("Flushing {} pending file sync(s)", changes.len()); - self.process_changes(changes).await?; - } - - Ok(()) - } - - /// Starts the debounce worker. - fn start_debounce_worker(self: &Arc) -> JoinHandle<()> { - let sync = self.clone(); - let debounce_ms = self.config.debounce_ms; - - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_millis(debounce_ms)).await; - - let changes = { - let mut pending = sync.pending_changes.write().await; - if pending.is_empty() { - continue; - } - pending.drain().collect::>() - }; - - if let Err(e) = sync.process_changes(changes).await { - warn!("Failed to process file changes: {}", e); - } - } - }) - } - - /// Processes file changes (batch sync). - async fn process_changes(self: &Arc, changes: Vec<(PathBuf, FileChange)>) -> Result<()> { - let grouped = self.group_by_workspace(changes).await; - - let mut tasks = Vec::new(); - - for (workspace, files) in grouped { - let sync = self.clone(); - let task = tokio::spawn(async move { - if let Err(e) = sync.sync_workspace_files(workspace.clone(), files).await { - warn!("Failed to sync workspace {:?}: {}", workspace, e); - } - }); - tasks.push(task); - } - - for task in tasks { - let _ = task.await; - } - - Ok(()) - } - - /// Groups files by workspace. - async fn group_by_workspace( - self: &Arc, - changes: Vec<(PathBuf, FileChange)>, - ) -> HashMap> { - let mut grouped: HashMap> = HashMap::new(); - let managers = self.workspace_managers.read().await; - - for (path, _change) in changes { - let workspace = managers.keys().find(|ws| path.starts_with(ws)); - - if let Some(ws) = workspace { - grouped.entry(ws.clone()).or_default().push(path); - } - } - - grouped - } - - /// Syncs workspace files to LSP. - async fn sync_workspace_files( - self: &Arc, - workspace: PathBuf, - files: Vec, - ) -> Result<()> { - let manager = { - let managers = self.workspace_managers.read().await; - managers - .get(&workspace) - .cloned() - .ok_or_else(|| anyhow!("Workspace manager not found: {:?}", workspace))? - }; - - info!( - "Syncing {} file(s) to LSP for workspace: {:?}", - files.len(), - workspace - ); - - let mut tasks = Vec::new(); - - for file in files { - let manager = manager.clone(); - let file = file.clone(); - - let task = tokio::spawn(async move { - if let Err(e) = Self::sync_single_file(manager, file.clone()).await { - debug!("Failed to sync file {:?}: {}", file, e); - } - }); - - tasks.push(task); - } - - for task in tasks { - let _ = task.await; - } - - Ok(()) - } - - /// Syncs a single file to LSP. - async fn sync_single_file(manager: Arc, file: PathBuf) -> Result<()> { - let content = tokio::fs::read_to_string(&file) - .await - .map_err(|e| anyhow!("Failed to read file {:?}: {}", file, e))?; - - let uri = format!("file://{}", file.display()); - - let language = detect_language(&file); - - let is_opened = manager.is_document_opened(&uri).await; - - if is_opened { - manager.change_document(uri, content).await?; - } else { - manager.open_document(uri, language, content).await?; - } - - Ok(()) - } -} - -/// Detects a file language. -fn detect_language(path: &Path) -> String { - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - match ext { - "rs" => "rust", - "ts" => "typescript", - "tsx" => "typescriptreact", - "js" => "javascript", - "jsx" => "javascriptreact", - "py" => "python", - "go" => "go", - "java" => "java", - "c" => "c", - "cpp" | "cc" | "cxx" => "cpp", - "h" | "hpp" => "cpp", - "cs" => "csharp", - "rb" => "ruby", - "php" => "php", - "swift" => "swift", - "kt" => "kotlin", - "scala" => "scala", - _ => "plaintext", - } - .to_string() - } else { - "plaintext".to_string() - } -} - -impl Drop for LspFileSync { - fn drop(&mut self) { - debug!("Dropping LspFileSync"); - } -} diff --git a/src/crates/assembly/core/src/service/lsp/global.rs b/src/crates/assembly/core/src/service/lsp/global.rs deleted file mode 100644 index 938fb0e91f..0000000000 --- a/src/crates/assembly/core/src/service/lsp/global.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! LSP global manager -//! -//! Uses a global singleton to avoid adding dependencies to `AppState`. - -use crate::infrastructure::try_get_path_manager_arc; -use log::{info, warn}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::{Arc, OnceLock}; -use tokio::sync::RwLock; - -use super::file_sync::{FileSyncConfig, LspFileSync}; -use super::WorkspaceLspManager; -use bitfun_services_core::lsp::manager::LspManager; - -type WorkspaceManagerMap = HashMap>; -type GlobalWorkspaceManagers = Arc>; - -/// Global LSP manager instance. -static GLOBAL_LSP_MANAGER: OnceLock>> = OnceLock::new(); - -/// Global workspace manager mapping. -static WORKSPACE_MANAGERS: OnceLock = OnceLock::new(); - -/// Global file sync manager. -static GLOBAL_FILE_SYNC: OnceLock> = OnceLock::new(); - -/// Initializes the global LSP manager. -pub async fn initialize_global_lsp_manager() -> anyhow::Result<()> { - if GLOBAL_LSP_MANAGER.get().is_some() { - warn!("LSP Manager already initialized"); - return Ok(()); - } - - let user_data_dir = try_get_path_manager_arc() - .map_err(|e| anyhow::anyhow!("Failed to create PathManager: {}", e))? - .user_data_dir(); - - let plugins_dir = user_data_dir.join("lsp-plugins"); - - let manager = LspManager::new(plugins_dir); - - manager.initialize().await?; - - GLOBAL_LSP_MANAGER - .set(Arc::new(RwLock::new(manager))) - .map_err(|_| anyhow::anyhow!("Failed to set global LSP manager"))?; - - WORKSPACE_MANAGERS - .set(Arc::new(RwLock::new(HashMap::new()))) - .map_err(|_| anyhow::anyhow!("Failed to set workspace managers"))?; - - let file_sync = LspFileSync::new(FileSyncConfig::default()); - GLOBAL_FILE_SYNC - .set(file_sync) - .map_err(|_| anyhow::anyhow!("Failed to set file sync"))?; - - info!("Global LSP Manager initialized"); - - Ok(()) -} - -/// Returns the global LSP manager. -pub fn get_global_lsp_manager() -> anyhow::Result>> { - GLOBAL_LSP_MANAGER - .get() - .cloned() - .ok_or_else(|| anyhow::anyhow!("LSP Manager not initialized")) -} - -/// Returns whether the LSP manager has been initialized. -pub fn is_lsp_manager_initialized() -> bool { - GLOBAL_LSP_MANAGER.get().is_some() -} - -/// Opens a workspace (creates a workspace manager). -pub async fn open_workspace(workspace_path: PathBuf) -> anyhow::Result> { - open_workspace_with_emitter(workspace_path, None).await -} - -/// Opens a workspace and sets an `EventEmitter` (used for emitting events). -pub async fn open_workspace_with_emitter( - workspace_path: PathBuf, - emitter: Option>, -) -> anyhow::Result> { - info!("Opening workspace: {:?}", workspace_path); - - if !is_lsp_manager_initialized() { - warn!("LSP Manager not initialized yet, initializing now..."); - initialize_global_lsp_manager().await?; - } - - let lsp_manager = get_global_lsp_manager()?; - let workspace_key = workspace_path.to_string_lossy().to_string(); - - let managers = WORKSPACE_MANAGERS - .get() - .ok_or_else(|| anyhow::anyhow!("Workspace managers not initialized"))?; - - { - let managers_read = managers.read().await; - if let Some(manager) = managers_read.get(&workspace_key) { - if let Some(e) = emitter { - manager.set_emitter(e).await; - } - return Ok(manager.clone()); - } - } - - let workspace_manager = WorkspaceLspManager::new(workspace_path.clone(), lsp_manager).await; - - if let Some(e) = emitter { - workspace_manager.set_emitter(e).await; - } - - { - let mut managers_write = managers.write().await; - managers_write.insert(workspace_key.clone(), workspace_manager.clone()); - } - - if let Some(file_sync) = GLOBAL_FILE_SYNC.get() { - if let Err(e) = file_sync - .watch_workspace(workspace_path.clone(), workspace_manager.clone()) - .await - { - warn!("Failed to start file sync for workspace: {}", e); - } - } - - info!("Workspace opened: {:?}", workspace_path); - Ok(workspace_manager) -} - -/// Closes a workspace (cleans up the workspace manager). -pub async fn close_workspace(workspace_path: PathBuf) -> anyhow::Result<()> { - info!("Closing workspace: {:?}", workspace_path); - - let workspace_key = workspace_path.to_string_lossy().to_string(); - - let managers = WORKSPACE_MANAGERS - .get() - .ok_or_else(|| anyhow::anyhow!("Workspace managers not initialized"))?; - - let manager = { - let mut managers_write = managers.write().await; - managers_write.remove(&workspace_key) - }; - - if let Some(manager) = manager { - if let Some(file_sync) = GLOBAL_FILE_SYNC.get() { - if let Err(e) = file_sync.unwatch_workspace(&workspace_path).await { - warn!("Failed to stop file sync: {}", e); - } - } - - manager.dispose().await?; - info!("Workspace closed: {:?}", workspace_path); - } else { - warn!("Workspace not found: {:?}", workspace_path); - } - - Ok(()) -} - -/// Returns the workspace manager. -pub async fn get_workspace_manager( - workspace_path: PathBuf, -) -> anyhow::Result> { - let workspace_key = workspace_path.to_string_lossy().to_string(); - - let managers = WORKSPACE_MANAGERS - .get() - .ok_or_else(|| anyhow::anyhow!("Workspace managers not initialized"))?; - - let managers_read = managers.read().await; - managers_read - .get(&workspace_key) - .cloned() - .ok_or_else(|| anyhow::anyhow!("Workspace not found: {:?}", workspace_path)) -} - -/// Returns all opened workspace paths. -pub async fn get_all_workspace_paths() -> anyhow::Result> { - let managers = WORKSPACE_MANAGERS - .get() - .ok_or_else(|| anyhow::anyhow!("Workspace managers not initialized"))?; - - let managers_read = managers.read().await; - Ok(managers_read.keys().cloned().collect()) -} diff --git a/src/crates/assembly/core/src/service/lsp/manager.rs b/src/crates/assembly/core/src/service/lsp/manager.rs deleted file mode 100644 index f058e117b7..0000000000 --- a/src/crates/assembly/core/src/service/lsp/manager.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Compatibility re-exports for LSP protocol-layer manager. -//! -//! The reusable LSP manager lives in `bitfun-services-core`. - -pub use bitfun_services_core::lsp::manager::LspManager; diff --git a/src/crates/assembly/core/src/service/lsp/mod.rs b/src/crates/assembly/core/src/service/lsp/mod.rs deleted file mode 100644 index 6ba16d9c90..0000000000 --- a/src/crates/assembly/core/src/service/lsp/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Product-facing LSP workspace bridge and compatibility re-exports. -//! -//! Reusable LSP package loading, protocol, process, manager, detection, watch, -//! and debounce helpers live in `bitfun-services-core`. This core module keeps -//! workspace/global/file-sync orchestration, frontend event bridging, and legacy -//! import paths. - -pub mod config_watcher; -pub mod debouncer; -pub mod file_sync; -pub mod global; -pub mod manager; -pub mod plugin_loader; -pub mod process; -pub mod project_detector; -pub mod protocol; -pub mod registry; -pub mod types; -pub mod workspace_manager; - -pub use global::{ - close_workspace, get_all_workspace_paths, get_global_lsp_manager, get_workspace_manager, - initialize_global_lsp_manager, is_lsp_manager_initialized, open_workspace, - open_workspace_with_emitter, -}; -pub use manager::LspManager; -pub use project_detector::{ProjectDetector, ProjectInfo}; -pub use types::{CompletionItem, LspPlugin, PluginSource}; -pub use workspace_manager::{LspEvent, ServerState, ServerStatus, WorkspaceLspManager}; diff --git a/src/crates/assembly/core/src/service/lsp/plugin_loader.rs b/src/crates/assembly/core/src/service/lsp/plugin_loader.rs deleted file mode 100644 index 5022e559a5..0000000000 --- a/src/crates/assembly/core/src/service/lsp/plugin_loader.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Compatibility re-exports for LSP plugin package loading. -//! -//! The reusable package loader lives in `bitfun-services-core`; this legacy path -//! remains for downstream callers that import through `bitfun_core::service::lsp`. - -pub use bitfun_services_core::lsp::plugin_loader::PluginLoader; diff --git a/src/crates/assembly/core/src/service/lsp/process.rs b/src/crates/assembly/core/src/service/lsp/process.rs deleted file mode 100644 index 5f5ee3c040..0000000000 --- a/src/crates/assembly/core/src/service/lsp/process.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Compatibility re-exports for LSP server process lifecycle. -//! -//! The reusable LSP process owner lives in `bitfun-services-core`. - -pub use bitfun_services_core::lsp::process::{ - CrashCallback, DiagnosticsCallback, LspServerProcess, ProgressCallback, TokenCreateCallback, -}; diff --git a/src/crates/assembly/core/src/service/lsp/project_detector.rs b/src/crates/assembly/core/src/service/lsp/project_detector.rs deleted file mode 100644 index 5329011f5f..0000000000 --- a/src/crates/assembly/core/src/service/lsp/project_detector.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Compatibility re-exports for LSP project detection. -//! -//! The reusable detector lives in `bitfun-services-core`. - -pub use bitfun_services_core::lsp::project_detector::{ProjectDetector, ProjectInfo}; diff --git a/src/crates/assembly/core/src/service/lsp/protocol.rs b/src/crates/assembly/core/src/service/lsp/protocol.rs deleted file mode 100644 index 11b90d5111..0000000000 --- a/src/crates/assembly/core/src/service/lsp/protocol.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Compatibility re-exports for LSP protocol encoding and decoding. -//! -//! The reusable protocol helpers live in `bitfun-services-core`. - -pub use bitfun_services_core::lsp::protocol::{ - create_notification, create_request, extract_result, read_message, write_message, -}; diff --git a/src/crates/assembly/core/src/service/lsp/registry.rs b/src/crates/assembly/core/src/service/lsp/registry.rs deleted file mode 100644 index bef84c3fb6..0000000000 --- a/src/crates/assembly/core/src/service/lsp/registry.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Compatibility re-exports for LSP plugin registry rules. -//! -//! The pure registry owner is `bitfun-services-core`; this legacy path remains -//! for downstream callers that import through `bitfun_core::service::lsp`. - -pub use bitfun_services_core::lsp::{ - LspPluginRegistryError, LspSupportedExtensions, PluginRegistry, -}; diff --git a/src/crates/assembly/core/src/service/lsp/types.rs b/src/crates/assembly/core/src/service/lsp/types.rs deleted file mode 100644 index 9338349b0e..0000000000 --- a/src/crates/assembly/core/src/service/lsp/types.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Compatibility re-exports for LSP protocol and plugin manifest DTOs. -//! -//! The shared contract owner is `bitfun-core-types`; this legacy path remains -//! for downstream callers that import through `bitfun_core::service::lsp`. - -pub use bitfun_core_types::lsp::*; diff --git a/src/crates/assembly/core/src/service/lsp/workspace_manager.rs b/src/crates/assembly/core/src/service/lsp/workspace_manager.rs deleted file mode 100644 index 5d9501af14..0000000000 --- a/src/crates/assembly/core/src/service/lsp/workspace_manager.rs +++ /dev/null @@ -1,1423 +0,0 @@ -//! Workspace-level LSP manager -//! -//! Core responsibilities: -//! - Manage the lifecycle of all LSP servers within a workspace -//! - Automatically start and stop servers -//! - Manage document state -//! - Error recovery and health checks -//! - Integrate filesystem monitoring -//! - Push real-time events to the frontend - -use anyhow::{anyhow, Result}; -use log::{debug, error, info, trace, warn}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; -use tokio::sync::RwLock; -use tokio::task::JoinHandle; - -use crate::infrastructure::events::EventEmitter; -use bitfun_core_types::lsp::{CompletionItem, InlayHint}; -use bitfun_services_core::lsp::config_watcher::ConfigWatcher; -use bitfun_services_core::lsp::manager::LspManager; -use bitfun_services_core::lsp::project_detector::{ProjectDetector, ProjectInfo}; - -/// LSP event types (pushed to the frontend). -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", content = "data")] -pub enum LspEvent { - /// Server state changed. - ServerStateChanged { - workspace_path: String, - language: String, - status: String, - message: Option, - }, - /// Document opened. - DocumentOpened { - workspace_path: String, - uri: String, - language: String, - }, - /// Document closed. - DocumentClosed { workspace_path: String, uri: String }, - /// Workspace opened. - WorkspaceOpened { workspace_path: String }, - /// Workspace closed. - WorkspaceClosed { workspace_path: String }, - /// Server error. - ServerError { - workspace_path: String, - language: String, - error: String, - }, - /// Project detection completed. - ProjectDetected { - workspace_path: String, - project_info: ProjectInfo, - }, - /// Indexing progress updated. - IndexingProgress { - workspace_path: String, - language: String, - plugin_name: String, - progress: u32, - message: String, - }, - /// Indexing completed. - IndexingComplete { - workspace_path: String, - language: String, - plugin_name: String, - }, - /// Diagnostics (errors, warnings, etc.). - Diagnostics { - workspace_path: String, - uri: String, - diagnostics: Vec, - }, -} - -/// Server status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum ServerStatus { - /// Stopped. - Stopped, - /// Starting. - Starting, - /// Running. - Running, - /// Failed. - Failed, - /// Restarting. - Restarting, -} - -/// Server state details. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ServerState { - /// Status. - pub status: ServerStatus, - /// Language identifier. - pub language: String, - /// Start time. - pub started_at: Option, - /// Last error message. - pub last_error: Option, - /// Restart count. - pub restart_count: u32, - /// Open document count. - pub document_count: usize, -} - -impl Default for ServerState { - fn default() -> Self { - Self { - status: ServerStatus::Stopped, - language: String::new(), - started_at: None, - last_error: None, - restart_count: 0, - document_count: 0, - } - } -} - -/// Document state. -#[derive(Debug, Clone)] -struct DocumentState { - #[allow(dead_code)] - uri: String, - language: String, - version: i32, - #[allow(dead_code)] - opened_at: SystemTime, -} - -/// Token state. -#[derive(Debug, Clone)] -enum TokenState { - /// Created but not started. - Created, - /// In progress (includes percentage). - InProgress(u32), - /// Completed. - Completed, -} - -/// Token tracking info. -#[derive(Debug, Clone)] -struct TokenInfo { - /// Token identifier. - token: String, - /// Token state. - state: TokenState, - /// Token title/description. - title: String, - /// Created time. - created_at: SystemTime, - /// Last updated time. - last_updated: SystemTime, -} - -/// Workspace LSP manager. -pub struct WorkspaceLspManager { - /// Workspace path. - workspace_path: PathBuf, - /// LSP manager handle. - lsp_manager: Arc>, - /// Server states. - server_states: Arc>>, - /// Document states. - documents: Arc>>, - /// Startup synchronization locks (prevents duplicate starts). - starting_locks: Arc>>>, - /// Health check task handle. - health_check_handle: Arc>>>, - /// `EventEmitter` used to emit events to the frontend. - emitter: Arc>>>, - /// Configuration file watcher. - config_watcher: Arc>>, - /// Indexing token tracking (`language -> token list`). - indexing_tokens: Arc>>>, - /// Workspace initialization complete flag (project detection + pre-start finished). - workspace_initialized: Arc>, -} - -impl WorkspaceLspManager { - /// Creates a new workspace manager. - pub async fn new(workspace_path: PathBuf, lsp_manager: Arc>) -> Arc { - let manager = Arc::new(Self { - workspace_path: workspace_path.clone(), - lsp_manager, - server_states: Arc::new(RwLock::new(HashMap::new())), - documents: Arc::new(RwLock::new(HashMap::new())), - starting_locks: Arc::new(RwLock::new(HashMap::new())), - health_check_handle: Arc::new(RwLock::new(None)), - emitter: Arc::new(RwLock::new(None)), - config_watcher: Arc::new(RwLock::new(None)), - indexing_tokens: Arc::new(RwLock::new(HashMap::new())), - workspace_initialized: Arc::new(tokio::sync::RwLock::new(false)), - }); - - manager.initialize().await; - - manager.start_config_watcher_internal(); - - let manager_clone = manager.clone(); - let workspace_path_clone = workspace_path.clone(); - tokio::spawn(async move { - manager_clone - .detect_and_prestart(workspace_path_clone) - .await; - }); - - manager - } - - /// Detects project type and pre-starts servers. - async fn detect_and_prestart(&self, workspace_path: PathBuf) { - debug!( - "Starting project detection and prestart for: {:?}", - workspace_path - ); - - match ProjectDetector::detect(&workspace_path).await { - Ok(project_info) => { - info!("Project detected: languages={:?}", project_info.languages); - - self.emit_event(LspEvent::ProjectDetected { - workspace_path: workspace_path.display().to_string(), - project_info: project_info.clone(), - }) - .await; - - let languages_to_start = ProjectDetector::should_prestart(&project_info); - - if !languages_to_start.is_empty() { - info!("Pre-starting language servers: {:?}", languages_to_start); - - for language in languages_to_start { - if let Err(e) = self.prestart_server(&language).await { - warn!("Failed to prestart {} server: {}", language, e); - } - } - } else { - debug!("Large project detected, using on-demand loading"); - } - } - Err(e) => { - warn!("Failed to detect project type: {}", e); - } - } - - { - let mut initialized = self.workspace_initialized.write().await; - *initialized = true; - } - } - - /// Sets an `EventEmitter` to enable event emission. - pub async fn set_emitter(&self, emitter: Arc) { - let mut e = self.emitter.write().await; - *e = Some(emitter); - } - - /// Emits an LSP event to the frontend. - async fn emit_event(&self, event: LspEvent) { - if let Some(emitter) = self.emitter.read().await.as_ref() { - let event_data = match serde_json::to_value(&event) { - Ok(v) => v, - Err(e) => { - error!("Failed to serialize LSP event: {}", e); - return; - } - }; - if let Err(e) = emitter.emit("lsp-event", event_data).await { - error!("Failed to emit LSP event: {}", e); - } - } - } - - /// Initializes the workspace. - async fn initialize(&self) { - self.emit_event(LspEvent::WorkspaceOpened { - workspace_path: self.workspace_path.display().to_string(), - }) - .await; - - self.start_health_check().await; - } - - /// Opens a document (auto-starts the server). - pub async fn open_document( - &self, - uri: String, - language: String, - content: String, - ) -> Result<()> { - { - let docs = self.documents.read().await; - if docs.contains_key(&uri) { - debug!("Document already open: {}", uri); - return Ok(()); - } - } - - let server_language = match self.get_running_server_for_language(&language).await { - Some(lang) => lang, - None => { - trace!( - "LSP server not running for language: {}, skipping didOpen", - language - ); - return Ok(()); - } - }; - - let lsp = self.lsp_manager.read().await; - lsp.did_open(&server_language, &uri, &content) - .await - .map_err(|e| { - error!("Failed to send didOpen: {}", e); - e - })?; - - { - let mut docs = self.documents.write().await; - docs.insert( - uri.clone(), - DocumentState { - uri: uri.clone(), - language: language.clone(), - version: 0, - opened_at: SystemTime::now(), - }, - ); - } - - self.update_server_document_count(&language).await; - - self.emit_event(LspEvent::DocumentOpened { - workspace_path: self.workspace_path.display().to_string(), - uri: uri.clone(), - language: language.clone(), - }) - .await; - - Ok(()) - } - - /// Updates a document. - pub async fn change_document(&self, uri: String, content: String) -> Result<()> { - let (language, version) = { - let mut docs = self.documents.write().await; - let doc = docs - .get_mut(&uri) - .ok_or_else(|| anyhow!("Document not open: {}", uri))?; - - doc.version += 1; - (doc.language.clone(), doc.version) - }; - - let server_language = self.get_server_language(&language).await; - - let lsp = self.lsp_manager.read().await; - lsp.did_change(&server_language, &uri, version, &content) - .await?; - - Ok(()) - } - - /// Saves a document. - pub async fn save_document(&self, uri: String) -> Result<()> { - let language = { - let docs = self.documents.read().await; - let doc = docs - .get(&uri) - .ok_or_else(|| anyhow!("Document not open: {}", uri))?; - doc.language.clone() - }; - - let server_language = self.get_server_language(&language).await; - - let lsp = self.lsp_manager.read().await; - lsp.did_save(&server_language, &uri).await?; - - Ok(()) - } - - /// Closes a document. - pub async fn close_document(&self, uri: String) -> Result<()> { - let language = { - let mut docs = self.documents.write().await; - let doc = docs - .remove(&uri) - .ok_or_else(|| anyhow!("Document not open: {}", uri))?; - doc.language.clone() - }; - - let server_language = self.get_server_language(&language).await; - - let lsp = self.lsp_manager.read().await; - lsp.did_close(&server_language, &uri).await?; - - self.update_server_document_count(&server_language).await; - - Ok(()) - } - - /// Returns whether a document is open (used by `LspFileSync`). - pub async fn is_document_opened(&self, uri: &str) -> bool { - let docs = self.documents.read().await; - docs.contains_key(uri) - } - - /// Quickly checks whether a server is running (does not trigger query or startup). - /// Returns the actual running server language key (may differ from the requested language). - async fn get_running_server_for_language(&self, language: &str) -> Option { - let states = self.server_states.read().await; - - if let Some(state) = states.get(language) { - if state.status == ServerStatus::Running { - return Some(language.to_string()); - } - } - - for (lang, state) in states.iter() { - if state.status == ServerStatus::Running { - let is_related = (language == "c" && lang == "cpp") - || (language == "cpp" && lang == "c") - || (language == "javascript" && lang == "typescript") - || (language == "typescript" && lang == "javascript") - || (language == "javascriptreact" && lang == "javascript") - || (language == "typescriptreact" && lang == "typescript"); - - if is_related { - return Some(lang.clone()); - } - } - } - - None - } - - /// Returns the actual server language key (handles aliases, e.g. c -> cpp). - async fn get_server_language(&self, language: &str) -> String { - { - let states = self.server_states.read().await; - if states.contains_key(language) { - return language.to_string(); - } - } - - let states = self.server_states.read().await; - for (lang, state) in states.iter() { - if state.status == ServerStatus::Running { - let is_related = (language == "c" && lang == "cpp") - || (language == "cpp" && lang == "c") - || (language == "javascript" && lang == "typescript") - || (language == "typescript" && lang == "javascript") - || (language == "javascriptreact" && lang == "javascript") - || (language == "typescriptreact" && lang == "typescript"); - - if is_related { - return lang.clone(); - } - } - } - - language.to_string() - } - - /// Ensures the server is running (prevents duplicate starts). - /// Returns the actual server language key in use (may differ from the requested one, e.g. c -> cpp). - #[allow(dead_code)] - async fn ensure_server_running(&self, language: &str) -> Result { - let status = { - let states = self.server_states.read().await; - - states.get(language).map(|state| state.status.clone()) - }; - - if let Some(status) = status { - match status { - ServerStatus::Running => { - return Ok(language.to_string()); - } - ServerStatus::Starting | ServerStatus::Restarting => { - debug!("Server is starting, waiting: {}", language); - self.wait_for_server_start(language).await?; - return Ok(language.to_string()); - } - _ => {} - } - } - - let related_lang = { - let states = self.server_states.read().await; - - let mut result = None; - for (lang, state) in states.iter() { - if state.status == ServerStatus::Running { - let is_related = (language == "c" && lang == "cpp") - || (language == "cpp" && lang == "c") - || (language == "javascript" && lang == "typescript") - || (language == "typescript" && lang == "javascript"); - - if is_related { - result = Some(lang.clone()); - break; - } - } - } - result - }; - - if let Some(related_lang) = related_lang { - debug!("Using {} server for {}", related_lang, language); - return Ok(related_lang); - } - - info!("Starting {} server", language); - self.start_server(language).await?; - Ok(language.to_string()) - } - - /// Starts a server (with retries). - async fn start_server(&self, language: &str) -> Result<()> { - let notify = Arc::new(tokio::sync::Notify::new()); - { - let mut locks = self.starting_locks.write().await; - locks.insert(language.to_string(), notify.clone()); - } - - { - let mut states = self.server_states.write().await; - states.insert( - language.to_string(), - ServerState { - status: ServerStatus::Starting, - language: language.to_string(), - started_at: None, - last_error: None, - restart_count: 0, - document_count: 0, - }, - ); - } - - self.emit_server_state_changed(language).await; - - let result = self.start_server_internal(language).await; - - let final_result = match result { - Ok(_) => { - { - let mut states = self.server_states.write().await; - if let Some(state) = states.get_mut(language) { - state.status = ServerStatus::Running; - state.started_at = match SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - { - Ok(duration) => Some(duration.as_secs()), - Err(e) => { - warn!( - "Failed to compute LSP server start timestamp: language={}, error={}", - language, e - ); - Some(0) - } - }; - } - info!("LSP server started: {}", language); - } - - self.emit_server_state_changed(language).await; - - notify.notify_waiters(); - - Ok(()) - } - Err(e) => { - { - let mut states = self.server_states.write().await; - if let Some(state) = states.get_mut(language) { - state.status = ServerStatus::Failed; - state.last_error = Some(e.to_string()); - } - - error!("Failed to start LSP server {}: {}", language, e); - } - - self.emit_server_state_changed(language).await; - - notify.notify_waiters(); - - Err(e) - } - }; - - { - let mut locks = self.starting_locks.write().await; - locks.remove(language); - } - - final_result - } - - /// Sends an aggregated overall progress event. - async fn emit_aggregated_progress( - tokens: Arc>>>, - workspace: PathBuf, - language: String, - emitter: Arc>>>, - lsp_manager: Arc>, - ) { - let tokens_map = tokens.read().await; - - if let Some(lang_tokens) = tokens_map.get(&language) { - if lang_tokens.is_empty() { - return; - } - - let active_tokens: Vec<_> = lang_tokens - .iter() - .filter(|t| !matches!(t.state, TokenState::Created)) - .collect(); - - if active_tokens.is_empty() { - return; - } - - let total = active_tokens.len(); - let completed = active_tokens - .iter() - .filter(|t| matches!(t.state, TokenState::Completed)) - .count(); - let in_progress_tokens: Vec<_> = active_tokens - .iter() - .filter(|t| matches!(t.state, TokenState::InProgress(_))) - .collect(); - - let progress_sum: u32 = active_tokens - .iter() - .map(|t| match t.state { - TokenState::Created => 0, - TokenState::InProgress(p) => p, - TokenState::Completed => 100, - }) - .sum(); - - let overall_progress = if total > 0 { - progress_sum / total as u32 - } else { - 0 - }; - - let message = if completed == total { - format!("Indexing completed ({} tasks)", total) - } else if let Some(active) = in_progress_tokens.first() { - let title = if active.title.is_empty() { - "..." - } else { - &active.title - }; - format!("{} ({}/{})", title, completed, total) - } else { - format!("Indexing ({}/{})", completed, total) - }; - - let plugin_name = { - let lsp_mgr = lsp_manager.read().await; - lsp_mgr - .find_plugin_by_language(&language) - .await - .map(|p| p.name.clone()) - .unwrap_or_else(|| language.clone()) - }; - - let is_completed = completed == total && total > 0; - if let Some(emit) = emitter.read().await.as_ref() { - let progress_event = LspEvent::IndexingProgress { - workspace_path: workspace.display().to_string(), - language: language.clone(), - plugin_name: plugin_name.clone(), - progress: overall_progress, - message: message.clone(), - }; - if let Ok(event_data) = serde_json::to_value(&progress_event) { - let _ = emit.emit("lsp-event", event_data).await; - } - - if is_completed { - info!("[{}] Indexing completed", language); - let complete_event = LspEvent::IndexingComplete { - workspace_path: workspace.display().to_string(), - language: language.clone(), - plugin_name: plugin_name.clone(), - }; - if let Ok(event_data) = serde_json::to_value(&complete_event) { - let _ = emit.emit("lsp-event", event_data).await; - } - } - } - - if is_completed { - drop(tokens_map); - let mut tokens_map_mut = tokens.write().await; - if let Some(lang_tokens) = tokens_map_mut.get_mut(&language) { - lang_tokens.clear(); - } - } - } - } - - /// Internal server startup implementation. - async fn start_server_internal(&self, language: &str) -> Result<()> { - let language_clone = language.to_string(); - let server_states = self.server_states.clone(); - let workspace_path = self.workspace_path.clone(); - let emitter = self.emitter.clone(); - - let crash_callback = Arc::new(move |plugin_id: String| { - let language = language_clone.clone(); - let states = server_states.clone(); - let workspace = workspace_path.clone(); - let emitter_clone = emitter.clone(); - - tokio::spawn(async move { - error!("LSP server crashed: {} (plugin: {})", language, plugin_id); - - { - let mut states = states.write().await; - if let Some(state) = states.get_mut(&language) { - state.status = ServerStatus::Failed; - state.last_error = - Some("Server process crashed or became unresponsive".to_string()); - } - } - - if let Some(emitter) = emitter_clone.read().await.as_ref() { - let error_event = LspEvent::ServerError { - workspace_path: workspace.display().to_string(), - language: language.clone(), - error: "Server process crashed or became unresponsive".to_string(), - }; - if let Ok(event_data) = serde_json::to_value(&error_event) { - let _ = emitter.emit("lsp-event", event_data).await; - } - - let state_event = LspEvent::ServerStateChanged { - workspace_path: workspace.display().to_string(), - language: language.clone(), - status: "failed".to_string(), - message: Some("Server crashed".to_string()), - }; - if let Ok(event_data) = serde_json::to_value(&state_event) { - let _ = emitter.emit("lsp-event", event_data).await; - } - } - }); - }) as Arc; - - let language_clone2 = language.to_string(); - let indexing_tokens2 = self.indexing_tokens.clone(); - let workspace_path_for_token = self.workspace_path.clone(); - let emitter_for_token = self.emitter.clone(); - let lsp_manager_for_token = self.lsp_manager.clone(); - - let token_create_callback = Arc::new(move |token: String| { - let language = language_clone2.clone(); - let tokens = indexing_tokens2.clone(); - let workspace = workspace_path_for_token.clone(); - let emitter_clone = emitter_for_token.clone(); - let lsp_mgr = lsp_manager_for_token.clone(); - - tokio::spawn(async move { - { - let mut tokens_map = tokens.write().await; - let lang_tokens = tokens_map.entry(language.clone()).or_insert_with(Vec::new); - - if !lang_tokens.iter().any(|t| t.token == token) { - let now = SystemTime::now(); - lang_tokens.push(TokenInfo { - token: token.clone(), - state: TokenState::Created, - title: String::new(), - created_at: now, - last_updated: now, - }); - } else { - return; - } - } - - Self::emit_aggregated_progress(tokens, workspace, language, emitter_clone, lsp_mgr) - .await; - }); - }) as Arc; - - let language_clone3 = language.to_string(); - let workspace_path3 = self.workspace_path.clone(); - let emitter_for_progress = self.emitter.clone(); - let indexing_tokens3 = self.indexing_tokens.clone(); - let lsp_manager_for_progress = self.lsp_manager.clone(); - - let progress_callback = Arc::new( - move |kind: String, token: String, percentage: Option, message: String| { - let language = language_clone3.clone(); - let workspace = workspace_path3.clone(); - let emitter_clone = emitter_for_progress.clone(); - let tokens = indexing_tokens3.clone(); - let lsp_mgr = lsp_manager_for_progress.clone(); - - tokio::spawn(async move { - { - let mut tokens_map = tokens.write().await; - if let Some(lang_tokens) = tokens_map.get_mut(&language) { - if let Some(token_info) = - lang_tokens.iter_mut().find(|t| t.token == token) - { - token_info.last_updated = SystemTime::now(); - match kind.as_str() { - "begin" => { - token_info.state = TokenState::InProgress(0); - token_info.title = message.clone(); - info!("[{}] Indexing started: {}", language, message); - } - "report" => { - let progress = percentage.unwrap_or(0); - token_info.state = TokenState::InProgress(progress); - } - "end" => { - token_info.state = TokenState::Completed; - info!("[{}] Indexing task completed", language); - } - _ => {} - } - } - } - } - - { - let mut tokens_map = tokens.write().await; - if let Some(lang_tokens) = tokens_map.get_mut(&language) { - let now = SystemTime::now(); - lang_tokens.retain(|t| { - if matches!(t.state, TokenState::Created) { - if let Ok(elapsed) = now.duration_since(t.created_at) { - return elapsed.as_secs() <= 5; - } - } - true - }); - } - } - - Self::emit_aggregated_progress( - tokens, - workspace, - language, - emitter_clone, - lsp_mgr, - ) - .await; - }); - }, - ) - as Arc, String) + Send + Sync>; - - let _language_clone4 = language.to_string(); - let workspace_path4 = self.workspace_path.clone(); - let emitter_for_diagnostics = self.emitter.clone(); - // Grab the dedicated cache handle once so the per-publishDiagnostics - // path never takes the outer `lsp_manager` lock, and never holds a - // read guard across the emitter await. - let diagnostics_cache = { - let lsp = self.lsp_manager.read().await; - lsp.diagnostics_cache_handle() - }; - - let diagnostics_callback = - Arc::new(move |uri: String, diagnostics: Vec| { - let workspace = workspace_path4.clone(); - let emitter_clone = emitter_for_diagnostics.clone(); - let cache = diagnostics_cache.clone(); - - tokio::spawn(async move { - // Cache and event share the same allocation via Arc. - let diagnostics = Arc::new(diagnostics); - { - let mut cache = cache.write().await; - cache.insert(uri.clone(), diagnostics.clone()); - } - - let emitter_guard = emitter_clone.read().await; - if let Some(emitter) = emitter_guard.as_ref() { - debug!( - "Emitting diagnostics event: uri={}, count={}", - uri, - diagnostics.len() - ); - // Hand-built to match `LspEvent::Diagnostics` under - // #[serde(tag = "type", content = "data")]; this avoids - // deep-cloning the diagnostics array into an - // intermediate event struct before serialization. - let event_data = serde_json::json!({ - "type": "Diagnostics", - "data": { - "workspace_path": workspace.display().to_string(), - "uri": uri, - "diagnostics": &*diagnostics, - } - }); - if let Err(e) = emitter.emit("lsp-event", event_data).await { - error!("Failed to emit diagnostics event: {}", e); - } - } - }); - }) as Arc) + Send + Sync>; - - let lsp = self.lsp_manager.read().await; - lsp.start_server( - language, - Some(self.workspace_path.clone()), - Some(crash_callback), - Some(progress_callback), - Some(token_create_callback), - Some(diagnostics_callback), - ) - .await - } - - /// Waits for server startup to complete. - #[allow(dead_code)] - async fn wait_for_server_start(&self, language: &str) -> Result<()> { - let notify = { - let locks = self.starting_locks.read().await; - locks.get(language).cloned() - }; - - if let Some(notify) = notify { - let timeout_duration = Duration::from_secs(60); - tokio::select! { - _ = notify.notified() => { - - let states = self.server_states.read().await; - if let Some(state) = states.get(language) { - if state.status == ServerStatus::Running { - return Ok(()); - } else { - return Err(anyhow!( - "Server failed to start: {}", - state.last_error.as_deref().unwrap_or("Unknown error") - )); - } - } - Err(anyhow!("Server state not found after start")) - } - _ = tokio::time::sleep(timeout_duration) => { - Err(anyhow!("Server start timeout")) - } - } - } else { - Ok(()) - } - } - - /// Pre-starts a server (used during workspace initialization). - pub async fn prestart_server(&self, language: &str) -> Result<()> { - info!("Pre-starting LSP server for language: {}", language); - - self.start_server(language).await?; - - Ok(()) - } - - /// Stops a server. - pub async fn stop_server(&self, language: &str) -> Result<()> { - info!("Stopping LSP server: {}", language); - - let docs_to_close: Vec = { - let docs = self.documents.read().await; - docs.iter() - .filter(|(_, doc)| doc.language == language) - .map(|(uri, _)| uri.clone()) - .collect() - }; - - for uri in docs_to_close { - let _ = self.close_document(uri).await; - } - - let lsp = self.lsp_manager.read().await; - lsp.stop_server(language).await?; - - { - let mut states = self.server_states.write().await; - states.remove(language); - } - - self.emit_server_state_changed(language).await; - - info!("LSP server stopped: {}", language); - Ok(()) - } - - /// Returns server state. - pub async fn get_server_state(&self, language: &str) -> ServerState { - let states = self.server_states.read().await; - states - .get(language) - .cloned() - .unwrap_or_else(|| ServerState { - status: ServerStatus::Stopped, - language: language.to_string(), - ..Default::default() - }) - } - - /// Returns all server states. - pub async fn get_all_server_states(&self) -> HashMap { - let states = self.server_states.read().await; - states.clone() - } - - /// Updates the server document count. - async fn update_server_document_count(&self, language: &str) { - let count = { - let docs = self.documents.read().await; - docs.values().filter(|doc| doc.language == language).count() - }; - - let mut states = self.server_states.write().await; - if let Some(state) = states.get_mut(language) { - state.document_count = count; - } - } - - /// Starts health checks. - async fn start_health_check(&self) { - let server_states = self.server_states.clone(); - let lsp_manager = self.lsp_manager.clone(); - - let handle = tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(30)); - - loop { - interval.tick().await; - - let languages: Vec = { - let states = server_states.read().await; - states - .iter() - .filter(|(_, state)| state.status == ServerStatus::Running) - .map(|(lang, _)| lang.clone()) - .collect() - }; - - for language in languages { - let states = server_states.read().await; - let needs_check = states - .get(&language) - .map(|s| matches!(s.status, ServerStatus::Running)) - .unwrap_or(false); - drop(states); - - if needs_check { - let lsp = lsp_manager.read().await; - let is_alive = lsp.is_server_alive(&language).await; - drop(lsp); - - if !is_alive { - error!("Health check detected dead process: {}", language); - - let mut states = server_states.write().await; - if let Some(state) = states.get_mut(&language) { - state.status = ServerStatus::Failed; - state.last_error = - Some("Server process died unexpectedly".to_string()); - } - drop(states); - - let lsp = lsp_manager.read().await; - if let Err(e) = lsp.stop_server(&language).await { - warn!("Failed to cleanup dead server {}: {}", language, e); - } - } - } - } - } - }); - - let mut handle_lock = self.health_check_handle.write().await; - *handle_lock = Some(handle); - } - - /// Emits a server state change event. - async fn emit_server_state_changed(&self, language: &str) { - let state = self.get_server_state(language).await; - - debug!("Server state changed: {} -> {:?}", language, state.status); - } - - /// Gets code completion (via business layer). - pub async fn get_completions( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result> { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - - let lsp = self.lsp_manager.read().await; - lsp.get_completions(&server_language, uri, line, character) - .await - } - - /// Gets hover information. - pub async fn get_hover( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.get_hover(&server_language, uri, line, character).await - } - - /// Go to definition. - pub async fn goto_definition( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.goto_definition(&server_language, uri, line, character) - .await - } - - /// Finds references. - pub async fn find_references( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.find_references(&server_language, uri, line, character) - .await - } - - /// Gets code actions. - pub async fn get_code_actions( - &self, - language: &str, - uri: &str, - range: serde_json::Value, - context: serde_json::Value, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.get_code_actions(&server_language, uri, range, context) - .await - } - - /// Formats a document. - pub async fn format_document( - &self, - language: &str, - uri: &str, - tab_size: u32, - insert_spaces: bool, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.format_document(&server_language, uri, tab_size, insert_spaces) - .await - } - - /// Gets inlay hints. - pub async fn get_inlay_hints( - &self, - language: &str, - uri: &str, - start_line: u32, - start_character: u32, - end_line: u32, - end_character: u32, - ) -> Result> { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.get_inlay_hints( - &server_language, - uri, - start_line, - start_character, - end_line, - end_character, - ) - .await - } - - /// Renames a symbol. - pub async fn rename( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - new_name: &str, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.rename(&server_language, uri, line, character, new_name) - .await - } - - /// Gets document highlights. - pub async fn get_document_highlight( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.get_document_highlight(&server_language, uri, line, character) - .await - } - - /// Starts the config file watcher (internal; requires `Arc`). - fn start_config_watcher_internal(self: &Arc) { - let workspace_path = self.workspace_path.clone(); - let manager_weak = Arc::downgrade(self); - - let on_config_changed = Arc::new(move |language: String, _config_file: String| { - if let Some(manager) = manager_weak.upgrade() { - info!( - "Config file changed for {}, scheduling server restart", - language - ); - - let manager_clone = manager.clone(); - let language_clone = language.clone(); - tokio::spawn(async move { - info!("Restarting {} server due to config change", language_clone); - - if let Err(e) = manager_clone.stop_server(&language_clone).await { - warn!("Failed to stop {} server: {}", language_clone, e); - return; - } - - tokio::time::sleep(Duration::from_millis(500)).await; - - if let Err(e) = manager_clone.start_server(&language_clone).await { - error!("Failed to restart {} server: {}", language_clone, e); - } else { - info!("{} server restarted successfully", language_clone); - - manager_clone - .emit_event(LspEvent::ServerStateChanged { - workspace_path: manager_clone.workspace_path.display().to_string(), - language: language_clone, - status: "running".to_string(), - message: Some("Config file updated, server restarted".to_string()), - }) - .await; - } - }); - } - }); - - let config_watcher = self.config_watcher.clone(); - let workspace_path_clone = workspace_path.clone(); - tokio::spawn(async move { - match ConfigWatcher::new(workspace_path_clone, on_config_changed) { - Ok(watcher) => { - let mut config_watcher_lock = config_watcher.write().await; - *config_watcher_lock = Some(watcher); - } - Err(e) => { - warn!("Failed to start config file watcher: {}", e); - } - } - }); - } - - /// Cleans up resources. - pub async fn dispose(&self) -> Result<()> { - info!("Disposing workspace LSP manager"); - - { - let mut handle = self.health_check_handle.write().await; - if let Some(h) = handle.take() { - h.abort(); - } - } - - { - let mut watcher = self.config_watcher.write().await; - *watcher = None; - } - - let docs: Vec = { - let docs = self.documents.read().await; - docs.keys().cloned().collect() - }; - - for uri in docs { - let _ = self.close_document(uri).await; - } - - let languages: Vec = { - let states = self.server_states.read().await; - states.keys().cloned().collect() - }; - - for language in languages { - let _ = self.stop_server(&language).await; - } - - info!("Workspace LSP manager disposed"); - Ok(()) - } - - /// Gets document symbols. - pub async fn get_document_symbols( - &self, - language: &str, - uri: &str, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.get_document_symbols(&server_language, uri).await - } - - /// Gets diagnostics for a file (e.g. for UI or other callers). - /// Returns cached diagnostics without triggering new LSP requests. - pub async fn get_diagnostics(&self, uri: &str) -> Result> { - let lsp = self.lsp_manager.read().await; - Ok(lsp.get_diagnostics(uri).await) - } - - /// Gets semantic tokens (used for semantic-level syntax highlighting). - pub async fn get_semantic_tokens( - &self, - language: &str, - uri: &str, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.get_semantic_tokens(&server_language, uri).await - } - - /// Gets semantic tokens range (for incremental updates). - pub async fn get_semantic_tokens_range( - &self, - language: &str, - uri: &str, - range: serde_json::Value, - ) -> Result { - let server_language = self - .get_running_server_for_language(language) - .await - .ok_or_else(|| anyhow!("LSP server not running for language: {}", language))?; - let lsp = self.lsp_manager.read().await; - lsp.get_semantic_tokens_range(&server_language, uri, range) - .await - } -} - -impl Drop for WorkspaceLspManager { - fn drop(&mut self) { - debug!("WorkspaceLspManager dropped"); - } -} diff --git a/src/crates/assembly/core/src/service/mod.rs b/src/crates/assembly/core/src/service/mod.rs index 5f03f47eb9..8c9159b8d4 100644 --- a/src/crates/assembly/core/src/service/mod.rs +++ b/src/crates/assembly/core/src/service/mod.rs @@ -22,8 +22,6 @@ pub mod git; // Git service pub mod i18n; // I18n service #[cfg(feature = "agent-runtime")] pub(crate) mod instruction_context; // Workspace instruction file prompt helpers -#[cfg(feature = "lsp")] -pub mod lsp; // LSP (Language Server Protocol) system #[cfg(feature = "mcp-runtime")] pub mod mcp; // MCP (Model Context Protocol) system #[cfg(feature = "remote-connect")] @@ -98,8 +96,6 @@ pub use git::GitService; #[cfg(feature = "i18n-runtime")] pub use i18n::{get_global_i18n_service, I18nService}; pub use i18n::{I18nConfig, LocaleId, LocaleMetadata}; -#[cfg(feature = "lsp")] -pub use lsp::LspManager; #[cfg(feature = "mcp-runtime")] pub use mcp::MCPService; #[cfg(feature = "review-platform")] diff --git a/src/crates/contracts/AGENTS-CN.md b/src/crates/contracts/AGENTS-CN.md index 4f82b96136..1ed5ce489b 100644 --- a/src/crates/contracts/AGENTS-CN.md +++ b/src/crates/contracts/AGENTS-CN.md @@ -8,7 +8,7 @@ | Crate | 职责 | 本地文档 | |---|---|---| -| `core-types` | 共享 DTO、错误、session/surface 数据、LSP protocol/plugin DTO 和小型 value type | [AGENTS.md](core-types/AGENTS.md) | +| `core-types` | 共享 DTO、错误、session/surface 数据和小型 value type | [AGENTS.md](core-types/AGENTS.md) | | `events` | 事件 payload 和 emitter 契约 | [AGENTS.md](events/AGENTS.md) | | `product-domains` | 产品领域 DTO、规则、策略和窄 port | [AGENTS.md](product-domains/AGENTS.md) | | `runtime-ports` | runtime owner crate 使用的 trait 和 port | [AGENTS.md](runtime-ports/AGENTS.md) | diff --git a/src/crates/contracts/AGENTS.md b/src/crates/contracts/AGENTS.md index 93ff20c3ab..73e1d26778 100644 --- a/src/crates/contracts/AGENTS.md +++ b/src/crates/contracts/AGENTS.md @@ -10,7 +10,7 @@ implementation details upward. | Crate | Responsibility | Local doc | |---|---|---| -| `core-types` | Shared DTOs, errors, session/surface data, LSP protocol/plugin DTOs, and small value types | [AGENTS.md](core-types/AGENTS.md) | +| `core-types` | Shared DTOs, errors, session/surface data, and small value types | [AGENTS.md](core-types/AGENTS.md) | | `events` | Event payloads and emitter contracts | [AGENTS.md](events/AGENTS.md) | | `product-domains` | Product domain DTOs, rules, policies, and narrow ports | [AGENTS.md](product-domains/AGENTS.md) | | `runtime-ports` | Runtime-facing traits and ports used by owner crates | [AGENTS.md](runtime-ports/AGENTS.md) | diff --git a/src/crates/contracts/core-types/AGENTS.md b/src/crates/contracts/core-types/AGENTS.md index 2898d18d9c..91f8345526 100644 --- a/src/crates/contracts/core-types/AGENTS.md +++ b/src/crates/contracts/core-types/AGENTS.md @@ -2,8 +2,8 @@ Scope: this guide applies to `src/crates/contracts/core-types`. -`bitfun-core-types` owns low-level shared DTOs and error/session/surface/LSP -protocol contracts. Keep it dependency-light and stable for cross-crate reuse. +`bitfun-core-types` owns low-level shared DTOs and error/session/surface +contracts. Keep it dependency-light and stable for cross-crate reuse. ## Guardrails @@ -12,8 +12,6 @@ protocol contracts. Keep it dependency-light and stable for cross-crate reuse. tool-runtime implementations. - Keep additions limited to portable data shapes, serialization contracts, and small pure helpers. -- LSP protocol and plugin manifest DTOs may live here; plugin loading, - registry rules, process lifecycle, and host integration belong above. - Preserve persisted and cross-process wire compatibility. Any field rename, enum variant change, or default change must be treated as a contract change. - Product policy, runtime behavior, IO, process execution, and platform diff --git a/src/crates/contracts/core-types/src/lib.rs b/src/crates/contracts/core-types/src/lib.rs index 28b777bf09..d377b1b08c 100644 --- a/src/crates/contracts/core-types/src/lib.rs +++ b/src/crates/contracts/core-types/src/lib.rs @@ -5,7 +5,6 @@ pub mod ai; pub mod errors; -pub mod lsp; pub mod model; pub mod session; pub mod session_usage; diff --git a/src/crates/contracts/core-types/src/lsp.rs b/src/crates/contracts/core-types/src/lsp.rs deleted file mode 100644 index 9ce4dd7ec6..0000000000 --- a/src/crates/contracts/core-types/src/lsp.rs +++ /dev/null @@ -1,424 +0,0 @@ -//! Shared LSP protocol and plugin manifest DTOs. -//! -//! These are wire/data contracts only. Plugin loading, filesystem access, and -//! process lifecycle stay in service/runtime owner crates. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; - -/// LSP plugin manifest. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LspPlugin { - /// Plugin unique identifier. - pub id: String, - /// Plugin display name. - pub name: String, - /// Version. - pub version: String, - /// Author. - pub author: String, - /// Description. - pub description: String, - /// Server configuration. - pub server: ServerConfig, - /// Supported languages. - pub languages: Vec, - /// File extensions. - pub file_extensions: Vec, - /// Capability configuration. - pub capabilities: CapabilitiesConfig, - /// Default settings. - #[serde(default)] - pub settings: HashMap, - /// Checksum. - #[serde(default)] - pub checksum: String, - /// Minimum BitFun version. - #[serde(default)] - pub min_bitfun_version: String, -} - -/// Server configuration. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ServerConfig { - /// Command path (relative to the plugin directory). - pub command: String, - /// Command arguments. - #[serde(default)] - pub args: Vec, - /// Environment variables. - #[serde(default)] - pub env: HashMap, - /// Runtime type (optional: "exe", "bash", "node"). - /// Defaults to "exe"; Windows is handled automatically. - #[serde(default)] - pub runtime: Option, -} - -/// Platform token used by LSP plugin manifest command expansion. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LspPluginRuntimePlatform { - Windows, - Macos, - Linux, -} - -impl LspPluginRuntimePlatform { - pub const fn manifest_token(self) -> &'static str { - match self { - Self::Windows => "win", - Self::Macos => "darwin", - Self::Linux => "linux", - } - } -} - -/// CPU architecture token used by LSP plugin manifest command expansion. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LspPluginRuntimeArch { - X64, - Arm64, -} - -impl LspPluginRuntimeArch { - pub const fn manifest_token(self) -> &'static str { - match self { - Self::X64 => "x64", - Self::Arm64 => "arm64", - } - } -} - -/// Runtime target facts used by LSP plugin manifests. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LspPluginRuntimeTarget { - pub platform: LspPluginRuntimePlatform, - pub arch: LspPluginRuntimeArch, -} - -impl LspPluginRuntimeTarget { - pub const fn new(platform: LspPluginRuntimePlatform, arch: LspPluginRuntimeArch) -> Self { - Self { platform, arch } - } -} - -/// Resolves `${platform}`, `${os}`, and `${arch}` placeholders in an LSP plugin -/// command without touching the filesystem. -pub fn resolve_lsp_plugin_command_for_target( - command: &str, - target: LspPluginRuntimeTarget, -) -> String { - command - .replace("${platform}", target.platform.manifest_token()) - .replace("${os}", target.platform.manifest_token()) - .replace("${arch}", target.arch.manifest_token()) -} - -/// Runtime type enum. -#[derive(Debug, Clone, PartialEq)] -pub enum RuntimeType { - /// Native executable (e.g. `.exe`). - Executable, - /// Bash script. - Bash, - /// Node.js program. - Node, -} - -/// Capability configuration. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CapabilitiesConfig { - #[serde(default)] - pub completion: bool, - #[serde(default)] - pub hover: bool, - #[serde(default)] - pub definition: bool, - #[serde(default)] - pub references: bool, - #[serde(default)] - pub rename: bool, - #[serde(default)] - pub formatting: bool, - #[serde(default)] - pub diagnostics: bool, - #[serde(default)] - pub inlay_hints: bool, -} - -/// JSON-RPC message. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum JsonRpcMessage { - Request(JsonRpcRequest), - Response(JsonRpcResponse), - Notification(JsonRpcNotification), -} - -/// JSON-RPC request. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonRpcRequest { - pub jsonrpc: String, - pub id: u64, - pub method: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub params: Option, -} - -/// JSON-RPC response. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonRpcResponse { - pub jsonrpc: String, - pub id: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -/// JSON-RPC error. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonRpcError { - pub code: i32, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -/// JSON-RPC notification. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonRpcNotification { - pub jsonrpc: String, - pub method: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub params: Option, -} - -/// Initialize parameters. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InitializeParams { - /// Process ID. - pub process_id: Option, - /// Workspace root path (deprecated). - #[serde(skip_serializing_if = "Option::is_none")] - pub root_path: Option, - /// Workspace URI. - pub root_uri: Option, - /// Client capabilities. - pub capabilities: ClientCapabilities, - /// Initialization options. - #[serde(skip_serializing_if = "Option::is_none")] - pub initialization_options: Option, - /// Workspace folders. - #[serde(skip_serializing_if = "Option::is_none")] - pub workspace_folders: Option>, -} - -/// Client capabilities. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientCapabilities { - #[serde(skip_serializing_if = "Option::is_none")] - pub window: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub workspace: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub text_document: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub experimental: Option, -} - -/// Workspace folder. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WorkspaceFolder { - pub uri: String, - pub name: String, -} - -/// Initialize result. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InitializeResult { - pub capabilities: ServerCapabilities, - #[serde(skip_serializing_if = "Option::is_none")] - pub server_info: Option, -} - -/// Server capabilities. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct ServerCapabilities { - #[serde(skip_serializing_if = "Option::is_none")] - pub completion_provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hover_provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub definition_provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub references_provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub rename_provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub document_formatting_provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub text_document_sync: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub inlay_hint_provider: Option, -} - -/// Server information. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ServerInfo { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, -} - -/// Completion item. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CompletionItem { - /// Label (display text). - pub label: String, - /// Kind. - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, - /// Detail. - #[serde(skip_serializing_if = "Option::is_none")] - pub detail: Option, - /// Documentation. - #[serde(skip_serializing_if = "Option::is_none")] - pub documentation: Option, - /// Sort text. - #[serde(skip_serializing_if = "Option::is_none")] - pub sort_text: Option, - /// Filter text. - #[serde(skip_serializing_if = "Option::is_none")] - pub filter_text: Option, - /// Insert text. - #[serde(skip_serializing_if = "Option::is_none")] - pub insert_text: Option, - /// Insert text format. - #[serde(skip_serializing_if = "Option::is_none")] - pub insert_text_format: Option, -} - -/// Completion list (LSP response format). -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CompletionList { - /// Whether the list is incomplete (more completion items exist). - #[serde(skip_serializing_if = "Option::is_none")] - pub is_incomplete: Option, - /// Completion items. - pub items: Vec, -} - -/// Position. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Position { - pub line: u32, - pub character: u32, -} - -/// Range. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Range { - pub start: Position, - pub end: Position, -} - -/// Text document identifier. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TextDocumentIdentifier { - pub uri: String, -} - -/// Text document position parameters. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TextDocumentPositionParams { - pub text_document: TextDocumentIdentifier, - pub position: Position, -} - -/// Diagnostic. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Diagnostic { - pub range: Range, - #[serde(skip_serializing_if = "Option::is_none")] - pub severity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - pub message: String, -} - -/// Plugin installation source. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum PluginSource { - Marketplace, - File(PathBuf), - Url(String), -} - -/// Inlay hint. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InlayHint { - /// Position. - pub position: Position, - /// Label (display text). - pub label: InlayHintLabel, - /// Kind (1 = Type, 2 = Parameter). - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, - /// Tooltip. - #[serde(skip_serializing_if = "Option::is_none")] - pub tooltip: Option, - /// Whether to render padding before the position. - #[serde(skip_serializing_if = "Option::is_none")] - pub padding_left: Option, - /// Whether to render padding after the position. - #[serde(skip_serializing_if = "Option::is_none")] - pub padding_right: Option, - /// Text edits. - #[serde(skip_serializing_if = "Option::is_none")] - pub text_edits: Option>, -} - -/// Inlay hint label. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum InlayHintLabel { - String(String), - Parts(Vec), -} - -/// Inlay hint label part. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InlayHintLabelPart { - pub value: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub tooltip: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub location: Option, -} - -/// Location. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Location { - pub uri: String, - pub range: Range, -} - -/// Text edit. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TextEdit { - pub range: Range, - #[serde(rename = "newText")] - pub new_text: String, -} diff --git a/src/crates/contracts/core-types/tests/core_type_contracts.rs b/src/crates/contracts/core-types/tests/core_type_contracts.rs index 9cc3a95460..3b5266bd91 100644 --- a/src/crates/contracts/core-types/tests/core_type_contracts.rs +++ b/src/crates/contracts/core-types/tests/core_type_contracts.rs @@ -1,5 +1,3 @@ -#[path = "core_type_contracts/lsp_contracts.rs"] -mod lsp_contracts; #[path = "core_type_contracts/session_contracts.rs"] mod session_contracts; #[path = "core_type_contracts/session_usage_contracts.rs"] diff --git a/src/crates/contracts/core-types/tests/core_type_contracts/lsp_contracts.rs b/src/crates/contracts/core-types/tests/core_type_contracts/lsp_contracts.rs deleted file mode 100644 index 0a27e5e88e..0000000000 --- a/src/crates/contracts/core-types/tests/core_type_contracts/lsp_contracts.rs +++ /dev/null @@ -1,62 +0,0 @@ -use bitfun_core_types::lsp::{ - resolve_lsp_plugin_command_for_target, CapabilitiesConfig, LspPlugin, LspPluginRuntimeArch, - LspPluginRuntimePlatform, LspPluginRuntimeTarget, -}; - -#[test] -fn lsp_plugin_manifest_defaults_preserve_legacy_shape() { - let plugin: LspPlugin = serde_json::from_value(serde_json::json!({ - "id": "rust-analyzer", - "name": "Rust Analyzer", - "version": "1.0.0", - "author": "BitFun", - "description": "Rust language support", - "server": { - "command": "bin/${platform}/${arch}/rust-analyzer", - "args": ["--stdio"] - }, - "languages": ["rust"], - "file_extensions": [".rs"], - "capabilities": { - "completion": true, - "definition": true - } - })) - .expect("legacy manifest should parse"); - - assert_eq!(plugin.server.env.len(), 0); - assert_eq!(plugin.server.runtime, None); - assert_eq!(plugin.settings.len(), 0); - assert_eq!(plugin.checksum, ""); - assert_eq!(plugin.min_bitfun_version, ""); - assert!(plugin.capabilities.completion); - assert!(plugin.capabilities.definition); - assert!(!plugin.capabilities.hover); -} - -#[test] -fn lsp_capability_config_missing_fields_default_to_false() { - let config: CapabilitiesConfig = - serde_json::from_value(serde_json::json!({})).expect("empty capabilities should parse"); - - assert!(!config.completion); - assert!(!config.hover); - assert!(!config.definition); - assert!(!config.references); - assert!(!config.rename); - assert!(!config.formatting); - assert!(!config.diagnostics); - assert!(!config.inlay_hints); -} - -#[test] -fn lsp_plugin_command_placeholder_resolution_is_contract_owned() { - let command = "bin/${platform}/${os}/${arch}/server"; - let target = - LspPluginRuntimeTarget::new(LspPluginRuntimePlatform::Macos, LspPluginRuntimeArch::Arm64); - - assert_eq!( - resolve_lsp_plugin_command_for_target(command, target), - "bin/darwin/darwin/arm64/server" - ); -} diff --git a/src/crates/contracts/events/src/emitter.rs b/src/crates/contracts/events/src/emitter.rs index fe7ee642cf..68d0a22f03 100644 --- a/src/crates/contracts/events/src/emitter.rs +++ b/src/crates/contracts/events/src/emitter.rs @@ -12,22 +12,6 @@ pub trait EventEmitter: Send + Sync { /// Send generic events async fn emit(&self, event_name: &str, payload: serde_json::Value) -> anyhow::Result<()>; - /// Send LSP events - async fn emit_lsp( - &self, - workspace_path: &str, - event_data: serde_json::Value, - ) -> anyhow::Result<()> { - self.emit( - "lsp-event", - serde_json::json!({ - "workspace_path": workspace_path, - "event_data": event_data - }), - ) - .await - } - /// Send Profile events async fn emit_profile( &self, diff --git a/src/crates/interfaces/acp/AGENTS-CN.md b/src/crates/interfaces/acp/AGENTS-CN.md index 51392ff76f..8985364a7a 100644 --- a/src/crates/interfaces/acp/AGENTS-CN.md +++ b/src/crates/interfaces/acp/AGENTS-CN.md @@ -8,7 +8,7 @@ 本 crate 暴露两个可加性的角色 feature。`client` 负责 ACP 进程发现、配置、远程探测、会话传输和 tool-card 投影,选择 Core Agent Runtime 与具体 SSH 能力;`server` 负责 CLI 托管的 ACP 服务端, -通过 `DeliveryProfile::Acp` 投影 runtime,并精确选择该路径使用的工具、文档、订阅、LSP 与外部来源 owner, +通过 `DeliveryProfile::Acp` 投影 runtime,并精确选择该路径使用的工具、文档、订阅与外部来源 owner, 但不选择 SSH。兼容默认值必须精确等于 `client + server`;Desktop 只选择 `client`,CLI 选择两者。 两个角色保持加法语义,不得用 `product-full` 替代任一闭包。 diff --git a/src/crates/interfaces/acp/AGENTS.md b/src/crates/interfaces/acp/AGENTS.md index a695abeb4b..9b755817f6 100644 --- a/src/crates/interfaces/acp/AGENTS.md +++ b/src/crates/interfaces/acp/AGENTS.md @@ -12,7 +12,7 @@ The crate exposes two additive roles. `client` owns ACP process discovery, configuration, remote probing, session transport, and tool-card projection; it selects the Core Agent Runtime plus concrete SSH support. `server` owns the CLI-hosted ACP server and runtime projection through `DeliveryProfile::Acp`; it -selects the exact Core tool, document, subscription, LSP, and external-source +selects the exact Core tool, document, subscription, and external-source owners used by that path, but does not select SSH. The compatibility default is exactly `client + server`. Desktop selects only `client`; CLI selects both. Keep these role features additive and do not replace either closure with diff --git a/src/crates/interfaces/acp/Cargo.toml b/src/crates/interfaces/acp/Cargo.toml index 254beb1715..1c16534f54 100644 --- a/src/crates/interfaces/acp/Cargo.toml +++ b/src/crates/interfaces/acp/Cargo.toml @@ -29,7 +29,6 @@ server = [ "bitfun-core/document-read", "bitfun-core/subscription-auth", "bitfun-core/deep-research", - "bitfun-core/lsp", "bitfun-core/external-sources", "bitfun-core/tools-basic", "bitfun-core/tools-git", diff --git a/src/crates/services/AGENTS-CN.md b/src/crates/services/AGENTS-CN.md index 0d87f31c36..17ca1633d9 100644 --- a/src/crates/services/AGENTS-CN.md +++ b/src/crates/services/AGENTS-CN.md @@ -2,13 +2,13 @@ # 服务实现层 -本层负责接触本地系统或 runtime infrastructure 的可复用具体实现:filesystem、git、file watch、terminal、MCP、LSP plugin registry、remote connectivity、process lifecycle、session persistence primitives、MiniApp runtime/import IO 以及类似 OS/network 能力。 +本层负责接触本地系统或 runtime infrastructure 的可复用具体实现:filesystem、git、file watch、terminal、MCP、remote connectivity、process lifecycle、session persistence primitives、MiniApp runtime/import IO 以及类似 OS/network 能力。 ## 模块 | Crate | 职责 | 本地文档 | |---|---|---| -| `services-core` | 不包含产品组装决策的本地 service primitive,包括 LSP plugin registry、session storage、metadata store CRUD/index rebuild、metadata 构造/计数/索引/字段 mutation、lineage 规则和 JSON file IO | [AGENTS.md](services-core/AGENTS.md) | +| `services-core` | 不包含产品组装决策的本地 service primitive,包括 session storage、metadata store CRUD/index rebuild、metadata 构造/计数/索引/字段 mutation、lineage 规则和 JSON file IO | [AGENTS.md](services-core/AGENTS.md) | | `services-integrations` | MCP、git、remote、file watch、MiniApp runtime、产品领域 port 具体实现,以及平台无关的 Remote Connect primitives | [AGENTS.md](services-integrations/AGENTS.md) | | `relay-service` | standalone 与 embedded 宿主共享的 Remote Connect relay 状态、存储及 HTTP/WebSocket 路由 | [AGENTS.md](relay-service/AGENTS.md) | | `page-function-runtime` | BitFun Pages 嵌入式 JS Page Function runtime(rquickjs) | [AGENTS.md](page-function-runtime/AGENTS.md) | @@ -16,7 +16,7 @@ ## 放置规则 -- 具体 OS、process、filesystem、git、terminal、MCP、LSP registry、remote SSH、file watch、session persistence primitives、MiniApp runtime IO 和 network service 实现放在这里。 +- 具体 OS、process、filesystem、git、terminal、MCP、remote SSH、file watch、session persistence primitives、MiniApp runtime IO 和 network service 实现放在这里。 - 需要具体依赖的 `contracts`、`execution` 或 `contracts/product-domains` port 实现在这里。 - 协议/transport projection 放在 `adapters`,产品能力选择放在 `assembly`。 diff --git a/src/crates/services/AGENTS.md b/src/crates/services/AGENTS.md index 3962917229..a161899b77 100644 --- a/src/crates/services/AGENTS.md +++ b/src/crates/services/AGENTS.md @@ -3,15 +3,15 @@ # Service Layer This layer owns reusable concrete implementations that touch local systems or -runtime infrastructure: filesystem, git, file watch, terminal, MCP, LSP plugin -registry, remote connectivity, process lifecycle, session persistence primitives, MiniApp concrete runtime IO, and similar +runtime infrastructure: filesystem, git, file watch, terminal, MCP, remote +connectivity, process lifecycle, session persistence primitives, MiniApp concrete runtime IO, and similar OS/network capabilities. ## Modules | Crate | Responsibility | Local doc | |---|---|---| -| `services-core` | Reusable local service primitives, managed process-tree lifecycle, filesystem helpers, LSP plugin registry rules, session storage layout/indexing/deletion, metadata store CRUD/index rebuild, metadata construction/counter/index/field mutation/lineage rules, and JSON file IO without product assembly decisions | [AGENTS.md](services-core/AGENTS.md) | +| `services-core` | Reusable local service primitives, managed process-tree lifecycle, filesystem helpers, session storage layout/indexing/deletion, metadata store CRUD/index rebuild, metadata construction/counter/index/field mutation/lineage rules, and JSON file IO without product assembly decisions | [AGENTS.md](services-core/AGENTS.md) | | `services-integrations` | Concrete MCP, git, remote, file-watch, MiniApp runtime, review-platform provider service, product-domain port implementations, and platform-neutral Remote Connect primitives | [AGENTS.md](services-integrations/AGENTS.md) | | `miniapp-market-service` | Concrete SQLite, artifact storage, GitHub OAuth, package validation, and HTTP behavior for the MiniApp market | [README.md](miniapp-market-service/README.md) | | `skin-market-service` | Concrete SQLite, artifact storage, Appearance package validation, review, and HTTP behavior for the Skin market | [README.md](skin-market-service/README.md) | @@ -21,7 +21,7 @@ OS/network capabilities. ## Placement Rules -- Put concrete OS, process, filesystem, git, terminal, MCP, LSP registry, remote SSH, +- Put concrete OS, process, filesystem, git, terminal, MCP, remote SSH, file-watch, MiniApp runtime IO, and network service implementations here. - Implement `contracts`, `execution`, or `contracts/product-domains` ports here when the implementation needs concrete dependencies. diff --git a/src/crates/services/services-core/AGENTS.md b/src/crates/services/services-core/AGENTS.md index a7859dda3b..13d0f39129 100644 --- a/src/crates/services/services-core/AGENTS.md +++ b/src/crates/services/services-core/AGENTS.md @@ -4,8 +4,8 @@ Scope: this guide applies to `src/crates/services/services-core`. `bitfun-services-core` owns cross-platform service DTOs and helpers that compile without the full product runtime. This includes generic filesystem/search/JSON -IO helpers, bounded local Instruction file reads, LSP package/protocol/watch/process primitives, session metadata -storage helpers, and local OS action primitives such as command lookup, +IO helpers, bounded local Instruction file reads, session metadata storage +helpers, and local OS action primitives such as command lookup, clipboard, file/url opening, script execution, workspace runtime FS/shell providers, managed process-tree lifecycle, process-level Agent Runtime ownership locks, and system facts. Product crates may layer routing, policy, capability selection, event emission, or legacy error mapping outside this @@ -24,20 +24,16 @@ crate. (JSON/session/usage persistence), `process-runtime` (command lookup and supervised child lifecycle), and `workspace-instructions` (declarative instruction discovery). Consumers enable those or the narrower - `lsp`, `workspace-runtime`, `workspace-identity`, `runtime-ownership`, + `workspace-runtime`, `workspace-identity`, `runtime-ownership`, `permission`, `dispatch-workspace`, `markdown`, `session-git`, and `workspace-text-runtime` extensions only for behavior they use. Products needing IANA time-zone ranges and dashboard aggregation additionally select `token-usage-statistics`. In particular, session metadata consumers must not compile libgit2 unless they use the memory-workspace baseline/diff API. Keep Tokio and platform API capabilities owner-scoped too: the empty profile - carries no Tokio dependency, `lsp` and `workspace-runtime` - explicitly compose `process-runtime`, and Windows storage/process bindings + carries no Tokio dependency, `workspace-runtime` explicitly composes + `process-runtime`, and Windows storage/process bindings must not be enabled from one shared dependency feature union. -- LSP manifest and protocol DTOs belong in `bitfun-core-types`; reusable LSP - package, protocol, detection, debounce, watch, and process-manager helpers - belong in `services-core`; product workspace state, event emission, global - singletons, and file-sync orchestration stay outside this crate. - Runtime call sites that touch agent execution, scheduler state, workspace managers, filesystem orchestration, or product behavior stay outside this crate. `workspace-runtime` may implement local `bitfun-runtime-ports` diff --git a/src/crates/services/services-core/Cargo.toml b/src/crates/services/services-core/Cargo.toml index 4f8cfe05f3..ba6f95a9e9 100644 --- a/src/crates/services/services-core/Cargo.toml +++ b/src/crates/services/services-core/Cargo.toml @@ -26,11 +26,9 @@ chrono = { workspace = true, optional = true } chrono-tz = { workspace = true, optional = true } git2 = { workspace = true, optional = true } dunce = { workspace = true, optional = true } -zip = { workspace = true, optional = true } thiserror = { workspace = true } log = { workspace = true } fs2 = { workspace = true, optional = true } -notify = { workspace = true, optional = true } ignore = { workspace = true, optional = true } globset = { workspace = true, optional = true } sha2 = { workspace = true, optional = true } @@ -117,16 +115,6 @@ workspace-instructions = [ "tokio/io-util", "tokio/rt", ] -lsp = [ - "dep:anyhow", - "dep:bitfun-core-types", - "dep:notify", - "dep:zip", - "process-runtime", - "tokio/fs", - "tokio/io-util", - "tokio/sync", -] markdown = ["dep:regex", "dep:serde_yaml"] workspace-runtime = [ "dep:anyhow", @@ -175,11 +163,6 @@ name = "declarative_workspace_instruction_contracts" path = "tests/declarative_workspace_instruction_contracts.rs" required-features = ["workspace-instructions"] -[[test]] -name = "lsp_plugin_registry_contracts" -path = "tests/lsp_plugin_registry_contracts.rs" -required-features = ["lsp"] - [[test]] name = "runtime_ownership_contracts" path = "tests/runtime_ownership_contracts.rs" diff --git a/src/crates/services/services-core/src/lib.rs b/src/crates/services/services-core/src/lib.rs index b24dfd0bec..d388b83fa0 100644 --- a/src/crates/services/services-core/src/lib.rs +++ b/src/crates/services/services-core/src/lib.rs @@ -23,8 +23,6 @@ pub mod jsonc; pub mod local_instructions; #[cfg(feature = "workspace-runtime")] pub mod local_runtime_ports; -#[cfg(feature = "lsp")] -pub mod lsp; #[cfg(feature = "process-runtime")] pub mod managed_runtime; #[cfg(feature = "markdown")] diff --git a/src/crates/services/services-core/src/lsp.rs b/src/crates/services/services-core/src/lsp.rs deleted file mode 100644 index 4123d43e20..0000000000 --- a/src/crates/services/services-core/src/lsp.rs +++ /dev/null @@ -1,262 +0,0 @@ -//! Cross-platform reusable LSP service rules. -//! -//! This module owns the reusable plugin registry, -//! plugin package filesystem loading, protocol helpers, project -//! detection, request debounce, configuration file watching, and LSP server -//! process/manager primitives. It does not own product workspace state, -//! frontend event emission, or global singleton wiring. - -pub mod config_watcher; -pub mod debouncer; -pub mod manager; -pub mod plugin_loader; -pub mod process; -pub mod project_detector; -pub mod protocol; - -use bitfun_core_types::lsp::LspPlugin; -pub use bitfun_core_types::lsp::{ - resolve_lsp_plugin_command_for_target as resolve_plugin_command_for_target, - LspPluginRuntimeArch, LspPluginRuntimePlatform, LspPluginRuntimeTarget, -}; -use log::{info, warn}; -use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; -use thiserror::Error; - -pub type LspPluginRegistryResult = Result; - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum LspPluginRegistryError { - #[error("Plugin already registered: {0}")] - AlreadyRegistered(String), - #[error("Plugin not found: {0}")] - NotFound(String), - #[error("Unsupported platform")] - UnsupportedPlatform, - #[error("Unsupported architecture")] - UnsupportedArchitecture, -} - -/// Returns the current runtime target supported by LSP plugin manifests. -pub fn current_lsp_plugin_runtime_target() -> LspPluginRegistryResult { - let platform = if cfg!(target_os = "windows") { - LspPluginRuntimePlatform::Windows - } else if cfg!(target_os = "macos") { - LspPluginRuntimePlatform::Macos - } else if cfg!(target_os = "linux") { - LspPluginRuntimePlatform::Linux - } else { - return Err(LspPluginRegistryError::UnsupportedPlatform); - }; - - let arch = if cfg!(target_arch = "x86_64") { - LspPluginRuntimeArch::X64 - } else if cfg!(target_arch = "aarch64") { - LspPluginRuntimeArch::Arm64 - } else { - return Err(LspPluginRegistryError::UnsupportedArchitecture); - }; - - Ok(LspPluginRuntimeTarget::new(platform, arch)) -} - -/// Resolves plugin command placeholders for the current runtime target. -pub fn resolve_plugin_command_for_current_target(command: &str) -> LspPluginRegistryResult { - Ok(resolve_plugin_command_for_target( - command, - current_lsp_plugin_runtime_target()?, - )) -} - -/// Supported extension summary used by product surfaces. -pub struct LspSupportedExtensions { - pub extension_to_language: HashMap, - pub supported_languages: Vec, -} - -/// Plugin registry. -pub struct PluginRegistry { - /// Registered plugins (`plugin_id -> plugin`). - plugins: HashMap, - /// Language-to-plugin mapping (`language -> plugin_id`). - language_map: HashMap, - /// File-extension-to-plugin mapping (`extension -> plugin_id`). - extension_map: HashMap, -} - -impl PluginRegistry { - /// Creates a new plugin registry. - pub fn new() -> Self { - Self { - plugins: HashMap::new(), - language_map: HashMap::new(), - extension_map: HashMap::new(), - } - } - - /// Registers a plugin. - pub fn register(&mut self, plugin: LspPlugin) -> LspPluginRegistryResult<()> { - let plugin_id = plugin.id.clone(); - - if self.plugins.contains_key(&plugin_id) { - return Err(LspPluginRegistryError::AlreadyRegistered(plugin_id)); - } - - for language in &plugin.languages { - if let Some(existing) = self.language_map.get(language) { - warn!( - "Language '{}' already mapped to plugin '{}', overwriting with '{}'", - language, existing, plugin_id - ); - } - self.language_map - .insert(language.clone(), plugin_id.clone()); - } - - for ext in &plugin.file_extensions { - if let Some(existing) = self.extension_map.get(ext) { - warn!( - "Extension '{}' already mapped to plugin '{}', overwriting with '{}'", - ext, existing, plugin_id - ); - } - self.extension_map.insert(ext.clone(), plugin_id.clone()); - } - - self.plugins.insert(plugin_id.clone(), plugin); - - info!( - "Plugin registered: {} with {} language(s) and {} extension(s)", - plugin_id, - self.language_map - .values() - .filter(|v| *v == &plugin_id) - .count(), - self.extension_map - .values() - .filter(|v| *v == &plugin_id) - .count() - ); - - Ok(()) - } - - /// Unregisters a plugin. - pub fn unregister(&mut self, plugin_id: &str) -> LspPluginRegistryResult<()> { - let plugin = self - .plugins - .remove(plugin_id) - .ok_or_else(|| LspPluginRegistryError::NotFound(plugin_id.to_string()))?; - - for language in &plugin.languages { - remove_index_if_owned(&mut self.language_map, language, plugin_id); - } - - for ext in &plugin.file_extensions { - remove_index_if_owned(&mut self.extension_map, ext, plugin_id); - } - - info!("Plugin unregistered: {}", plugin_id); - - Ok(()) - } - - /// Gets a plugin by plugin ID. - pub fn get_plugin(&self, plugin_id: &str) -> Option<&LspPlugin> { - self.plugins.get(plugin_id) - } - - /// Finds a plugin by language ID. - pub fn find_by_language(&self, language: &str) -> Option<&LspPlugin> { - self.language_map - .get(language) - .and_then(|id| self.plugins.get(id)) - } - - /// Finds a plugin by file extension. - pub fn find_by_extension(&self, extension: &str) -> Option<&LspPlugin> { - let ext = if extension.starts_with('.') { - extension.to_string() - } else { - format!(".{}", extension) - }; - - self.extension_map - .get(&ext) - .and_then(|id| self.plugins.get(id)) - } - - /// Finds a plugin by file path. - pub fn find_by_file_path(&self, file_path: &str) -> Option<&LspPlugin> { - let path = PathBuf::from(file_path); - if let Some(extension) = path.extension() { - if let Some(ext_str) = extension.to_str() { - return self.find_by_extension(ext_str); - } - } - None - } - - /// Lists all registered plugins. - pub fn list_all(&self) -> Vec<&LspPlugin> { - self.plugins.values().collect() - } - - /// Lists all plugins that support a specific language. - pub fn list_by_language(&self, language: &str) -> Vec<&LspPlugin> { - self.plugins - .values() - .filter(|p| p.languages.iter().any(|candidate| candidate == language)) - .collect() - } - - /// Returns surface-facing supported extension facts. - pub fn supported_extensions(&self) -> LspSupportedExtensions { - let mut extension_to_language = HashMap::new(); - let mut supported_languages = HashSet::new(); - - for plugin in self.plugins.values() { - for language in &plugin.languages { - supported_languages.insert(language.clone()); - } - - for ext in &plugin.file_extensions { - if let Some(language) = plugin.languages.first() { - extension_to_language.insert(ext.clone(), language.clone()); - } - } - } - - LspSupportedExtensions { - extension_to_language, - supported_languages: supported_languages.into_iter().collect(), - } - } - - /// Returns whether a plugin is registered. - pub fn is_registered(&self, plugin_id: &str) -> bool { - self.plugins.contains_key(plugin_id) - } - - /// Returns the number of registered plugins. - pub fn count(&self) -> usize { - self.plugins.len() - } -} - -impl Default for PluginRegistry { - fn default() -> Self { - Self::new() - } -} - -fn remove_index_if_owned(index: &mut HashMap, key: &str, plugin_id: &str) { - if index - .get(key) - .map(|current_plugin_id| current_plugin_id == plugin_id) - .unwrap_or(false) - { - index.remove(key); - } -} diff --git a/src/crates/services/services-core/src/lsp/config_watcher.rs b/src/crates/services/services-core/src/lsp/config_watcher.rs deleted file mode 100644 index 04ae4c8c76..0000000000 --- a/src/crates/services/services-core/src/lsp/config_watcher.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Configuration file watcher -//! -//! Features: -//! - Watches configuration file changes (tsconfig.json, package.json, etc.) -//! - Automatically restarts the corresponding LSP server when config changes - -use anyhow::Result; -use log::{debug, info, warn}; -use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use tokio::sync::mpsc; - -/// Configuration file watcher. -pub struct ConfigWatcher { - workspace_path: PathBuf, - _watcher: RecommendedWatcher, // Keep the watcher alive (prevent it from being dropped) -} - -impl ConfigWatcher { - /// Creates a configuration file watcher. - pub fn new( - workspace_path: PathBuf, - on_config_changed: Arc, - ) -> Result { - info!( - "Setting up config file watcher for workspace: {:?}", - workspace_path - ); - - let (tx, mut rx) = mpsc::channel(100); - - let mut watcher = RecommendedWatcher::new( - move |res: Result| { - if let Ok(event) = res { - let _ = tx.blocking_send(event); - } - }, - Config::default(), - )?; - - let config_files = vec![ - "tsconfig.json", - "package.json", - "Cargo.toml", - ".eslintrc.json", - ".eslintrc.js", - "pyproject.toml", - "setup.py", - "go.mod", - "pom.xml", - "build.gradle", - "CMakeLists.txt", - ]; - - for file_name in config_files { - let file_path = workspace_path.join(file_name); - if file_path.exists() { - if let Err(e) = watcher.watch(&file_path, RecursiveMode::NonRecursive) { - warn!("Failed to watch config file {}: {}", file_name, e); - } - } - } - - let workspace_path_clone = workspace_path.clone(); - tokio::spawn(async move { - while let Some(event) = rx.recv().await { - Self::handle_file_event(event, &workspace_path_clone, &on_config_changed); - } - }); - - info!("Config file watcher started"); - - Ok(Self { - workspace_path, - _watcher: watcher, - }) - } - - /// Handles file change events. - fn handle_file_event( - event: Event, - _workspace_path: &Path, - on_config_changed: &Arc, - ) { - if !matches!(event.kind, EventKind::Modify(_)) { - return; - } - - for path in event.paths { - if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { - let language = Self::config_file_to_language(file_name); - - if let Some(lang) = language { - info!( - "Config file changed: {}, restarting {} server", - file_name, lang - ); - on_config_changed(lang.to_string(), file_name.to_string()); - } - } - } - } - - /// Infers a language from a configuration filename. - fn config_file_to_language(file_name: &str) -> Option<&'static str> { - match file_name { - "tsconfig.json" | "package.json" => Some("typescript"), - "Cargo.toml" => Some("rust"), - "pyproject.toml" | "setup.py" => Some("python"), - "go.mod" => Some("go"), - "pom.xml" | "build.gradle" => Some("java"), - "CMakeLists.txt" => Some("cpp"), - ".eslintrc.json" | ".eslintrc.js" => Some("javascript"), - _ => None, - } - } -} - -impl Drop for ConfigWatcher { - fn drop(&mut self) { - debug!( - "ConfigWatcher dropped for workspace: {:?}", - self.workspace_path - ); - } -} diff --git a/src/crates/services/services-core/src/lsp/debouncer.rs b/src/crates/services/services-core/src/lsp/debouncer.rs deleted file mode 100644 index d7dbd04035..0000000000 --- a/src/crates/services/services-core/src/lsp/debouncer.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! LSP request debouncer -//! -//! Prevents sending a burst of duplicate requests in a short time, improving performance and -//! stability. - -use log::debug; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::RwLock; - -/// Request debouncer. -pub struct RequestDebouncer { - /// Last request time (`uri + method -> last_time`). - last_requests: Arc>>, - /// Debounce delay (milliseconds). - debounce_ms: u64, -} - -impl RequestDebouncer { - /// Creates a new debouncer. - pub fn new(debounce_ms: u64) -> Self { - Self { - last_requests: Arc::new(RwLock::new(HashMap::new())), - debounce_ms, - } - } - - /// Returns whether a request should be sent. - /// Returns `true` if it can be sent, or `false` if it should be skipped (too frequent). - pub async fn should_send(&self, uri: &str, method: &str) -> bool { - let key = format!("{}:{}", uri, method); - let now = Instant::now(); - - let mut requests = self.last_requests.write().await; - - if let Some(last_time) = requests.get(&key) { - let elapsed = now.duration_since(*last_time); - if elapsed < Duration::from_millis(self.debounce_ms) { - debug!( - "Request debounced: {} ({}ms elapsed)", - key, - elapsed.as_millis() - ); - return false; - } - } - - requests.insert(key, now); - true - } - - /// Cleans up expired records (call periodically). - pub async fn cleanup(&self, max_age: Duration) { - let mut requests = self.last_requests.write().await; - let now = Instant::now(); - - requests.retain(|_, last_time| now.duration_since(*last_time) < max_age); - } -} - -#[cfg(test)] -mod tests { - use super::RequestDebouncer; - - #[tokio::test] - async fn rejects_duplicate_request_inside_debounce_window() { - let debouncer = RequestDebouncer::new(1_000); - - assert!( - debouncer - .should_send("file:///a.rs", "textDocument/hover") - .await - ); - assert!( - !debouncer - .should_send("file:///a.rs", "textDocument/hover") - .await - ); - assert!( - debouncer - .should_send("file:///a.rs", "textDocument/definition") - .await - ); - } -} diff --git a/src/crates/services/services-core/src/lsp/manager.rs b/src/crates/services/services-core/src/lsp/manager.rs deleted file mode 100644 index 7f7618b2b6..0000000000 --- a/src/crates/services/services-core/src/lsp/manager.rs +++ /dev/null @@ -1,751 +0,0 @@ -//! LSP protocol-layer manager - -use anyhow::{anyhow, Result}; -use log::{debug, error, info, warn}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::{Mutex, RwLock}; - -use crate::lsp::plugin_loader::PluginLoader; -use crate::lsp::process::{ - CrashCallback, DiagnosticsCallback, LspServerProcess, ProgressCallback, TokenCreateCallback, -}; -use crate::lsp::{LspSupportedExtensions, PluginRegistry}; -use bitfun_core_types::lsp::{CompletionItem, CompletionList, InlayHint, LspPlugin}; - -/// LSP protocol-layer manager (stateless, pure protocol implementation). -pub struct LspManager { - /// Plugin loader. - plugin_loader: PluginLoader, - /// Plugin registry. - registry: Arc>, - /// Running LSP server processes (`language -> process`). - processes: Arc>>>, - /// Per-language start/stop guards prevent duplicate process spawns. - lifecycle_locks: Arc>>>>, - /// Diagnostics cache (`uri -> diagnostics`). - /// - /// Values are shared via `Arc` so callers (cache + emitted events) can - /// reference the same allocation without deep-cloning diagnostic arrays. - diagnostics_cache: Arc>>>>, -} - -impl LspManager { - /// Creates a new LSP manager. - pub fn new(plugins_dir: PathBuf) -> Self { - Self { - plugin_loader: PluginLoader::new(plugins_dir), - registry: Arc::new(RwLock::new(PluginRegistry::new())), - processes: Arc::new(RwLock::new(HashMap::new())), - lifecycle_locks: Arc::new(Mutex::new(HashMap::new())), - diagnostics_cache: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Initializes the manager (loads installed plugins). - pub async fn initialize(&self) -> Result<()> { - info!("Initializing LSP Manager"); - - if let Err(e) = self.plugin_loader.cleanup_temp_dirs().await { - warn!("Failed to cleanup temp directories: {}", e); - } - - let plugins = self.plugin_loader.load_all_plugins().await?; - - for plugin in plugins { - if let Err(e) = self.register_plugin_internal(plugin).await { - error!("Failed to register plugin: {}", e); - } - } - - let count = { - let registry = self.registry.read().await; - registry.count() - }; - - info!("LSP Manager initialized with {} plugin(s)", count); - - Ok(()) - } - - // Note: workspace root path management has been moved to WorkspaceLspManager. - // LspManager is responsible for protocol-layer operations only. - - /// Registers a plugin (internal). - async fn register_plugin_internal(&self, plugin: LspPlugin) -> Result<()> { - let mut registry = self.registry.write().await; - registry.register(plugin)?; - Ok(()) - } - - /// Installs a plugin. - pub async fn install_plugin(&self, package_path: PathBuf) -> Result { - info!("Installing plugin from: {:?}", package_path); - - let plugin_id = self - .plugin_loader - .install_plugin_package(&package_path) - .await?; - - let plugin = self.plugin_loader.load_plugin(&plugin_id).await?; - - { - let mut registry = self.registry.write().await; - registry.register(plugin)?; - } - - info!("Plugin installed and registered: {}", plugin_id); - - Ok(plugin_id) - } - - /// Uninstalls a plugin. - pub async fn uninstall_plugin(&self, plugin_id: &str) -> Result<()> { - info!("Uninstalling plugin: {}", plugin_id); - - if let Err(e) = self.stop_server(plugin_id).await { - warn!("Failed to stop server for {}: {}", plugin_id, e); - } - - { - let mut registry = self.registry.write().await; - registry.unregister(plugin_id)?; - } - - self.plugin_loader.uninstall_plugin(plugin_id).await?; - - info!("Plugin uninstalled: {}", plugin_id); - - Ok(()) - } - - /// Starts an LSP server. - /// workspace_root: Workspace root path, provided by the caller (WorkspaceLspManager). - /// crash_callback: Callback invoked when the process crashes. - /// progress_callback: Indexing progress callback. - /// token_create_callback: Token creation callback. - /// diagnostics_callback: Diagnostics callback. - pub async fn start_server( - &self, - language: &str, - workspace_root: Option, - crash_callback: Option, - progress_callback: Option, - token_create_callback: Option, - diagnostics_callback: Option, - ) -> Result<()> { - let plugin = { - let registry = self.registry.read().await; - match registry.find_by_language(language).cloned() { - Some(plugin) => plugin, - None => { - let err = anyhow!("No LSP plugin found for language: {}", language); - warn!("{} (this is expected for plaintext)", err); - return Err(err); - } - } - }; - - let plugin_id = plugin.id.clone(); - let lifecycle_lock = self.lifecycle_lock_for(language).await; - let _lifecycle_guard = lifecycle_lock.lock().await; - - { - let processes = self.processes.read().await; - if processes.contains_key(language) { - return Ok(()); - } - } - - let server_path = self.plugin_loader.get_server_path(&plugin).map_err(|e| { - error!("Failed to get server path: {}", e); - e - })?; - - let process = LspServerProcess::spawn( - plugin_id.clone(), - server_path.clone(), - &plugin.server, - crash_callback, - progress_callback, - token_create_callback, - diagnostics_callback, - ) - .await - .map_err(|e| { - error!("Failed to spawn process: {}", e); - e - })?; - - let root_uri = workspace_root.and_then(|p| p.to_str().map(|s| s.to_string())); - - process.initialize(root_uri.clone()).await.map_err(|e| { - error!("Failed to initialize LSP connection: {}", e); - e - })?; - - { - let mut processes = self.processes.write().await; - processes.insert(language.to_string(), Arc::new(process)); - } - - info!("LSP server started successfully: {}", language); - Ok(()) - } - - /// Stops an LSP server. - pub async fn stop_server(&self, language: &str) -> Result<()> { - debug!("Stopping LSP server: {}", language); - let lifecycle_lock = self.lifecycle_lock_for(language).await; - let _lifecycle_guard = lifecycle_lock.lock().await; - - let process = { - let mut processes = self.processes.write().await; - processes.remove(language) - }; - - if let Some(process) = process { - if let Err(e) = process.shutdown().await { - warn!("Failed to shutdown server {}: {}", language, e); - } - } - - info!("LSP server stopped: {}", language); - Ok(()) - } - - async fn lifecycle_lock_for(&self, language: &str) -> Arc> { - let mut lifecycle_locks = self.lifecycle_locks.lock().await; - lifecycle_locks - .entry(language.to_string()) - .or_insert_with(|| Arc::new(Mutex::new(()))) - .clone() - } - - /// Returns whether the server is running. - pub async fn is_server_running(&self, language: &str) -> bool { - let processes = self.processes.read().await; - processes.contains_key(language) - } - - /// Returns whether the server process is alive. - pub async fn is_server_alive(&self, language: &str) -> bool { - let process = { - let processes = self.processes.read().await; - processes.get(language).cloned() - }; - - match process { - Some(process) => process.is_alive().await, - None => false, - } - } - - /// Gets the server process (internal use). - async fn get_process(&self, language: &str) -> Result> { - let processes = self.processes.read().await; - processes - .get(language) - .cloned() - .ok_or_else(|| anyhow!("LSP server not running for: {}", language)) - } - - /// Lists all installed plugins. - pub async fn list_plugins(&self) -> Vec { - let registry = self.registry.read().await; - registry.list_all().into_iter().cloned().collect() - } - - /// Gets plugin information. - pub async fn get_plugin(&self, plugin_id: &str) -> Option { - let registry = self.registry.read().await; - registry.get_plugin(plugin_id).cloned() - } - - /// Finds a plugin by language. - pub async fn find_plugin_by_language(&self, language: &str) -> Option { - let registry = self.registry.read().await; - registry.find_by_language(language).cloned() - } - - /// Finds a plugin by file path. - pub async fn find_plugin_by_file(&self, file_path: &str) -> Option { - let registry = self.registry.read().await; - registry.find_by_file_path(file_path).cloned() - } - - /// Returns surface-facing supported extension facts. - pub async fn supported_extensions(&self) -> LspSupportedExtensions { - let registry = self.registry.read().await; - registry.supported_extensions() - } - - /// Shuts down all servers. - pub async fn shutdown(&self) -> Result<()> { - info!("Shutting down all LSP servers"); - - let plugin_ids: Vec = { - let processes = self.processes.read().await; - processes.keys().cloned().collect() - }; - - for plugin_id in plugin_ids { - if let Err(e) = self.stop_server(&plugin_id).await { - error!("Failed to stop server {}: {}", plugin_id, e); - } - } - - info!("All LSP servers stopped"); - - Ok(()) - } - - /// Shuts down all servers (alias). - pub async fn stop_all_servers(&self) -> Result<()> { - self.shutdown().await - } - - /// Document open notification (protocol-only; does not include startup logic). - pub async fn did_open(&self, language: &str, uri: &str, text: &str) -> Result<()> { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri, - "languageId": language, - "version": 1, - "text": text - } - }); - - process - .send_notification("textDocument/didOpen", Some(params)) - .await - } - - /// Document change notification. - pub async fn did_change( - &self, - language: &str, - uri: &str, - version: i32, - text: &str, - ) -> Result<()> { - let process = self.get_process(language).await?; - - let content_len = text.len(); - debug!( - "Sending didChange to LSP: lang={}, uri={}, version={}, size={} bytes", - language, uri, version, content_len - ); - - let params = serde_json::json!({ - "textDocument": { - "uri": uri, - "version": version - }, - "contentChanges": [{ - "text": text - }] - }); - - process - .send_notification("textDocument/didChange", Some(params)) - .await - } - - /// Document save notification. - pub async fn did_save(&self, language: &str, uri: &str) -> Result<()> { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - } - }); - - process - .send_notification("textDocument/didSave", Some(params)) - .await - } - - /// Document close notification. - pub async fn did_close(&self, language: &str, uri: &str) -> Result<()> { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - } - }); - - process - .send_notification("textDocument/didClose", Some(params)) - .await - } - - /// Gets code completion (protocol-only). - pub async fn get_completions( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result> { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "position": { - "line": line, - "character": character - } - }); - - let result = process - .send_request("textDocument/completion", Some(params)) - .await?; - - let items = if let Ok(list) = serde_json::from_value::(result.clone()) { - list.items - } else if let Ok(items) = serde_json::from_value::>(result.clone()) { - items - } else { - warn!("Unexpected completion response format, returning empty list"); - Vec::new() - }; - - Ok(items) - } - - /// Go to definition (protocol-only). - pub async fn goto_definition( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "position": { - "line": line, - "character": character - } - }); - - process - .send_request("textDocument/definition", Some(params)) - .await - } - - /// Gets hover information. - pub async fn get_hover( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "position": { - "line": line, - "character": character - } - }); - - process - .send_request("textDocument/hover", Some(params)) - .await - } - - /// Finds references. - pub async fn find_references( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "position": { - "line": line, - "character": character - }, - "context": { - "includeDeclaration": true - } - }); - - process - .send_request("textDocument/references", Some(params)) - .await - } - - /// Gets code actions. - pub async fn get_code_actions( - &self, - language: &str, - uri: &str, - range: serde_json::Value, - context: serde_json::Value, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "range": range, - "context": context - }); - - process - .send_request("textDocument/codeAction", Some(params)) - .await - } - - /// Formats a document. - pub async fn format_document( - &self, - language: &str, - uri: &str, - tab_size: u32, - insert_spaces: bool, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "options": { - "tabSize": tab_size, - "insertSpaces": insert_spaces - } - }); - - process - .send_request("textDocument/formatting", Some(params)) - .await - } - - /// Gets inlay hints. - pub async fn get_inlay_hints( - &self, - language: &str, - uri: &str, - start_line: u32, - start_character: u32, - end_line: u32, - end_character: u32, - ) -> Result> { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "range": { - "start": { - "line": start_line, - "character": start_character - }, - "end": { - "line": end_line, - "character": end_character - } - } - }); - - let result = process - .send_request("textDocument/inlayHint", Some(params)) - .await?; - - if result.is_null() { - return Ok(vec![]); - } - - let hints: Vec = serde_json::from_value(result) - .map_err(|e| anyhow!("Failed to parse inlay hints: {}", e))?; - - Ok(hints) - } - - /// Renames a symbol. - pub async fn rename( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - new_name: &str, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "position": { - "line": line, - "character": character - }, - "newName": new_name - }); - - process - .send_request("textDocument/rename", Some(params)) - .await - } - - /// Gets document highlights (Document Highlight). - /// Used to highlight all references of the symbol at the cursor. - pub async fn get_document_highlight( - &self, - language: &str, - uri: &str, - line: u32, - character: u32, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "position": { - "line": line, - "character": character - } - }); - - process - .send_request("textDocument/documentHighlight", Some(params)) - .await - } - - /// Gets document symbols (Document Symbols). - /// Used for outlines, symbol navigation, etc. - pub async fn get_document_symbols( - &self, - language: &str, - uri: &str, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - } - }); - - process - .send_request("textDocument/documentSymbol", Some(params)) - .await - } - - /// Gets semantic tokens (Semantic Tokens). - /// Used for semantic-level syntax highlighting. - pub async fn get_semantic_tokens( - &self, - language: &str, - uri: &str, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - } - }); - - process - .send_request("textDocument/semanticTokens/full", Some(params)) - .await - } - - /// Gets semantic tokens range (Semantic Tokens Range). - /// Used for incremental updates to semantic highlighting. - pub async fn get_semantic_tokens_range( - &self, - language: &str, - uri: &str, - range: serde_json::Value, - ) -> Result { - let process = self.get_process(language).await?; - - let params = serde_json::json!({ - "textDocument": { - "uri": uri - }, - "range": range - }); - - process - .send_request("textDocument/semanticTokens/range", Some(params)) - .await - } - - /// Returns server capabilities. - pub async fn get_server_capabilities(&self, language: &str) -> Result { - let process = self.get_process(language).await?; - - let capabilities = process - .get_capabilities() - .await - .ok_or_else(|| anyhow!("Server capabilities not available"))?; - - Ok(capabilities) - } - - /// Gets diagnostics for a file (from cache). - pub async fn get_diagnostics(&self, uri: &str) -> Vec { - let cache = self.diagnostics_cache.read().await; - cache - .get(uri) - .map(|d| d.as_ref().clone()) - .unwrap_or_default() - } - - /// Updates the diagnostics cache (called by `diagnostics_callback`). - pub async fn update_diagnostics_cache( - &self, - uri: String, - diagnostics: Arc>, - ) { - let mut cache = self.diagnostics_cache.write().await; - cache.insert(uri, diagnostics); - } - - /// Returns a handle to the diagnostics cache so hot paths can update it - /// without acquiring any lock on the `LspManager` itself. - pub fn diagnostics_cache_handle( - &self, - ) -> Arc>>>> { - self.diagnostics_cache.clone() - } -} - -impl Drop for LspManager { - fn drop(&mut self) { - debug!("Dropping LSP Manager"); - } -} diff --git a/src/crates/services/services-core/src/lsp/plugin_loader.rs b/src/crates/services/services-core/src/lsp/plugin_loader.rs deleted file mode 100644 index d8f024fb36..0000000000 --- a/src/crates/services/services-core/src/lsp/plugin_loader.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! LSP plugin loader -//! -//! Responsible for loading and installing plugins from the filesystem. - -use anyhow::{anyhow, Result}; -use log::{debug, error, info, warn}; -use std::path::{Path, PathBuf}; -use tokio::fs; - -use bitfun_core_types::lsp::LspPlugin; - -/// Plugin loader. -pub struct PluginLoader { - /// Plugins directory. - plugins_dir: PathBuf, -} - -impl PluginLoader { - /// Creates a new plugin loader. - pub fn new(plugins_dir: PathBuf) -> Self { - Self { plugins_dir } - } - - /// Loads a specific plugin. - pub async fn load_plugin(&self, plugin_id: &str) -> Result { - let plugin_dir = self.plugins_dir.join(plugin_id); - let manifest_path = plugin_dir.join("manifest.json"); - - if !manifest_path.exists() { - return Err(anyhow!( - "Plugin manifest not found: {}", - manifest_path.display() - )); - } - - let content = fs::read_to_string(&manifest_path).await?; - let plugin: LspPlugin = serde_json::from_str(&content) - .map_err(|e| anyhow!("Failed to parse manifest: {}", e))?; - - if plugin.id != plugin_id { - return Err(anyhow!( - "Plugin ID mismatch: expected '{}', found '{}'", - plugin_id, - plugin.id - )); - } - - info!("Plugin loaded: {} v{}", plugin.name, plugin.version); - debug!("Supported languages: {:?}", plugin.languages); - debug!("File extensions: {:?}", plugin.file_extensions); - - Ok(plugin) - } - - /// Loads all installed plugins. - pub async fn load_all_plugins(&self) -> Result> { - if !self.plugins_dir.exists() { - fs::create_dir_all(&self.plugins_dir).await?; - info!("Created plugins directory: {:?}", self.plugins_dir); - return Ok(vec![]); - } - - let mut plugins = Vec::new(); - let mut entries = fs::read_dir(&self.plugins_dir).await?; - - while let Some(entry) = entries.next_entry().await? { - let path = entry.path(); - - if path.is_dir() { - if let Some(plugin_id) = path.file_name().and_then(|n| n.to_str()) { - if plugin_id.starts_with('.') { - continue; - } - - if plugin_id == "temp" || plugin_id == "cache" || plugin_id == "backup" { - continue; - } - - match self.load_plugin(plugin_id).await { - Ok(plugin) => { - plugins.push(plugin); - } - Err(e) => { - error!("Failed to load plugin '{}': {}", plugin_id, e); - } - } - } - } - } - - info!("Successfully loaded {} plugin(s)", plugins.len()); - - Ok(plugins) - } - - /// Installs a plugin package (a `.vcpkg` file). - pub async fn install_plugin_package(&self, package_path: &Path) -> Result { - info!("Installing plugin package: {:?}", package_path); - - if !package_path.exists() { - error!("Plugin package not found: {:?}", package_path); - return Err(anyhow!("Plugin package not found: {:?}", package_path)); - } - - if package_path.extension().and_then(|e| e.to_str()) != Some("vcpkg") { - error!("Invalid plugin package format (expected .vcpkg)"); - return Err(anyhow!("Invalid plugin package format (expected .vcpkg)")); - } - - let temp_id = format!(".temp-{}", std::process::id()); - let temp_dir = self.plugins_dir.join(&temp_id); - - if temp_dir.exists() { - fs::remove_dir_all(&temp_dir).await?; - } - - fs::create_dir_all(&temp_dir).await?; - - let file = std::fs::File::open(package_path)?; - let mut archive = zip::ZipArchive::new(file)?; - - let mut manifest_content = String::new(); - { - let mut manifest_file = archive.by_name("manifest.json")?; - std::io::Read::read_to_string(&mut manifest_file, &mut manifest_content)?; - } - - let plugin: LspPlugin = serde_json::from_str(&manifest_content)?; - let plugin_id = plugin.id.clone(); - - let plugin_dir = self.plugins_dir.join(&plugin_id); - if plugin_dir.exists() { - return Err(anyhow!("Plugin already installed: {}", plugin_id)); - } - - archive.extract(&plugin_dir)?; - - if temp_dir.exists() { - let _ = fs::remove_dir_all(&temp_dir).await; - } - - info!( - "Plugin installed: {} v{} (id: {})", - plugin.name, plugin.version, plugin_id - ); - - Ok(plugin_id) - } - - /// Uninstalls a plugin. - pub async fn uninstall_plugin(&self, plugin_id: &str) -> Result<()> { - info!("Uninstalling plugin: {}", plugin_id); - - let plugin_dir = self.plugins_dir.join(plugin_id); - - if !plugin_dir.exists() { - error!("Plugin not found: {}", plugin_id); - return Err(anyhow!("Plugin not found: {}", plugin_id)); - } - - fs::remove_dir_all(&plugin_dir).await?; - - info!("Plugin uninstalled successfully: {}", plugin_id); - - Ok(()) - } - - /// Cleans up temporary directories. - pub async fn cleanup_temp_dirs(&self) -> Result<()> { - let mut entries = fs::read_dir(&self.plugins_dir).await?; - let mut cleaned_count = 0; - - while let Some(entry) = entries.next_entry().await? { - let path = entry.path(); - - if path.is_dir() { - if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) { - if dir_name.starts_with(".temp") { - if let Err(e) = fs::remove_dir_all(&path).await { - warn!("Failed to remove temp directory {}: {}", dir_name, e); - } else { - cleaned_count += 1; - } - } - } - } - } - - if cleaned_count > 0 { - info!("Cleaned {} temporary director(ies)", cleaned_count); - } - - Ok(()) - } - - /// Returns the plugin server executable path. - pub fn get_server_path(&self, plugin: &LspPlugin) -> Result { - let plugin_dir = self.plugins_dir.join(&plugin.id); - - let command = - crate::lsp::resolve_plugin_command_for_current_target(&plugin.server.command)?; - - let command = command.replace('/', std::path::MAIN_SEPARATOR_STR); - - let server_path = plugin_dir.join(&command); - - if !server_path.exists() { - #[cfg(windows)] - { - let mut server_path = server_path.clone(); - let extensions = vec![".exe", ".bat", ".cmd"]; - let mut found = false; - - for ext in extensions { - let path_with_ext = plugin_dir.join(format!("{}{}", command, ext)); - - if path_with_ext.exists() { - server_path = path_with_ext; - found = true; - break; - } - } - - if !found { - error!("LSP server binary not found at: {:?}", server_path); - error!("Tried extensions: .exe, .bat, .cmd"); - error!("Plugin directory: {:?}", plugin_dir); - return Err(anyhow!( - "LSP server binary not found: {}\nTried: {}.exe, {}.bat, {}.cmd", - server_path.display(), - command, - command, - command - )); - } - } - - #[cfg(not(windows))] - { - error!("LSP server binary not found: {:?}", server_path); - return Err(anyhow!( - "LSP server binary not found: {}", - server_path.display() - )); - } - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(&server_path)?.permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&server_path, perms)?; - } - - Ok(server_path) - } - - /// Returns the plugin directory path. - pub fn get_plugin_dir(&self, plugin_id: &str) -> PathBuf { - self.plugins_dir.join(plugin_id) - } - - /// Returns the plugins root directory. - pub fn get_plugins_root(&self) -> &Path { - &self.plugins_dir - } -} diff --git a/src/crates/services/services-core/src/lsp/process.rs b/src/crates/services/services-core/src/lsp/process.rs deleted file mode 100644 index 820aa73390..0000000000 --- a/src/crates/services/services-core/src/lsp/process.rs +++ /dev/null @@ -1,1139 +0,0 @@ -//! LSP server process management -//! -//! Manages the lifecycle of a single LSP server process. - -use anyhow::{anyhow, Result}; -use log::{debug, error, info, warn}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use tokio::io::BufReader; -use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout}; -use tokio::sync::{mpsc, oneshot, RwLock}; -use tokio::time::{timeout, Duration}; - -use crate::lsp::protocol::{ - create_notification, create_request, extract_result, read_message, write_message, -}; -use bitfun_core_types::lsp::{ - ClientCapabilities, InitializeParams, InitializeResult, JsonRpcError, JsonRpcMessage, - JsonRpcResponse, RuntimeType, ServerConfig, WorkspaceFolder, -}; - -/// Process crash callback type. -pub type CrashCallback = Arc; - -/// Progress notification callback type. -/// Parameters: `(kind: "begin" | "report" | "end", token: String, percentage: Option, message: String)`. -pub type ProgressCallback = Arc, String) + Send + Sync>; - -/// Token creation callback type. -/// Parameters: `(token: String)`. -pub type TokenCreateCallback = Arc; - -/// Diagnostics callback type. -/// Parameters: `(uri: String, diagnostics: Vec)`. -pub type DiagnosticsCallback = Arc) + Send + Sync>; - -/// LSP server process. -pub struct LspServerProcess { - /// Plugin ID. - pub id: String, - /// Child process. - child: Arc>, - /// Standard input. - stdin: Arc>, - /// Request ID counter. - request_id: Arc, - /// Pending requests waiting for a response. - pending_requests: Arc>>>, - /// Notification sender. - notification_tx: mpsc::UnboundedSender, - /// Server capabilities. - capabilities: Arc>>, - /// Crash callback. - crash_callback: Option, - /// Progress callback. - progress_callback: Option, - /// Token creation callback. - token_create_callback: Option, - /// Diagnostics callback. - diagnostics_callback: Option, -} - -impl LspServerProcess { - /// Spawns a new LSP server process. - pub async fn spawn( - id: String, - server_bin: PathBuf, - config: &ServerConfig, - crash_callback: Option, - progress_callback: Option, - token_create_callback: Option, - diagnostics_callback: Option, - ) -> Result { - info!("Spawning LSP server: {} at {:?}", id, server_bin); - debug!( - "LSP config - args: {:?}, env: {:?}", - config.args, config.env - ); - - if !server_bin.exists() { - error!("LSP server binary not found: {:?}", server_bin); - return Err(anyhow!("LSP server binary not found: {:?}", server_bin)); - } - - let runtime_type = Self::detect_runtime_type(config, &server_bin); - debug!("Detected runtime type: {:?}", runtime_type); - - let mut cmd = Self::build_command(&runtime_type, &server_bin, config)?; - - cmd.stdin(Stdio::piped()); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - - let mut child = cmd.spawn().map_err(|e| { - error!("Failed to spawn LSP server {}: {}", id, e); - anyhow!("Failed to spawn LSP server {}: {}", id, e) - })?; - - if let Some(pid) = child.id() { - debug!("LSP server process started with PID: {}", pid); - } - - let stdin = child - .stdin - .take() - .ok_or_else(|| anyhow!("Failed to capture stdin"))?; - let stdout = child - .stdout - .take() - .ok_or_else(|| anyhow!("Failed to capture stdout"))?; - - let stderr = child - .stderr - .take() - .ok_or_else(|| anyhow!("Failed to capture stderr"))?; - - let (notification_tx, notification_rx) = mpsc::unbounded_channel(); - - let process = Self { - id: id.clone(), - child: Arc::new(RwLock::new(child)), - stdin: Arc::new(RwLock::new(stdin)), - request_id: Arc::new(AtomicU64::new(1)), - pending_requests: Arc::new(RwLock::new(HashMap::new())), - notification_tx, - capabilities: Arc::new(RwLock::new(None)), - crash_callback, - progress_callback, - token_create_callback, - diagnostics_callback, - }; - - process.start_read_task(stdout).await; - - process.start_stderr_task(stderr).await; - - process.start_notification_task(notification_rx).await; - - info!("LSP server process spawned: {}", id); - - Ok(process) - } - - /// Starts the message reader task. - async fn start_read_task(&self, stdout: ChildStdout) { - let pending_requests = self.pending_requests.clone(); - let notification_tx = self.notification_tx.clone(); - let id = self.id.clone(); - let crash_callback = self.crash_callback.clone(); - - tokio::spawn(async move { - let mut reader = BufReader::new(stdout); - let mut consecutive_timeouts = 0; - const MAX_CONSECUTIVE_TIMEOUTS: u32 = 3; - - loop { - match timeout(Duration::from_secs(30), read_message(&mut reader)).await { - Ok(Ok(message)) => { - consecutive_timeouts = 0; - - match message { - JsonRpcMessage::Response(response) => { - let request_id = response.id; - let mut pending = pending_requests.write().await; - - if let Some(sender) = pending.remove(&request_id) { - // Move the response instead of cloning - // (responses can be MB-sized, e.g. semanticTokens). - let _ = sender.send(response); - } else { - warn!( - "[{}] Received response for unknown request ID: {}", - id, request_id - ); - } - } - message @ JsonRpcMessage::Notification(_) => { - if let Err(e) = notification_tx.send(message) { - error!("[{}] Failed to send notification: {}", id, e); - break; - } - } - message @ JsonRpcMessage::Request(_) => { - if let Err(e) = notification_tx.send(message) { - error!("[{}] Failed to send request: {}", id, e); - break; - } - } - } - } - Ok(Err(e)) => { - error!("[{}] Failed to read message: {}", id, e); - error!("[{}] This usually means the LSP server is outputting non-protocol data to stdout", id); - break; - } - Err(_) => { - consecutive_timeouts += 1; - - if consecutive_timeouts >= MAX_CONSECUTIVE_TIMEOUTS { - warn!( - "[{}] No LSP messages for {}s (this is normal if idle)", - id, - 30 * MAX_CONSECUTIVE_TIMEOUTS - ); - - consecutive_timeouts = 0; - } - } - } - } - - error!("LSP server read task ended abnormally: {}", id); - - { - let mut pending = pending_requests.write().await; - let count = pending.len(); - if count > 0 { - warn!("Dropping {} pending request(s) for server {}", count, id); - } - pending.clear(); - } - - if let Some(callback) = crash_callback { - error!("Invoking crash callback - server connection lost: {}", id); - callback(id.clone()); - } - }); - } - - /// Starts the stderr reader task. - /// - /// This task continuously reads the LSP server's stderr output to prevent the pipe buffer from - /// filling up and blocking the process. - /// The LSP protocol specifies using stdout for protocol communication; stderr is used for the - /// server's diagnostic logs. - async fn start_stderr_task(&self, stderr: ChildStderr) { - let id = self.id.clone(); - - tokio::spawn(async move { - use tokio::io::AsyncBufReadExt; - let mut reader = BufReader::new(stderr); - let mut line = String::new(); - let mut line_count = 0; - let mut error_count = 0; - let mut warn_count = 0; - - let mut missing_cmake = false; - let mut missing_spectre = false; - let mut build_script_errors = std::collections::HashSet::new(); - - loop { - line.clear(); - match reader.read_line(&mut line).await { - Ok(0) => break, - Ok(_) => { - let trimmed = line.trim(); - if !trimmed.is_empty() { - line_count += 1; - - let lower = trimmed.to_lowercase(); - - if lower.contains("missing dependency: cmake") - || (lower.contains("failed to spawn") && lower.contains("cmake")) - { - if !missing_cmake { - missing_cmake = true; - warn!("[{}] Missing build dependency: CMake not installed or not in PATH", id); - info!("[{}] Tip: Some Rust crates require CMake to compile C/C++ code. Download: https://cmake.org/download/", id); - } - continue; - } - - if lower.contains("no spectre-mitigated libs") { - if !missing_spectre { - missing_spectre = true; - warn!("[{}] Missing build dependency: MSVC Spectre mitigation libraries not installed", id); - info!("[{}] Tip: Some Rust crates require MSVC Spectre libraries. Install via Visual Studio Installer", id); - } - continue; - } - - if lower.contains("failed to run custom build command") { - if let Some(start) = trimmed.find("for `") { - if let Some(end) = trimmed[start + 5..].find('`') { - let package = &trimmed[start + 5..start + 5 + end]; - if build_script_errors.insert(package.to_string()) { - warn!("[{}] Build script failed for package: {} (LSP may still work but code analysis accuracy may be affected)", id, package); - } - } - } - continue; - } - - if lower.contains("compiling") - || lower.contains("building") - || lower.contains("cargo:rerun-if") - { - continue; - } - - if lower.contains("panic") { - error_count += 1; - if error_count <= 3 { - debug!("[{}] Build script panic: {}", id, trimmed); - } - continue; - } - - if lower.contains("error") || lower.contains("fatal") { - error_count += 1; - - if error_count <= 5 { - error!("[{}] stderr: {}", id, trimmed); - } else if error_count % 10 == 0 { - error!("[{}] stderr: ... (omitted {} errors)", id, error_count); - } - } else if lower.contains("warn") || lower.contains("warning") { - warn_count += 1; - - if warn_count <= 10 { - warn!("[{}] stderr: {}", id, trimmed); - } else if warn_count % 100 == 0 { - warn!("[{}] stderr: ... (omitted {} warnings)", id, warn_count); - } - } else { - if line_count <= 5 || line_count % 1000 == 0 { - debug!("[{}] stderr: {}", id, trimmed); - } - } - } - } - Err(e) => { - error!("Failed to read stderr from {}: {}", id, e); - break; - } - } - } - - if line_count > 0 || error_count > 0 || warn_count > 0 { - info!( - "LSP server stderr task ended: {} (read {} lines, {} errors, {} warnings)", - id, line_count, error_count, warn_count - ); - - if !build_script_errors.is_empty() { - warn!("[{}] {} package(s) had build script failures, but LSP service is still running", id, build_script_errors.len()); - } - - if missing_cmake || missing_spectre { - info!("[{}] Tip: Installing missing dependencies may improve code analysis accuracy", id); - } - } - }); - } - - /// Starts the notification handler task. - async fn start_notification_task( - &self, - mut notification_rx: mpsc::UnboundedReceiver, - ) { - let id = self.id.clone(); - let progress_callback = self.progress_callback.clone(); - let token_create_callback = self.token_create_callback.clone(); - let diagnostics_callback = self.diagnostics_callback.clone(); - let stdin = self.stdin.clone(); - - tokio::spawn(async move { - while let Some(message) = notification_rx.recv().await { - match message { - JsonRpcMessage::Notification(mut notif) => match notif.method.as_str() { - "$/progress" => { - if let Some(params) = ¬if.params { - let token = params - .get("token") - .and_then(|t| t.as_str()) - .unwrap_or("unknown") - .to_string(); - - if let Some(value) = params.get("value") { - if let Some(kind) = value.get("kind").and_then(|k| k.as_str()) { - match kind { - "begin" => { - let title = value - .get("title") - .and_then(|t| t.as_str()) - .unwrap_or(""); - info!("[{}] Indexing started: {}", id, title); - - if let Some(ref callback) = progress_callback { - callback( - "begin".to_string(), - token.clone(), - Some(0), - title.to_string(), - ); - } - } - "report" => { - let percentage = value - .get("percentage") - .and_then(|p| p.as_u64()); - let message = value - .get("message") - .and_then(|m| m.as_str()) - .unwrap_or(""); - - if let Some(ref callback) = progress_callback { - callback( - "report".to_string(), - token.clone(), - percentage.map(|p| p as u32), - message.to_string(), - ); - } - } - "end" => { - let message = value - .get("message") - .and_then(|m| m.as_str()) - .unwrap_or(""); - info!("[{}] Indexing completed: {}", id, message); - - if let Some(ref callback) = progress_callback { - callback( - "end".to_string(), - token.clone(), - Some(100), - message.to_string(), - ); - } - } - _ => {} - } - } - } - } - } - "textDocument/publishDiagnostics" => { - if let Some(params) = notif.params.as_mut() { - if let Some(uri) = params - .get("uri") - .and_then(|u| u.as_str()) - .map(str::to_string) - { - // Move the diagnostics array out of the owned - // notification instead of deep-cloning it. - if let Some(serde_json::Value::Array(diags)) = params - .get_mut("diagnostics") - .filter(|d| d.is_array()) - .map(serde_json::Value::take) - { - debug!( - "[{}] Diagnostics: {} items for {}", - id, - diags.len(), - uri - ); - - if let Some(callback) = &diagnostics_callback { - callback(uri, diags); - } - } - } - } - } - "window/logMessage" => { - if let Some(params) = ¬if.params { - let msg_type = - params.get("type").and_then(|t| t.as_u64()).unwrap_or(3); - if let Some(msg) = params.get("message").and_then(|m| m.as_str()) { - match msg_type { - 1 => error!("[{}] Server log: {}", id, msg), - 2 => warn!("[{}] Server log: {}", id, msg), - 3 => info!("[{}] Server log: {}", id, msg), - 4 => debug!("[{}] Server log: {}", id, msg), - _ => debug!("[{}] Server log: {}", id, msg), - } - } - } - } - "window/showMessage" => { - if let Some(params) = ¬if.params { - let msg_type = - params.get("type").and_then(|t| t.as_u64()).unwrap_or(3); - if let Some(msg) = params.get("message").and_then(|m| m.as_str()) { - match msg_type { - 1 => error!("[{}] Server message: {}", id, msg), - 2 => warn!("[{}] Server message: {}", id, msg), - 3 => info!("[{}] Server message: {}", id, msg), - 4 => debug!("[{}] Server message: {}", id, msg), - _ => info!("[{}] Server message: {}", id, msg), - } - } - } - } - _ => {} - }, - - JsonRpcMessage::Request(req) => match req.method.as_str() { - "window/workDoneProgress/create" => { - if let Some(params) = &req.params { - if let Some(token) = params.get("token") { - let token_str = token.as_str().unwrap_or("unknown").to_string(); - - if let Some(ref callback) = token_create_callback { - callback(token_str); - } - } - } - - let response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: req.id, - result: Some(serde_json::Value::Null), - error: None, - }; - - let response_message = JsonRpcMessage::Response(response); - let mut stdin_lock = stdin.write().await; - if let Err(e) = write_message(&mut stdin_lock, &response_message).await - { - error!( - "[{}] Failed to send workDoneProgress/create response: {}", - id, e - ); - } - } - "client/registerCapability" => { - let response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: req.id, - result: Some(serde_json::Value::Null), - error: None, - }; - - let response_message = JsonRpcMessage::Response(response); - let mut stdin_lock = stdin.write().await; - if let Err(e) = write_message(&mut stdin_lock, &response_message).await - { - error!( - "[{}] Failed to send registerCapability response: {}", - id, e - ); - } - } - "workspace/configuration" => { - let response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: req.id, - result: Some(serde_json::json!([])), - error: None, - }; - - let response_message = JsonRpcMessage::Response(response); - let mut stdin_lock = stdin.write().await; - if let Err(e) = write_message(&mut stdin_lock, &response_message).await - { - error!("[{}] Failed to send configuration response: {}", id, e); - } - } - _ => { - warn!("[{}] Unhandled server request: {}", id, req.method); - - let response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: req.id, - result: None, - error: Some(JsonRpcError { - code: -32601, - message: format!("Method not supported: {}", req.method), - data: None, - }), - }; - - let response_message = JsonRpcMessage::Response(response); - let mut stdin_lock = stdin.write().await; - if let Err(e) = write_message(&mut stdin_lock, &response_message).await - { - error!("[{}] Failed to send error response: {}", id, e); - } - } - }, - _ => {} - } - } - - info!("LSP notification task ended: {}", id); - }); - } - - /// Sends a request and waits for the response. - pub async fn send_request( - &self, - method: impl Into, - params: Option, - ) -> Result { - let id = self.request_id.fetch_add(1, Ordering::SeqCst); - let method_str = method.into(); - - let message = create_request(id, method_str.clone(), params); - - let (tx, rx) = oneshot::channel(); - - { - let mut pending = self.pending_requests.write().await; - pending.insert(id, tx); - } - - { - let mut stdin = self.stdin.write().await; - write_message(&mut stdin, &message).await?; - } - - let response = timeout(Duration::from_secs(60), rx).await.map_err(|_| { - error!("LSP request timeout after 60s: {}", method_str); - anyhow!( - "LSP request timeout (60s): {}. The LSP server may not be responding.", - method_str - ) - })??; - - extract_result(response) - } - - /// Sends a notification (does not wait for a response). - pub async fn send_notification( - &self, - method: impl Into, - params: Option, - ) -> Result<()> { - let method_str = method.into(); - let message = create_notification(method_str, params); - - let mut stdin = self.stdin.write().await; - write_message(&mut stdin, &message).await?; - - Ok(()) - } - - /// Initializes the server. - pub async fn initialize(&self, workspace_root: Option) -> Result { - info!("Initializing LSP server: {}", self.id); - - let root_uri = workspace_root.as_ref().map(|path| { - if cfg!(windows) { - format!("file:///{}", path.replace('\\', "/")) - } else { - format!("file://{}", path) - } - }); - - let workspace_folders = workspace_root.as_ref().map(|root| { - let uri = if cfg!(windows) { - format!("file:///{}", root.replace('\\', "/")) - } else { - format!("file://{}", root) - }; - - let name = std::path::Path::new(root) - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("workspace") - .to_string(); - - vec![WorkspaceFolder { uri, name }] - }); - - let params = InitializeParams { - process_id: Some(std::process::id()), - root_path: None, - root_uri: root_uri.clone(), - capabilities: ClientCapabilities { - window: Some(serde_json::json!({ - "workDoneProgress": true, - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": false - } - }, - "showDocument": { - "support": true - } - })), - - workspace: Some(serde_json::json!({ - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": ["create", "rename", "delete"] - }, - "didChangeConfiguration": { - "dynamicRegistration": false - }, - "didChangeWatchedFiles": { - "dynamicRegistration": false - }, - "symbol": { - "dynamicRegistration": false - }, - "executeCommand": { - "dynamicRegistration": false - }, - "workspaceFolders": true, - "configuration": true - })), - text_document: Some(serde_json::json!({ - "synchronization": { - "dynamicRegistration": false, - "didSave": true, - "willSave": false, - "willSaveWaitUntil": false - }, - "completion": { - "dynamicRegistration": false, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": false, - "documentationFormat": ["plaintext", "markdown"], - "deprecatedSupport": false, - "preselectSupport": false - }, - "contextSupport": false - }, - "hover": { - "dynamicRegistration": false, - "contentFormat": ["plaintext", "markdown"] - }, - "signatureHelp": { - "dynamicRegistration": false, - "signatureInformation": { - "documentationFormat": ["plaintext", "markdown"] - } - }, - "definition": { - "dynamicRegistration": false, - "linkSupport": true - }, - "references": { - "dynamicRegistration": false - }, - "documentHighlight": { - "dynamicRegistration": false - }, - "documentSymbol": { - "dynamicRegistration": false, - "hierarchicalDocumentSymbolSupport": true - }, - "codeAction": { - "dynamicRegistration": false, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": ["quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports"] - } - } - }, - "formatting": { - "dynamicRegistration": false - }, - "rangeFormatting": { - "dynamicRegistration": false - }, - "rename": { - "dynamicRegistration": false, - "prepareSupport": false - }, - "publishDiagnostics": { - "relatedInformation": true, - "tagSupport": { - "valueSet": [1, 2] - } - }, - "inlayHint": { - "dynamicRegistration": false, - "resolveSupport": { - "properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"] - } - } - })), - experimental: None, - }, - - initialization_options: Some(serde_json::json!({ - - "checkOnSave": { - "command": "clippy" - }, - "cargo": { - "allFeatures": true - }, - - })), - - workspace_folders, - }; - - let result = self - .send_request("initialize", Some(serde_json::to_value(params)?)) - .await?; - - let init_result: InitializeResult = serde_json::from_value(result)?; - - { - let mut caps = self.capabilities.write().await; - *caps = Some(serde_json::to_value(&init_result.capabilities)?); - } - - self.send_notification("initialized", Some(serde_json::json!({}))) - .await?; - - info!("LSP server initialized: {}", self.id); - - Ok(init_result) - } - - /// Shuts down the server. - pub async fn shutdown(&self) -> Result<()> { - info!("Shutting down LSP server: {}", self.id); - - let _ = self.send_request("shutdown", None).await; - - let _ = self.send_notification("exit", None).await; - - tokio::time::sleep(Duration::from_millis(500)).await; - - let mut child = self.child.write().await; - let _ = child.kill().await; - - info!("LSP server shut down: {}", self.id); - - Ok(()) - } - - /// Returns server capabilities. - pub async fn get_capabilities(&self) -> Option { - let caps = self.capabilities.read().await; - caps.clone() - } - - /// Returns whether the process is still alive. - pub async fn is_alive(&self) -> bool { - let mut child = self.child.write().await; - match child.try_wait() { - Ok(Some(status)) => { - warn!("[{}] Process has exited with status: {:?}", self.id, status); - false - } - Ok(None) => true, - Err(e) => { - error!("[{}] Failed to check process status: {}", self.id, e); - false - } - } - } - - /// Detects the runtime type. - fn detect_runtime_type(config: &ServerConfig, server_bin: &Path) -> RuntimeType { - if let Some(runtime) = &config.runtime { - debug!("Runtime explicitly specified: {}", runtime); - return match runtime.to_lowercase().as_str() { - "bash" | "sh" => RuntimeType::Bash, - "node" | "nodejs" => RuntimeType::Node, - "exe" | "executable" => RuntimeType::Executable, - _ => { - warn!( - "Unknown runtime type '{}', defaulting to executable", - runtime - ); - RuntimeType::Executable - } - }; - } - - if let Some(ext) = server_bin.extension().and_then(|e| e.to_str()) { - match ext.to_lowercase().as_str() { - "sh" | "bash" => return RuntimeType::Bash, - "js" | "mjs" | "cjs" => return RuntimeType::Node, - _ => {} - } - } - - RuntimeType::Executable - } - - /// Builds the command based on the runtime type. - fn build_command( - runtime_type: &RuntimeType, - server_bin: &PathBuf, - config: &ServerConfig, - ) -> Result { - match runtime_type { - RuntimeType::Executable => { - #[cfg(windows)] - { - if let Some(ext) = server_bin.extension().and_then(|e| e.to_str()) { - let ext_lower = ext.to_lowercase(); - - if ext_lower == "bat" || ext_lower == "cmd" { - debug!( - "Detected batch file (.{}), extracting node command", - ext_lower - ); - - if let Ok(content) = std::fs::read_to_string(server_bin) { - let mut script_path: Option = None; - - for line in content.lines() { - let line = line.trim(); - - if line.starts_with("node ") || line.starts_with("node.exe ") { - info!("Found node execution command: {}", line); - - if let Some(start_quote) = line.find('"') { - if let Some(end_quote) = - line[start_quote + 1..].find('"') - { - let path_expr = &line - [start_quote + 1..start_quote + 1 + end_quote]; - debug!("Extracted path expression: {}", path_expr); - - for prev_line in content.lines() { - let prev_line = prev_line.trim(); - if prev_line.starts_with("set ") - && prev_line.contains( - path_expr - .trim_matches('%') - .split('%') - .next() - .unwrap_or(""), - ) - { - if let Some(eq_pos) = prev_line.find('=') { - let value_part = &prev_line - [eq_pos + 1..] - .trim_matches('"'); - - if let Some(parent) = - server_bin.parent() - { - let mut resolved_path = - parent.to_path_buf(); - - let rel_part = value_part - .replace("%SCRIPT_DIR%", ""); - - for component in - rel_part.split(['\\', '/']) - { - match component { - "" | "." => continue, - ".." => { - resolved_path.pop(); - } - part => { - resolved_path.push(part) - } - } - } - - if resolved_path.exists() { - script_path = - Some(resolved_path); - break; - } else { - warn!("Resolved path does not exist: {:?}", resolved_path); - } - } - } - } - } - } - } - break; - } - } - - if let Some(js_path) = script_path { - let node_cmd = if cfg!(windows) { "node.exe" } else { "node" }; - - let mut cmd = - crate::process_manager::create_tokio_command(node_cmd); - cmd.arg(js_path); - cmd.args(&config.args); - cmd.envs(&config.env); - return Ok(cmd); - } - } - - error!("Failed to extract node command from bat file"); - error!("Bat files cannot be executed directly without cmd wrapper"); - return Err(anyhow!( - "Failed to parse batch file. Please check the plugin installation." - )); - } - } - } - - let mut cmd = crate::process_manager::create_tokio_command(server_bin); - cmd.args(&config.args); - cmd.envs(&config.env); - Ok(cmd) - } - RuntimeType::Bash => { - #[cfg(windows)] - { - let bash_paths = vec![ - "bash.exe", - "C:\\Program Files\\Git\\bin\\bash.exe", - "C:\\Program Files (x86)\\Git\\bin\\bash.exe", - "wsl.exe", - ]; - - let mut bash_exe = None; - for path in &bash_paths { - if crate::process_manager::create_command(path) - .arg("--version") - .output() - .is_ok() - { - bash_exe = Some(path.to_string()); - break; - } - } - - let bash_cmd = bash_exe.ok_or_else(|| { - error!( - "Bash not found on Windows. Searched paths: {:?}", - bash_paths - ); - anyhow!( - "Bash not found on Windows. Please install Git Bash or WSL.\n\ - - Git Bash: https://git-scm.com/download/win\n\ - - WSL: https://docs.microsoft.com/windows/wsl/install" - ) - })?; - - let mut cmd = crate::process_manager::create_tokio_command(&bash_cmd); - cmd.arg(server_bin); - cmd.args(&config.args); - cmd.envs(&config.env); - Ok(cmd) - } - - #[cfg(not(windows))] - { - let mut cmd = crate::process_manager::create_tokio_command("bash"); - cmd.arg(server_bin); - cmd.args(&config.args); - cmd.envs(&config.env); - Ok(cmd) - } - } - RuntimeType::Node => { - let node_cmd = if cfg!(windows) { "node.exe" } else { "node" }; - - match crate::process_manager::create_command(node_cmd) - .arg("--version") - .output() - { - Ok(_) => {} - Err(e) => { - error!("Node.js not found: {}", e); - return Err(anyhow!( - "Node.js not found. Please install Node.js from https://nodejs.org/\n\ - The LSP plugin requires Node.js to be installed and available in PATH." - )); - } - } - - let mut cmd = crate::process_manager::create_tokio_command(node_cmd); - cmd.arg(server_bin); - cmd.args(&config.args); - cmd.envs(&config.env); - Ok(cmd) - } - } - } -} - -impl Drop for LspServerProcess { - fn drop(&mut self) { - debug!("Dropping LSP server process: {}", self.id); - } -} - -#[cfg(test)] -mod tests { - use super::LspServerProcess; - use bitfun_core_types::lsp::{RuntimeType, ServerConfig}; - use std::collections::HashMap; - use std::path::PathBuf; - - fn server_config(runtime: Option<&str>) -> ServerConfig { - ServerConfig { - command: "server".to_string(), - args: Vec::new(), - env: HashMap::new(), - runtime: runtime.map(str::to_string), - } - } - - #[test] - fn detects_runtime_from_manifest_before_extension() { - let config = server_config(Some("node")); - let server_bin = PathBuf::from("server.sh"); - - assert_eq!( - LspServerProcess::detect_runtime_type(&config, &server_bin), - RuntimeType::Node - ); - } - - #[test] - fn detects_runtime_from_server_extension() { - assert_eq!( - LspServerProcess::detect_runtime_type( - &server_config(None), - &PathBuf::from("server.sh") - ), - RuntimeType::Bash - ); - assert_eq!( - LspServerProcess::detect_runtime_type( - &server_config(None), - &PathBuf::from("server.mjs") - ), - RuntimeType::Node - ); - assert_eq!( - LspServerProcess::detect_runtime_type( - &server_config(None), - &PathBuf::from("server.exe") - ), - RuntimeType::Executable - ); - } -} diff --git a/src/crates/services/services-core/src/lsp/project_detector.rs b/src/crates/services/services-core/src/lsp/project_detector.rs deleted file mode 100644 index 1ba1c4c36c..0000000000 --- a/src/crates/services/services-core/src/lsp/project_detector.rs +++ /dev/null @@ -1,510 +0,0 @@ -//! Project type detector -//! -//! Features: -//! - Scans the workspace to identify project types -//! - Detects programming languages in use -//! - Counts files by type -//! - Determines the primary programming language -//! - Supports monorepos and subdirectory project layouts - -use anyhow::Result; -use log::{debug, info}; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; -use tokio::fs; - -/// Project information. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[derive(Default)] -pub struct ProjectInfo { - /// Detected languages. - pub languages: Vec, - /// Primary language (usually the one with the most files). - pub primary_language: Option, - /// File counts per language. - pub file_counts: HashMap, - /// Project type tags. - pub project_types: Vec, - /// Total file count. - pub total_files: usize, -} - -/// Project type detector. -pub struct ProjectDetector; - -impl ProjectDetector { - /// Detects the project type. - pub async fn detect(workspace_path: &Path) -> Result { - debug!("Detecting project type for: {:?}", workspace_path); - - let mut info = ProjectInfo::default(); - - Self::detect_by_file_extensions(workspace_path, &mut info).await?; - - Self::detect_by_config_files(workspace_path, &mut info).await; - - Self::determine_primary_language(&mut info); - - Self::deduplicate_languages(&mut info); - - info!( - "Project detection complete: languages={:?}, primary={:?}, project_types={:?}", - info.languages, info.primary_language, info.project_types - ); - - Ok(info) - } - - /// Detects project type via config files (supports root and subdirectories). - async fn detect_by_config_files(workspace_path: &Path, info: &mut ProjectInfo) { - Self::detect_root_config_files(workspace_path, info); - - Self::detect_subdirectory_config_files(workspace_path, info).await; - } - - /// Detects config files in the workspace root. - fn detect_root_config_files(workspace_path: &Path, info: &mut ProjectInfo) { - if workspace_path.join("tsconfig.json").exists() { - Self::add_language(info, "typescript"); - Self::add_project_type(info, "typescript"); - } - - if workspace_path.join("package.json").exists() { - if !info.languages.contains(&"typescript".to_string()) { - Self::add_language(info, "javascript"); - } - Self::add_project_type(info, "nodejs"); - } - - if workspace_path.join("Cargo.toml").exists() { - Self::add_language(info, "rust"); - Self::add_project_type(info, "rust"); - } - - if workspace_path.join("pyproject.toml").exists() - || workspace_path.join("setup.py").exists() - || workspace_path.join("requirements.txt").exists() - { - Self::add_language(info, "python"); - Self::add_project_type(info, "python"); - } - - if workspace_path.join("go.mod").exists() { - Self::add_language(info, "go"); - Self::add_project_type(info, "go"); - } - - if workspace_path.join("pom.xml").exists() - || workspace_path.join("build.gradle").exists() - || workspace_path.join("build.gradle.kts").exists() - { - Self::add_language(info, "java"); - Self::add_project_type(info, "java"); - } - - if workspace_path.join("CMakeLists.txt").exists() - || workspace_path.join("Makefile").exists() - || workspace_path.join("meson.build").exists() - { - Self::add_language(info, "cpp"); - Self::add_project_type(info, "cpp"); - } - - if Self::has_file_with_extension(workspace_path, "csproj") - || Self::has_file_with_extension(workspace_path, "fsproj") - || workspace_path.join("global.json").exists() - { - Self::add_language(info, "csharp"); - Self::add_project_type(info, "dotnet"); - } - } - - /// Detects config files in subdirectories (supports monorepo layouts). - async fn detect_subdirectory_config_files(workspace_path: &Path, info: &mut ProjectInfo) { - let subdirectories = [ - "cli", - "src-tauri", - "crates", - "rust", - "backend", - "core", - "lib", - "packages", - "apps", - "frontend", - "web", - "client", - "server", - "api", - "src", - ]; - - for subdir in subdirectories { - let subdir_path = workspace_path.join(subdir); - if !subdir_path.exists() || !subdir_path.is_dir() { - continue; - } - - if subdir_path.join("Cargo.toml").exists() { - Self::add_language(info, "rust"); - Self::add_project_type(info, "rust"); - } - - if subdir_path.join("go.mod").exists() { - Self::add_language(info, "go"); - Self::add_project_type(info, "go"); - } - - if subdir_path.join("pyproject.toml").exists() || subdir_path.join("setup.py").exists() - { - Self::add_language(info, "python"); - Self::add_project_type(info, "python"); - } - - if subdir_path.join("pom.xml").exists() - || subdir_path.join("build.gradle").exists() - || subdir_path.join("build.gradle.kts").exists() - { - Self::add_language(info, "java"); - Self::add_project_type(info, "java"); - } - } - - if let Ok(mut entries) = fs::read_dir(workspace_path).await { - while let Ok(Some(entry)) = entries.next_entry().await { - let path = entry.path(); - if path.is_dir() { - let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); - - if matches!( - dir_name, - "node_modules" | "target" | ".git" | "dist" | "build" | "out" - ) { - continue; - } - - if path.join("Cargo.toml").exists() - && !info.languages.contains(&"rust".to_string()) - { - Self::add_language(info, "rust"); - Self::add_project_type(info, "rust"); - } - } - } - } - } - - /// Checks whether a directory contains any file with the given extension. - fn has_file_with_extension(dir: &Path, ext: &str) -> bool { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - if let Some(file_ext) = entry.path().extension() { - if file_ext - .to_str() - .map(|e| e.eq_ignore_ascii_case(ext)) - .unwrap_or(false) - { - return true; - } - } - } - } - false - } - - /// Detects languages by file extension (deep scan with a file count limit). - async fn detect_by_file_extensions( - workspace_path: &Path, - info: &mut ProjectInfo, - ) -> Result<()> { - let mut counts: HashMap = HashMap::new(); - let max_scan_files = 5000; - let mut scanned = 0; - - Self::scan_directory_iterative(workspace_path, &mut counts, &mut scanned, max_scan_files) - .await?; - - info.total_files = scanned; - - for (ext, count) in counts { - let language = Self::extension_to_language(&ext); - if language != "unknown" { - *info.file_counts.entry(language.clone()).or_insert(0) += count; - - let threshold = Self::language_threshold(&language); - if count >= threshold { - Self::add_language(info, &language); - - if count >= 10 { - if let Some(project_type) = Self::language_to_project_type(&language) { - Self::add_project_type(info, project_type); - } - } - } - } - } - - Ok(()) - } - - /// Mapping from language to project types. - fn language_to_project_type(language: &str) -> Option<&'static str> { - match language { - "rust" => Some("rust"), - "python" => Some("python"), - "go" => Some("go"), - "java" => Some("java"), - "kotlin" => Some("kotlin"), - "typescript" => Some("typescript"), - "javascript" => Some("nodejs"), - "cpp" | "c" => Some("cpp"), - "csharp" => Some("dotnet"), - "swift" => Some("swift"), - "ruby" => Some("ruby"), - "php" => Some("php"), - "scala" => Some("scala"), - _ => None, - } - } - - /// Returns the file-count threshold for a language. - fn language_threshold(language: &str) -> usize { - match language { - "rust" | "go" | "java" | "python" | "typescript" | "javascript" => 3, - "json5" | "yaml" | "toml" => 10, - _ => 5, - } - } - - /// Iteratively scans directories (avoids recursion depth limits). - async fn scan_directory_iterative( - root: &Path, - counts: &mut HashMap, - scanned: &mut usize, - max_files: usize, - ) -> Result<()> { - let mut dir_stack: Vec = vec![root.to_path_buf()]; - - while let Some(dir) = dir_stack.pop() { - if *scanned >= max_files { - break; - } - - let dir_name = dir.file_name().and_then(|n| n.to_str()).unwrap_or(""); - if matches!( - dir_name, - "node_modules" - | "target" - | ".git" - | "dist" - | "build" - | "out" - | "__pycache__" - | ".hvigor" - | "hvigor" - | "vendor" - | ".cargo" - | ".venv" - | "venv" - | "env" - | "screenshots" - | "signature" - ) { - continue; - } - - let mut entries = match fs::read_dir(&dir).await { - Ok(entries) => entries, - Err(_) => continue, - }; - - while let Some(entry) = entries.next_entry().await? { - if *scanned >= max_files { - break; - } - - let path = entry.path(); - let metadata = match entry.metadata().await { - Ok(m) => m, - Err(_) => continue, - }; - - if metadata.is_dir() { - dir_stack.push(path); - } else if metadata.is_file() { - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - *counts.entry(ext.to_lowercase()).or_insert(0) += 1; - } - *scanned += 1; - } - } - } - - Ok(()) - } - - /// Determines the primary language based on file counts. - fn determine_primary_language(info: &mut ProjectInfo) { - if info.primary_language.is_some() { - return; - } - - if info.file_counts.is_empty() { - return; - } - - let programming_languages: HashSet<&str> = [ - "rust", - "python", - "go", - "java", - "javascript", - "typescript", - "cpp", - "c", - "csharp", - "kotlin", - "swift", - "ruby", - "php", - "scala", - ] - .into_iter() - .collect(); - - let primary = info - .file_counts - .iter() - .filter(|(lang, _)| programming_languages.contains(lang.as_str())) - .max_by_key(|(_, count)| *count) - .map(|(lang, _)| lang.clone()); - - if let Some(lang) = primary { - info.primary_language = Some(lang.clone()); - } - } - - /// Deduplicates the language list. - fn deduplicate_languages(info: &mut ProjectInfo) { - let mut seen = HashSet::new(); - info.languages.retain(|lang| seen.insert(lang.clone())); - - let mut seen = HashSet::new(); - info.project_types.retain(|pt| seen.insert(pt.clone())); - } - - /// Adds a language (avoids duplicates). - fn add_language(info: &mut ProjectInfo, language: &str) { - if !info.languages.contains(&language.to_string()) { - info.languages.push(language.to_string()); - } - } - - /// Adds a project type (avoids duplicates). - fn add_project_type(info: &mut ProjectInfo, project_type: &str) { - if !info.project_types.contains(&project_type.to_string()) { - info.project_types.push(project_type.to_string()); - } - } - - /// Mapping from file extension to language. - fn extension_to_language(ext: &str) -> String { - match ext { - "json5" => "json5", - "ts" | "tsx" | "ets" => "typescript", - "js" | "jsx" | "mjs" | "cjs" => "javascript", - "rs" => "rust", - "py" | "pyw" => "python", - "go" => "go", - "java" => "java", - "c" | "h" => "c", - "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp", - "cs" => "csharp", - "rb" => "ruby", - "php" => "php", - "swift" => "swift", - "kt" | "kts" => "kotlin", - "scala" => "scala", - "sh" | "bash" => "shell", - _ => "unknown", - } - .to_string() - } - - /// Returns whether the server should be pre-started (based on project size). - pub fn should_prestart(info: &ProjectInfo) -> Vec { - let mut languages_to_start = Vec::new(); - - match info.total_files { - 0..=100 => { - languages_to_start.extend(info.languages.clone()); - debug!( - "Small project detected ({} files), will prestart all languages", - info.total_files - ); - } - 101..=1000 => { - if let Some(primary) = &info.primary_language { - languages_to_start.push(primary.clone()); - debug!( - "Medium project detected ({} files), will prestart primary language: {}", - info.total_files, primary - ); - } - } - _ => { - debug!( - "Large project detected ({} files), will use on-demand loading", - info.total_files - ); - } - } - - languages_to_start - } -} - -#[cfg(test)] -mod tests { - use super::ProjectDetector; - use tokio::fs; - - #[tokio::test] - async fn detects_rust_workspace_from_manifest_and_sources() { - let tempdir = tempfile::tempdir().expect("create tempdir"); - let root = tempdir.path(); - fs::write( - root.join("Cargo.toml"), - "[package]\nname = \"sample\"\nversion = \"0.1.0\"\n", - ) - .await - .expect("write manifest"); - fs::create_dir(root.join("src")) - .await - .expect("create src dir"); - fs::write(root.join("src/lib.rs"), "pub fn sample() {}\n") - .await - .expect("write rust source"); - - let info = ProjectDetector::detect(root).await.expect("detect project"); - - assert!(info.languages.contains(&"rust".to_string())); - assert!(info.project_types.contains(&"rust".to_string())); - } - - #[test] - fn prestarts_all_languages_for_small_projects() { - let info = super::ProjectInfo { - languages: vec!["rust".to_string(), "typescript".to_string()], - total_files: 20, - ..Default::default() - }; - - assert_eq!( - ProjectDetector::should_prestart(&info), - vec!["rust".to_string(), "typescript".to_string()] - ); - } -} diff --git a/src/crates/services/services-core/src/lsp/protocol.rs b/src/crates/services/services-core/src/lsp/protocol.rs deleted file mode 100644 index 67eaeea37e..0000000000 --- a/src/crates/services/services-core/src/lsp/protocol.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! LSP protocol handling -//! -//! Implements encoding and decoding of JSON-RPC messages. - -use anyhow::{anyhow, Result}; -use log::{error, warn}; -use tokio::io::{AsyncWriteExt, BufReader}; -use tokio::process::{ChildStdin, ChildStdout}; - -use bitfun_core_types::lsp::{ - JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, -}; - -/// Reads an LSP message. -/// -/// LSP uses HTTP-style headers: -/// Content-Length: xxx\r\n -/// \r\n -/// {json content} -pub async fn read_message(reader: &mut BufReader) -> Result { - let mut content_length: Option = None; - let mut line_count = 0; - let mut empty_line_count = 0; - let mut found_lsp_header = false; - - const MAX_LINES: usize = 100; - const MAX_EMPTY_LINES: usize = 50; - - loop { - let mut raw_line = Vec::new(); - let _bytes_read = - tokio::io::AsyncBufReadExt::read_until(reader, b'\n', &mut raw_line).await?; - line_count += 1; - - let header = match String::from_utf8(raw_line.clone()) { - Ok(s) => s, - Err(e) => { - warn!( - "[LSP Protocol] Line {} contains non-UTF8 data: {:?}", - line_count, e - ); - - String::from_utf8_lossy(&raw_line).to_string() - } - }; - - let header = header.trim(); - - if line_count > MAX_LINES { - return Err(anyhow!( - "Protocol error: Read {} lines without finding valid LSP header. \ - The LSP server may be outputting non-protocol data to stdout. \ - Check server stderr logs for details.", - line_count - )); - } - - if !found_lsp_header && header.is_empty() { - empty_line_count += 1; - - if empty_line_count > MAX_EMPTY_LINES { - return Err(anyhow!( - "Protocol error: Skipped {} empty lines without finding LSP header. \ - The LSP server stdout may be misconfigured. \ - Ensure the server only outputs LSP protocol messages to stdout.", - empty_line_count - )); - } - - if empty_line_count <= 10 || empty_line_count % 10 == 0 { - warn!( - "[LSP Protocol] Skipped {} empty lines, still waiting for LSP header (will fail after {} empty lines)", - empty_line_count, - MAX_EMPTY_LINES - ); - } - continue; - } - - if found_lsp_header && header.is_empty() { - break; - } - - if header.starts_with("Content-Length:") { - found_lsp_header = true; - let length_str = header - .strip_prefix("Content-Length:") - .ok_or_else(|| anyhow!("Invalid Content-Length header"))? - .trim(); - content_length = Some(length_str.parse()?); - } else if header.starts_with("Content-Type:") { - found_lsp_header = true; - } else if !header.is_empty() { - if found_lsp_header { - warn!("[LSP Protocol] Unexpected header line: {:?}", header); - } else { - if line_count <= 10 { - warn!("[LSP Protocol] Non-LSP output (skipping): {:?}", header); - } - } - } - } - - let content_length = content_length.ok_or_else(|| { - error!( - "[LSP Protocol] Missing Content-Length header after {} lines", - line_count - ); - anyhow!("Missing Content-Length header") - })?; - - let mut buffer = vec![0u8; content_length]; - tokio::io::AsyncReadExt::read_exact(reader, &mut buffer).await?; - - let message: JsonRpcMessage = serde_json::from_slice(&buffer).map_err(|e| { - let content_preview = String::from_utf8_lossy(&buffer); - let preview = if content_preview.len() > 500 { - let pos = content_preview - .char_indices() - .take_while(|(i, _)| *i < 500) - .last() - .map(|(i, c)| i + c.len_utf8()) - .unwrap_or(0); - format!("{}...", &content_preview[..pos]) - } else { - content_preview.to_string() - }; - error!("[LSP Protocol] Failed to parse JSON: {}", e); - error!("[LSP Protocol] Content preview: {}", preview); - anyhow!("Failed to parse JSON: {}", e) - })?; - - Ok(message) -} - -/// Writes an LSP message. -pub async fn write_message(writer: &mut ChildStdin, message: &JsonRpcMessage) -> Result<()> { - let content = serde_json::to_string(message)?; - let content_bytes = content.as_bytes(); - - let header = format!("Content-Length: {}\r\n\r\n", content_bytes.len()); - - writer.write_all(header.as_bytes()).await?; - writer.write_all(content_bytes).await?; - writer.flush().await?; - - Ok(()) -} - -/// Creates a request message. -pub fn create_request( - id: u64, - method: impl Into, - params: Option, -) -> JsonRpcMessage { - JsonRpcMessage::Request(JsonRpcRequest { - jsonrpc: "2.0".to_string(), - id, - method: method.into(), - params, - }) -} - -/// Creates a notification message. -pub fn create_notification( - method: impl Into, - params: Option, -) -> JsonRpcMessage { - JsonRpcMessage::Notification(JsonRpcNotification { - jsonrpc: "2.0".to_string(), - method: method.into(), - params, - }) -} - -/// Extracts the result from a response. -pub fn extract_result(response: JsonRpcResponse) -> Result { - if let Some(error) = response.error { - return Err(anyhow!("LSP Error {}: {}", error.code, error.message)); - } - - response - .result - .ok_or_else(|| anyhow!("Missing result in response")) -} - -#[cfg(test)] -mod tests { - use super::{create_notification, create_request, extract_result}; - use bitfun_core_types::lsp::{JsonRpcMessage, JsonRpcResponse}; - use serde_json::json; - - #[test] - fn creates_request_and_notification_messages() { - assert!(matches!( - create_request(7, "textDocument/hover", Some(json!({ "uri": "file:///a.rs" }))), - JsonRpcMessage::Request(request) - if request.id == 7 && request.method == "textDocument/hover" - )); - - assert!(matches!( - create_notification("textDocument/didOpen", None), - JsonRpcMessage::Notification(notification) - if notification.method == "textDocument/didOpen" - )); - } - - #[test] - fn extracts_response_result_without_losing_payload() { - let result = json!({ "capabilities": { "hoverProvider": true } }); - let response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: 1, - result: Some(result.clone()), - error: None, - }; - - assert_eq!(extract_result(response).expect("extract result"), result); - } -} diff --git a/src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs b/src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs deleted file mode 100644 index 53e787312a..0000000000 --- a/src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs +++ /dev/null @@ -1,186 +0,0 @@ -#![cfg(feature = "lsp")] - -use bitfun_core_types::lsp::{CapabilitiesConfig, LspPlugin, ServerConfig}; -use bitfun_services_core::lsp::{ - resolve_plugin_command_for_target, LspPluginRegistryError, LspPluginRuntimeArch, - LspPluginRuntimePlatform, LspPluginRuntimeTarget, PluginRegistry, -}; -use std::collections::HashMap; - -fn plugin(id: &str, languages: &[&str], extensions: &[&str]) -> LspPlugin { - LspPlugin { - id: id.to_string(), - name: id.to_string(), - version: "1.0.0".to_string(), - author: "BitFun".to_string(), - description: "test plugin".to_string(), - server: ServerConfig { - command: "server".to_string(), - args: vec![], - env: HashMap::new(), - runtime: None, - }, - languages: languages.iter().map(|value| value.to_string()).collect(), - file_extensions: extensions.iter().map(|value| value.to_string()).collect(), - capabilities: CapabilitiesConfig { - completion: true, - hover: false, - definition: false, - references: false, - rename: false, - formatting: false, - diagnostics: false, - inlay_hints: false, - }, - settings: HashMap::new(), - checksum: String::new(), - min_bitfun_version: String::new(), - } -} - -#[test] -fn registry_preserves_language_extension_and_file_path_lookup() { - let mut registry = PluginRegistry::new(); - registry - .register(plugin("rust", &["rust"], &[".rs"])) - .expect("plugin should register"); - - assert_eq!(registry.count(), 1); - assert!(registry.is_registered("rust")); - assert_eq!(registry.find_by_language("rust").unwrap().id, "rust"); - assert_eq!(registry.find_by_extension("rs").unwrap().id, "rust"); - assert_eq!(registry.find_by_extension(".rs").unwrap().id, "rust"); - assert_eq!( - registry - .find_by_file_path("workspace/src/main.rs") - .unwrap() - .id, - "rust" - ); -} - -#[test] -fn registry_unregister_removes_plugin_indexes() { - let mut registry = PluginRegistry::new(); - registry - .register(plugin("python", &["python"], &[".py"])) - .expect("plugin should register"); - - registry - .unregister("python") - .expect("plugin should unregister"); - - assert_eq!(registry.count(), 0); - assert!(!registry.is_registered("python")); - assert!(registry.find_by_language("python").is_none()); - assert!(registry.find_by_extension("py").is_none()); -} - -#[test] -fn registry_unregister_preserves_indexes_owned_by_newer_plugin() { - let mut registry = PluginRegistry::new(); - registry - .register(plugin("legacy-rust", &["rust"], &[".rs"])) - .expect("legacy plugin should register"); - registry - .register(plugin("current-rust", &["rust"], &[".rs"])) - .expect("current plugin should register"); - - assert_eq!( - registry.find_by_language("rust").unwrap().id, - "current-rust" - ); - assert_eq!(registry.find_by_extension("rs").unwrap().id, "current-rust"); - - registry - .unregister("legacy-rust") - .expect("legacy plugin should unregister without clearing current indexes"); - - assert_eq!(registry.count(), 1); - assert_eq!( - registry.find_by_language("rust").unwrap().id, - "current-rust" - ); - assert_eq!(registry.find_by_extension("rs").unwrap().id, "current-rust"); -} - -#[test] -fn registry_duplicate_and_missing_errors_keep_legacy_messages() { - let mut registry = PluginRegistry::new(); - registry - .register(plugin("rust", &["rust"], &[".rs"])) - .expect("plugin should register"); - - assert_eq!( - registry.register(plugin("rust", &["rust"], &[".rs"])), - Err(LspPluginRegistryError::AlreadyRegistered( - "rust".to_string() - )) - ); - assert_eq!( - registry - .register(plugin("rust", &["rust"], &[".rs"])) - .unwrap_err() - .to_string(), - "Plugin already registered: rust" - ); - assert_eq!( - registry.unregister("missing"), - Err(LspPluginRegistryError::NotFound("missing".to_string())) - ); - assert_eq!( - registry.unregister("missing").unwrap_err().to_string(), - "Plugin not found: missing" - ); -} - -#[test] -fn registry_supported_extensions_matches_desktop_api_shape() { - let mut registry = PluginRegistry::new(); - registry - .register(plugin("rust", &["rust"], &[".rs"])) - .expect("rust plugin should register"); - registry - .register(plugin( - "typescript", - &["typescript", "javascript"], - &[".ts", ".tsx"], - )) - .expect("typescript plugin should register"); - - let summary = registry.supported_extensions(); - - assert_eq!(summary.extension_to_language.get(".rs").unwrap(), "rust"); - assert_eq!( - summary.extension_to_language.get(".ts").unwrap(), - "typescript" - ); - assert_eq!( - summary.extension_to_language.get(".tsx").unwrap(), - "typescript" - ); - assert!(summary - .supported_languages - .iter() - .any(|language| language == "rust")); - assert!(summary - .supported_languages - .iter() - .any(|language| language == "typescript")); - assert!(summary - .supported_languages - .iter() - .any(|language| language == "javascript")); -} - -#[test] -fn plugin_command_placeholder_resolution_is_target_driven() { - let command = "bin/${platform}/${os}/${arch}/server"; - let target = - LspPluginRuntimeTarget::new(LspPluginRuntimePlatform::Macos, LspPluginRuntimeArch::Arm64); - - assert_eq!( - resolve_plugin_command_for_target(command, target), - "bin/darwin/darwin/arm64/server" - ); -} diff --git a/src/web-ui/LOGGING.md b/src/web-ui/LOGGING.md index 3d4a81e69f..e9419f5b93 100644 --- a/src/web-ui/LOGGING.md +++ b/src/web-ui/LOGGING.md @@ -64,7 +64,7 @@ log.error('Failed to load', { location: 'initializeApp', error }); Examples: ``` [WorkspaceManager] Loading workspace configuration -[LspService] Failed to start server {"code":"ENOENT"}, Error: spawn node ENOENT +[FileSystemService] Failed to read workspace file {"code":"ENOENT"}, Error: file not found at ChildProcess._handle.onexit (...) ``` diff --git a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx index 0402849910..35447da0e8 100644 --- a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx +++ b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx @@ -595,11 +595,9 @@ const FlexiblePanel: React.FC = memo(({ originalContent={originalCode} modifiedContent={modifiedCode} filePath={diffFilePath} - workspacePath={workspacePath || diffMigrationContext?.workspacePath} revealLine={diffData.revealLine} readOnly={false} renderSideBySide={true} - enableLsp={true} onSave={async (content) => { try { const targetWorkspacePath = workspacePath || diffMigrationContext?.workspacePath; diff --git a/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx b/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx index 60cab372b6..555ca23ade 100644 --- a/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx +++ b/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx @@ -188,7 +188,6 @@ export const CodeEditor: React.FC = ({ readOnly={readOnly} showLineNumbers={lineNumbers !== 'off'} showMinimap={minimap} - enableLsp={false} onContentChange={handleContentChange} onEditorReady={handleEditorReady} /> diff --git a/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts b/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts index 0e26e8d199..9369ac345e 100644 --- a/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts +++ b/src/web-ui/src/component-library/styles/overlay-surfaces.contract.test.ts @@ -211,7 +211,6 @@ describe('overlay surface contracts', () => { 'flow_chat/components/ModelSelector.scss', 'app/components/NavPanel/NavPanel.scss', 'app/layout/FloatingMiniChat.scss', - 'tools/lsp/components/ReferencesPanel/ReferencesPanel.scss', ])('%s consumes FloatingSurface instead of owning popup chrome', (path) => { expect(readSource(path)).toContain('@include surfaces.floating-surface'); }); diff --git a/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.test.ts b/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.test.ts index 522c3dfd1b..dedb534079 100644 --- a/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.test.ts +++ b/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.test.ts @@ -128,7 +128,6 @@ describe('peerInvokePriorityFor', () => { expect(peerInvokePriorityFor('git_is_repository')).toBe('low'); expect(peerInvokePriorityFor('ssh_is_connected')).toBe('low'); expect(peerInvokePriorityFor('get_file_metadata')).toBe('low'); - expect(peerInvokePriorityFor('lsp_detect_project')).toBe('low'); expect(peerInvokePriorityFor('search_get_repo_status')).toBe('low'); expect(peerInvokePriorityFor('load_canvas_artifact')).toBe('low'); expect(peerInvokePriorityFor('get_file_tree')).toBe('low'); diff --git a/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.ts b/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.ts index 95ac819c98..24905a7b2d 100644 --- a/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.ts +++ b/src/web-ui/src/infrastructure/api/adapters/peer-device-adapter.ts @@ -293,7 +293,6 @@ export function peerInvokePriorityFor(command: string): PeerInvokePriority { if ( command.startsWith('git_') || command.startsWith('ssh_') || - command.startsWith('lsp_') || command.startsWith('search_') || command.startsWith('explorer_') || command.startsWith('miniapp_') || diff --git a/src/web-ui/src/infrastructure/appearance/registry/defaultAppearanceRegistry.ts b/src/web-ui/src/infrastructure/appearance/registry/defaultAppearanceRegistry.ts index 548d532d15..34a76241ce 100644 --- a/src/web-ui/src/infrastructure/appearance/registry/defaultAppearanceRegistry.ts +++ b/src/web-ui/src/infrastructure/appearance/registry/defaultAppearanceRegistry.ts @@ -75,7 +75,6 @@ import { createPlanDisplayAppearanceDescriptor } from '@/flow_chat/tool-cards/Cr import { execProcessToolCardAppearanceDescriptor } from '@/flow_chat/tool-cards/ExecProcessToolCardView.appearance'; import { editorConfigAppearanceDescriptor } from '@/infrastructure/config/components/EditorConfig.appearance'; import { appearanceSettingsAppearanceDescriptor } from '@/infrastructure/config/components/AppearanceSettingsPage.appearance'; -import { lspConfigAppearanceDescriptor } from '@/infrastructure/config/components/LspConfig.appearance'; import { configAppearanceDescriptor } from '@/infrastructure/config/appearance'; import { fontPreferenceAppearanceDescriptor } from '@/infrastructure/font-preference/appearance'; import { languageSelectorAppearanceDescriptor } from '@/infrastructure/i18n/appearance'; @@ -92,7 +91,6 @@ import { generativeWidgetAppearanceDescriptor } from '@/tools/generative-widget/ import { editorToolAppearanceDescriptor } from '@/tools/editor/appearance'; import { fileSystemAppearanceDescriptor } from '@/tools/file-system/appearance'; import { gitToolAppearanceDescriptor } from '@/tools/git/appearance'; -import { lspAppearanceDescriptor } from '@/tools/lsp/appearance'; import { terminalToolAppearanceDescriptor } from '@/tools/terminal/appearance'; import { workspaceToolAppearanceDescriptor } from '@/tools/workspace/appearance'; import { relayDeployAppearanceDescriptor } from '@/features/relay-deploy/appearance'; @@ -175,7 +173,6 @@ import { gitSettingsViewAppearanceDescriptor } from '@/tools/git/components/GitS import { quickActionsConfigAppearanceDescriptor } from '@/infrastructure/config/components/QuickActionsConfig.appearance'; import { statusBarPopoversAppearanceDescriptor } from '@/tools/editor/components/StatusBarPopovers/StatusBarPopovers.appearance'; import { mEditorAppearanceDescriptor } from '@/tools/editor/meditor/components/MEditor.appearance'; -import { referencesPanelAppearanceDescriptor } from '@/tools/lsp/components/ReferencesPanel/ReferencesPanel.appearance'; import { globalSearchAppearanceDescriptor } from '@/app/global-search/GlobalSearchRoot.appearance'; import { workspaceRelatedPathsDialogAppearanceDescriptor } from '@/app/components/NavPanel/sections/workspaces/WorkspaceRelatedPathsDialog.appearance'; import { branchSelectModalAppearanceDescriptor } from '@/app/components/panels/BranchSelectModal.appearance'; @@ -373,7 +370,6 @@ export function createDefaultAppearanceRegistry(): AppearanceRegistry { .registerComponent(execProcessToolCardAppearanceDescriptor) .registerComponent(editorConfigAppearanceDescriptor) .registerComponent(appearanceSettingsAppearanceDescriptor) - .registerComponent(lspConfigAppearanceDescriptor) .registerComponent(configAppearanceDescriptor) .registerComponent(fontPreferenceAppearanceDescriptor) .registerComponent(languageSelectorAppearanceDescriptor) @@ -395,7 +391,6 @@ export function createDefaultAppearanceRegistry(): AppearanceRegistry { .registerComponent(editorToolAppearanceDescriptor) .registerComponent(fileSystemAppearanceDescriptor) .registerComponent(gitToolAppearanceDescriptor) - .registerComponent(lspAppearanceDescriptor) .registerComponent(terminalToolAppearanceDescriptor) .registerComponent(workspaceToolAppearanceDescriptor) .registerComponent(gitGraphViewAppearanceDescriptor) @@ -477,7 +472,6 @@ export function createDefaultAppearanceRegistry(): AppearanceRegistry { .registerComponent(quickActionsConfigAppearanceDescriptor) .registerComponent(statusBarPopoversAppearanceDescriptor) .registerComponent(mEditorAppearanceDescriptor) - .registerComponent(referencesPanelAppearanceDescriptor) .registerComponent(globalSearchAppearanceDescriptor) .registerComponent(workspaceRelatedPathsDialogAppearanceDescriptor) .registerComponent(branchSelectModalAppearanceDescriptor) diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.appearance.ts b/src/web-ui/src/infrastructure/config/components/LspConfig.appearance.ts deleted file mode 100644 index 2a793b62c5..0000000000 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.appearance.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance'; - -export const lspConfigAppearanceDescriptor: AppearanceSurfaceDescriptor = { - id: 'lsp-config', - parts: [{ id: 'root' }, { id: 'message' }, { id: 'plugins' }], -}; diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.scss b/src/web-ui/src/infrastructure/config/components/LspConfig.scss deleted file mode 100644 index 78ff257fdf..0000000000 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.scss +++ /dev/null @@ -1,13 +0,0 @@ -@use '../../../component-library/styles/tokens' as *; - -.bitfun-lsp-config { - &__message-container { - padding: $size-gap-4 $size-gap-4 0; - animation: bitfun-collection-slide-down 0.2s $easing-standard; - } - - &__plugins { - overflow: hidden; - background: transparent; - } -} diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx deleted file mode 100644 index 738244e458..0000000000 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Alert, Switch, IconButton } from '@/component-library'; -import { Save, X, RefreshCw, Upload } from 'lucide-react'; -import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow } from './common'; -import { LspPluginList } from '@/tools/lsp'; -import { lspService } from '@/tools/lsp/services/LspService'; -import { open } from '@tauri-apps/plugin-dialog'; -import { createLogger } from '@/shared/utils/logger'; -import './LspConfig.scss'; - -const log = createLogger('LspConfig'); - -interface LspSettings { - autoStartEnabled: boolean; -} - -const DEFAULT_LSP_SETTINGS: LspSettings = { autoStartEnabled: true }; -const LSP_SETTINGS_KEY = 'bitfun_lsp_settings'; - -const LspConfig: React.FC = () => { - const { t } = useTranslation('settings/lsp'); - const pluginReloadRef = useRef<(() => void) | null>(null); - const [isInstalling, setIsInstalling] = useState(false); - const [installMessage, setInstallMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); - const [isInitializing, setIsInitializing] = useState(false); - const [settings, setSettings] = useState(DEFAULT_LSP_SETTINGS); - const [hasSettingsChanges, setHasSettingsChanges] = useState(false); - - function loadSettings() { - try { - const saved = localStorage.getItem(LSP_SETTINGS_KEY); - if (saved) setSettings({ ...DEFAULT_LSP_SETTINGS, ...JSON.parse(saved) }); - } catch (error) { - log.error('Failed to load settings', error); - } - } - - useEffect(() => { loadSettings(); }, []); - - const saveSettings = () => { - try { - localStorage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); - setHasSettingsChanges(false); - setInstallMessage({ type: 'success', text: t('messages.settingsSaved') }); - setTimeout(() => setInstallMessage(null), 2000); - } catch (error) { - log.error('Failed to save settings', error); - setInstallMessage({ type: 'error', text: t('messages.saveSettingsFailed') }); - } - }; - - const handleSettingChange = (key: keyof LspSettings, value: boolean) => { - setSettings(prev => ({ ...prev, [key]: value })); - setHasSettingsChanges(true); - }; - - const handleInitialize = async () => { - setIsInitializing(true); - setInstallMessage(null); - try { - await lspService.initialize(); - setInstallMessage({ type: 'success', text: t('messages.initSuccess') }); - } catch (error) { - setInstallMessage({ type: 'error', text: `${t('messages.initFailed')}: ${error instanceof Error ? error.message : String(error)}` }); - } finally { - setIsInitializing(false); - } - }; - - const handleInstallPlugin = async () => { - try { - const selected = await open({ - multiple: false, - filters: [{ name: t('fileDialog.pluginPackage'), extensions: ['vcpkg'] }] - }); - if (!selected) return; - - setIsInstalling(true); - setInstallMessage(null); - await lspService.initialize(); - const pluginId = await lspService.installPlugin(selected as string); - setInstallMessage({ type: 'success', text: t('messages.installSuccess', { pluginId }) }); - setTimeout(() => setInstallMessage(null), 3000); - } catch (error) { - setInstallMessage({ type: 'error', text: `${t('messages.installFailed')}: ${error instanceof Error ? error.message : String(error)}` }); - } finally { - setIsInstalling(false); - } - }; - - const saveActions = hasSettingsChanges ? ( - <> - - - - - - - - ) : null; - - return ( - - - - - {installMessage && ( -
- -
- )} - - - - handleSettingChange('autoStartEnabled', e.target.checked)} - size="medium" - /> - - - - - - - - - - - pluginReloadRef.current?.()} - tooltip={t('pluginList.refreshTooltip')} - > - - - - } - > -
- { pluginReloadRef.current = fn; }} - /> -
-
-
-
- ); -}; - -export default LspConfig; diff --git a/src/web-ui/src/infrastructure/i18n/core/I18nService.ts b/src/web-ui/src/infrastructure/i18n/core/I18nService.ts index ea4279c3c3..f99aba3177 100644 --- a/src/web-ui/src/infrastructure/i18n/core/I18nService.ts +++ b/src/web-ui/src/infrastructure/i18n/core/I18nService.ts @@ -49,7 +49,6 @@ const bootstrapLocaleModules = import.meta.glob([ '../../../locales/*/panels/files.json', '../../../locales/*/panels/git.json', '../../../locales/*/settings/models.json', - '../../../locales/*/settings/lsp.json', '../../../locales/*/tools.json', ], { eager: true, diff --git a/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts b/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts index 9ab7953355..f981201b14 100644 --- a/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts +++ b/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts @@ -32,7 +32,6 @@ export const ALL_NAMESPACES = [ 'settings/editor', 'settings/external-apps', 'settings/hooks', - 'settings/lsp', 'settings/mcp', 'settings/mcp-tools', 'settings/memory', @@ -56,7 +55,6 @@ export const WEB_UI_BOOTSTRAP_NAMESPACES = [ 'panels/files', 'panels/git', 'settings/models', - 'settings/lsp', 'shared', 'tools', ] as const satisfies readonly (typeof ALL_NAMESPACES)[number][]; diff --git a/src/web-ui/src/infrastructure/language-detection/core/LanguageDetector.ts b/src/web-ui/src/infrastructure/language-detection/core/LanguageDetector.ts index d9c2b4969f..bea68709e6 100644 --- a/src/web-ui/src/infrastructure/language-detection/core/LanguageDetector.ts +++ b/src/web-ui/src/infrastructure/language-detection/core/LanguageDetector.ts @@ -4,8 +4,7 @@ import type { Language, DetectionContext, FileDetectionResult, - LanguagePlugin, - LspConfig + LanguagePlugin } from '../types'; import { languageRegistry } from './LanguageRegistry'; import { createLogger } from '@/shared/utils/logger'; @@ -17,62 +16,6 @@ const log = createLogger('LanguageDetector'); // ============================================================================ -const LSP_CONFIG_MAP: Record = { - typescript: { - serverId: 'typescript-language-server', - enabled: true, - }, - javascript: { - serverId: 'typescript-language-server', - enabled: true, - }, - python: { - serverId: 'pylsp', - enabled: true, - }, - rust: { - serverId: 'rust-analyzer', - enabled: true, - }, - go: { - serverId: 'gopls', - enabled: true, - }, - java: { - serverId: 'jdtls', - enabled: true, - }, - html: { - serverId: 'vscode-html-language-server', - enabled: true, - }, - css: { - serverId: 'vscode-css-language-server', - enabled: true, - }, - json: { - serverId: 'vscode-json-language-server', - enabled: true, - }, - yaml: { - serverId: 'yaml-language-server', - enabled: true, - }, - vue: { - serverId: 'vue-language-server', - enabled: true, - }, - svelte: { - serverId: 'svelte-language-server', - enabled: true, - }, -}; - -// ============================================================================ - -// ============================================================================ - - class LanguageDetector { private static instance: LanguageDetector; @@ -178,7 +121,7 @@ class LanguageDetector { } if (byExtension.length > 1) { - const primary = this.resolvePrimaryLanguage(byExtension, context); + const primary = this.resolvePrimaryLanguage(byExtension); return { language: primary, confidence: 0.8, @@ -223,17 +166,7 @@ class LanguageDetector { } - private resolvePrimaryLanguage(candidates: Language[], context: DetectionContext): Language { - - if (context.projectInfo) { - const projectLang = context.projectInfo.primaryLanguage; - const matching = candidates.find(c => c.id === projectLang || c.parent === projectLang); - if (matching) { - return matching; - } - } - - + private resolvePrimaryLanguage(candidates: Language[]): Language { const primary = candidates.find(c => !c.parent); if (primary) { return primary; @@ -263,17 +196,6 @@ class LanguageDetector { } - public hasLspSupport(filePathOrName: string): boolean { - const lang = this.detectFile(filePathOrName).language; - return !!lang.lspId && !!LSP_CONFIG_MAP[lang.id]; - } - - - public getLspConfig(languageId: string): LspConfig | undefined { - return LSP_CONFIG_MAP[languageId]; - } - - public getLanguage(id: string): Language | undefined { return languageRegistry.getById(id); } @@ -349,9 +271,6 @@ class LanguageDetector { if (context?.hints?.language) { key += `|hint:${context.hints.language}`; } - if (context?.projectInfo?.type) { - key += `|project:${context.projectInfo.type}`; - } return key; } diff --git a/src/web-ui/src/infrastructure/language-detection/core/LanguageRegistry.ts b/src/web-ui/src/infrastructure/language-detection/core/LanguageRegistry.ts index 721ab5ac7c..cbef9c62a0 100644 --- a/src/web-ui/src/infrastructure/language-detection/core/LanguageRegistry.ts +++ b/src/web-ui/src/infrastructure/language-detection/core/LanguageRegistry.ts @@ -22,7 +22,6 @@ const BUILTIN_LANGUAGES: Language[] = [ extensions: ['ts', 'mts', 'cts', 'ets'], monacoId: 'typescript', prismId: 'typescript', - lspId: 'typescript', iconType: 'typescript', color: APPEARANCE_LANGUAGE_TOKENS.typescript, aliases: ['ts'], @@ -37,7 +36,6 @@ const BUILTIN_LANGUAGES: Language[] = [ extensions: ['tsx'], monacoId: 'typescript', prismId: 'tsx', - lspId: 'typescriptreact', iconType: 'react', color: APPEARANCE_LANGUAGE_TOKENS.typescriptReact, aliases: ['tsx'], @@ -53,7 +51,6 @@ const BUILTIN_LANGUAGES: Language[] = [ extensions: ['js', 'mjs', 'cjs'], monacoId: 'javascript', prismId: 'javascript', - lspId: 'javascript', iconType: 'javascript', color: APPEARANCE_LANGUAGE_TOKENS.javascript, aliases: ['js', 'es6', 'ecmascript'], @@ -68,7 +65,6 @@ const BUILTIN_LANGUAGES: Language[] = [ extensions: ['jsx'], monacoId: 'javascript', prismId: 'jsx', - lspId: 'javascriptreact', iconType: 'react', color: APPEARANCE_LANGUAGE_TOKENS.javascriptReact, aliases: ['jsx'], @@ -83,7 +79,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['py', 'pyw', 'pyi'], monacoId: 'python', - lspId: 'python', iconType: 'python', color: APPEARANCE_LANGUAGE_TOKENS.python, aliases: ['py', 'python3'], @@ -97,7 +92,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['rs'], monacoId: 'rust', - lspId: 'rust', iconType: 'rust', color: APPEARANCE_LANGUAGE_TOKENS.rust, supportsComments: true, @@ -110,7 +104,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['go'], monacoId: 'go', - lspId: 'go', iconType: 'go', color: APPEARANCE_LANGUAGE_TOKENS.go, aliases: ['golang'], @@ -124,7 +117,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['java'], monacoId: 'java', - lspId: 'java', iconType: 'java', color: APPEARANCE_LANGUAGE_TOKENS.java, supportsComments: true, @@ -137,7 +129,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['kt', 'kts'], monacoId: 'kotlin', - lspId: 'kotlin', iconType: 'kotlin', color: APPEARANCE_LANGUAGE_TOKENS.kotlin, supportsComments: true, @@ -150,7 +141,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['cpp', 'cxx', 'cc', 'c++', 'hpp', 'hxx', 'hh', 'h++'], monacoId: 'cpp', - lspId: 'cpp', iconType: 'c-cpp', color: APPEARANCE_LANGUAGE_TOKENS.cpp, aliases: ['c++', 'cplusplus'], @@ -164,7 +154,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['c', 'h'], monacoId: 'c', - lspId: 'c', iconType: 'c-cpp', color: APPEARANCE_LANGUAGE_TOKENS.c, supportsComments: true, @@ -177,7 +166,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['cs', 'csx'], monacoId: 'csharp', - lspId: 'csharp', iconType: 'csharp', color: APPEARANCE_LANGUAGE_TOKENS.csharp, aliases: ['c#', 'dotnet'], @@ -191,7 +179,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['swift'], monacoId: 'swift', - lspId: 'swift', iconType: 'swift', color: APPEARANCE_LANGUAGE_TOKENS.swift, supportsComments: true, @@ -204,7 +191,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['php', 'phtml', 'php3', 'php4', 'php5', 'phps'], monacoId: 'php', - lspId: 'php', iconType: 'php', color: APPEARANCE_LANGUAGE_TOKENS.php, supportsComments: true, @@ -218,7 +204,6 @@ const BUILTIN_LANGUAGES: Language[] = [ extensions: ['rb', 'rbw', 'rake', 'gemspec'], filenames: ['Rakefile', 'Gemfile'], monacoId: 'ruby', - lspId: 'ruby', iconType: 'ruby', color: APPEARANCE_LANGUAGE_TOKENS.ruby, supportsComments: true, @@ -231,7 +216,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['scala', 'sc'], monacoId: 'scala', - lspId: 'scala', iconType: 'scala', color: APPEARANCE_LANGUAGE_TOKENS.scala, supportsComments: true, @@ -244,7 +228,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['dart'], monacoId: 'dart', - lspId: 'dart', iconType: 'dart', color: APPEARANCE_LANGUAGE_TOKENS.dart, supportsComments: true, @@ -257,7 +240,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['lua'], monacoId: 'lua', - lspId: 'lua', iconType: 'lua', color: APPEARANCE_LANGUAGE_TOKENS.lua, supportsComments: true, @@ -270,7 +252,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'programming', extensions: ['r', 'R', 'rmd'], monacoId: 'r', - lspId: 'r', iconType: 'r', color: APPEARANCE_LANGUAGE_TOKENS.r, supportsComments: true, @@ -286,7 +267,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'markup', extensions: ['html', 'htm', 'xhtml', 'shtml'], monacoId: 'html', - lspId: 'html', iconType: 'html', color: APPEARANCE_LANGUAGE_TOKENS.html, supportsComments: true, @@ -309,7 +289,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'markup', extensions: ['vue'], monacoId: 'vue', - lspId: 'vue', iconType: 'vue', color: APPEARANCE_LANGUAGE_TOKENS.vue, supportsComments: true, @@ -321,7 +300,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'markup', extensions: ['svelte'], monacoId: 'html', - lspId: 'svelte', iconType: 'svelte', color: APPEARANCE_LANGUAGE_TOKENS.svelte, supportsComments: true, @@ -337,7 +315,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'stylesheet', extensions: ['css'], monacoId: 'css', - lspId: 'css', iconType: 'css', color: APPEARANCE_LANGUAGE_TOKENS.css, supportsComments: true, @@ -349,7 +326,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'stylesheet', extensions: ['scss'], monacoId: 'scss', - lspId: 'scss', iconType: 'sass', color: APPEARANCE_LANGUAGE_TOKENS.scss, parent: 'css', @@ -375,7 +351,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'stylesheet', extensions: ['less'], monacoId: 'less', - lspId: 'less', iconType: 'less', color: APPEARANCE_LANGUAGE_TOKENS.less, parent: 'css', @@ -404,7 +379,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'data', extensions: ['yaml', 'yml'], monacoId: 'yaml', - lspId: 'yaml', iconType: 'yaml', color: APPEARANCE_LANGUAGE_TOKENS.yaml, supportsComments: true, @@ -428,7 +402,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'data', extensions: ['sql', 'mysql', 'pgsql', 'sqlite'], monacoId: 'sql', - lspId: 'sql', iconType: 'database', color: APPEARANCE_LANGUAGE_TOKENS.sql, supportsComments: true, @@ -509,7 +482,6 @@ const BUILTIN_LANGUAGES: Language[] = [ extensions: ['sh', 'bash', 'zsh', 'fish'], firstLineMatch: /^#!.*\b(bash|sh|zsh|fish)\b/, monacoId: 'shell', - lspId: 'shellscript', iconType: 'shell', color: APPEARANCE_LANGUAGE_TOKENS.shell, aliases: ['bash', 'zsh'], @@ -522,7 +494,6 @@ const BUILTIN_LANGUAGES: Language[] = [ category: 'script', extensions: ['ps1', 'psm1', 'psd1'], monacoId: 'powershell', - lspId: 'powershell', iconType: 'powershell', color: APPEARANCE_LANGUAGE_TOKENS.powershell, supportsComments: true, diff --git a/src/web-ui/src/infrastructure/language-detection/core/ProjectDetector.ts b/src/web-ui/src/infrastructure/language-detection/core/ProjectDetector.ts deleted file mode 100644 index 2ec9fa63e2..0000000000 --- a/src/web-ui/src/infrastructure/language-detection/core/ProjectDetector.ts +++ /dev/null @@ -1,500 +0,0 @@ - - -import type { - ProjectInfo, - ProjectType, - FrameworkInfo, - DetectionContext, - ProjectDetectionPlugin -} from '../types'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('ProjectDetector'); - -// ============================================================================ - -// ============================================================================ - - -interface ProjectMarker { - - configFile: string; - - - projectType: ProjectType; - - - primaryLanguage: string; - - - packageManager?: ProjectInfo['packageManager']; - - - checkFields?: { - path: string[]; - detect: (value: unknown) => FrameworkInfo | null; - }[]; -} - - -const PROJECT_MARKERS: ProjectMarker[] = [ - - { - configFile: 'package.json', - projectType: 'nodejs', - primaryLanguage: 'javascript', - packageManager: 'npm', - checkFields: [ - { - path: ['dependencies', 'react'], - detect: (v) => v ? { name: 'react', version: String(v), source: 'package.json' } : null, - }, - { - path: ['dependencies', 'vue'], - detect: (v) => v ? { name: 'vue', version: String(v), source: 'package.json' } : null, - }, - { - path: ['dependencies', '@angular/core'], - detect: (v) => v ? { name: 'angular', version: String(v), source: 'package.json' } : null, - }, - { - path: ['dependencies', 'svelte'], - detect: (v) => v ? { name: 'svelte', version: String(v), source: 'package.json' } : null, - }, - { - path: ['dependencies', 'next'], - detect: (v) => v ? { name: 'nextjs', version: String(v), source: 'package.json' } : null, - }, - { - path: ['devDependencies', 'vite'], - detect: (v) => v ? { name: 'vite', version: String(v), source: 'package.json' } : null, - }, - { - path: ['devDependencies', '@tauri-apps/cli'], - detect: (v) => v ? { name: 'tauri', version: String(v), source: 'package.json' } : null, - }, - { - path: ['devDependencies', 'electron'], - detect: (v) => v ? { name: 'electron', version: String(v), source: 'package.json' } : null, - }, - ], - }, - - - { - configFile: 'Cargo.toml', - projectType: 'rust', - primaryLanguage: 'rust', - packageManager: 'cargo', - }, - { - configFile: 'pyproject.toml', - projectType: 'python', - primaryLanguage: 'python', - packageManager: 'poetry', - }, - { - configFile: 'requirements.txt', - projectType: 'python', - primaryLanguage: 'python', - packageManager: 'pip', - }, - { - configFile: 'setup.py', - projectType: 'python', - primaryLanguage: 'python', - packageManager: 'pip', - }, - { - configFile: 'go.mod', - projectType: 'go', - primaryLanguage: 'go', - packageManager: 'go', - }, - { - configFile: 'pom.xml', - projectType: 'maven', - primaryLanguage: 'java', - packageManager: 'maven', - }, - { - configFile: 'build.gradle', - projectType: 'gradle', - primaryLanguage: 'java', - packageManager: 'gradle', - }, - { - configFile: 'build.gradle.kts', - projectType: 'gradle', - primaryLanguage: 'kotlin', - packageManager: 'gradle', - }, - { - configFile: '*.csproj', - projectType: 'dotnet', - primaryLanguage: 'csharp', - }, - { - configFile: '*.sln', - projectType: 'dotnet', - primaryLanguage: 'csharp', - }, - { - configFile: 'pubspec.yaml', - projectType: 'flutter', - primaryLanguage: 'dart', - }, -]; - - -const MONOREPO_MARKERS = [ - 'pnpm-workspace.yaml', - 'lerna.json', - 'nx.json', - 'rush.json', - 'turbo.json', -]; - -// ============================================================================ - -// ============================================================================ - - -class ProjectDetector { - private static instance: ProjectDetector; - - - private cache = new Map(); - - - private plugins: ProjectDetectionPlugin[] = []; - - private constructor() {} - - - public static getInstance(): ProjectDetector { - if (!ProjectDetector.instance) { - ProjectDetector.instance = new ProjectDetector(); - } - return ProjectDetector.instance; - } - - // =========================================================================== - - // =========================================================================== - - - public async detect( - workspacePath: string, - options?: { - - useBackend?: boolean; - - forceRefresh?: boolean; - } - ): Promise { - const normalizedPath = this.normalizePath(workspacePath); - - - if (!options?.forceRefresh) { - const cached = this.cache.get(normalizedPath); - if (cached) { - return cached; - } - } - - - const context: DetectionContext = { - workspacePath: normalizedPath, - }; - - let result: ProjectInfo; - - - if (options?.useBackend !== false) { - try { - result = await this.detectWithBackend(normalizedPath); - } catch (error) { - log.warn('Backend detection failed, using frontend fallback', error); - result = await this.detectWithFrontend(context); - } - } else { - result = await this.detectWithFrontend(context); - } - - - this.cache.set(normalizedPath, result); - - return result; - } - - - private async detectWithBackend(workspacePath: string): Promise { - const { invoke } = await import('@tauri-apps/api/core'); - - const backendResult = await invoke<{ - languages: string[]; - primaryLanguage?: string; - fileCount: Record; - projectTypes: string[]; - totalFiles: number; - }>('lsp_detect_project', { - request: { workspacePath }, - }); - - - return { - type: this.inferProjectType(backendResult.projectTypes), - primaryLanguage: backendResult.primaryLanguage || backendResult.languages[0] || 'plaintext', - languages: backendResult.languages, - languageStats: backendResult.fileCount, - frameworks: [], - rootPath: workspacePath, - configFiles: [], - isMonorepo: false, - confidence: 0.9, - }; - } - - - private async detectWithFrontend(context: DetectionContext): Promise { - const workspacePath = context.workspacePath!; - - - for (const plugin of this.plugins.sort((a, b) => b.priority - a.priority)) { - const result = await plugin.detect(context); - if (result) { - return result; - } - } - - - const configResult = await this.detectByConfigFiles(workspacePath); - if (configResult) { - return configResult; - } - - - return this.createUnknownProject(workspacePath); - } - - - private async detectByConfigFiles(workspacePath: string): Promise { - try { - const { workspaceAPI } = await import('@/infrastructure/api'); - - - const files = await workspaceAPI.listFiles(workspacePath); - const fileNames = new Set(files.map((f: { name: string }) => f.name.toLowerCase())); - - - const isMonorepo = MONOREPO_MARKERS.some(marker => fileNames.has(marker.toLowerCase())); - - - let matchedMarker: ProjectMarker | null = null; - const detectedFrameworks: FrameworkInfo[] = []; - const configFiles: string[] = []; - - for (const marker of PROJECT_MARKERS) { - const configName = marker.configFile.toLowerCase(); - - - if (configName.startsWith('*')) { - const ext = configName.substring(1); - const matching = files.find(f => f.name.toLowerCase().endsWith(ext)); - if (matching) { - matchedMarker = marker; - configFiles.push(matching.name); - break; - } - } else if (fileNames.has(configName)) { - matchedMarker = marker; - configFiles.push(marker.configFile); - - - if (marker.checkFields) { - try { - const configPath = `${workspacePath}/${marker.configFile}`; - const content = await workspaceAPI.readFileContent(configPath); - const parsed = JSON.parse(content); - - for (const field of marker.checkFields) { - const value = this.getNestedValue(parsed, field.path); - const framework = field.detect(value); - if (framework) { - detectedFrameworks.push(framework); - } - } - } catch { - - } - } - - break; - } - } - - if (!matchedMarker) { - return null; - } - - - let projectType = matchedMarker.projectType; - if (detectedFrameworks.length > 0) { - projectType = this.inferProjectTypeFromFrameworks(detectedFrameworks) || projectType; - } - - - let primaryLanguage = matchedMarker.primaryLanguage; - if (fileNames.has('tsconfig.json') && primaryLanguage === 'javascript') { - primaryLanguage = 'typescript'; - } - - - let packageManager = matchedMarker.packageManager; - if (matchedMarker.configFile === 'package.json') { - if (fileNames.has('pnpm-lock.yaml')) { - packageManager = 'pnpm'; - } else if (fileNames.has('yarn.lock')) { - packageManager = 'yarn'; - } else if (fileNames.has('bun.lockb')) { - packageManager = 'bun'; - } - } - - return { - type: projectType, - primaryLanguage, - languages: [primaryLanguage], - languageStats: {}, - frameworks: detectedFrameworks, - packageManager, - rootPath: workspacePath, - configFiles, - isMonorepo, - confidence: 0.85, - }; - } catch (error) { - log.error('Config file detection failed', error); - return null; - } - } - - - private createUnknownProject(workspacePath: string): ProjectInfo { - return { - type: 'unknown', - primaryLanguage: 'plaintext', - languages: [], - languageStats: {}, - frameworks: [], - rootPath: workspacePath, - configFiles: [], - isMonorepo: false, - confidence: 0.1, - }; - } - - // =========================================================================== - - // =========================================================================== - - - private inferProjectType(backendTypes: string[]): ProjectType { - const typeMap: Record = { - 'nodejs': 'nodejs', - 'rust': 'rust', - 'python': 'python', - 'go': 'go', - 'java': 'java', - }; - - for (const t of backendTypes) { - const mapped = typeMap[t.toLowerCase()]; - if (mapped) { - return mapped; - } - } - - return 'unknown'; - } - - - private inferProjectTypeFromFrameworks(frameworks: FrameworkInfo[]): ProjectType | null { - const frameworkNames = frameworks.map(f => f.name.toLowerCase()); - - if (frameworkNames.includes('react')) { - if (frameworkNames.includes('nextjs')) return 'nextjs'; - return 'react'; - } - if (frameworkNames.includes('vue')) { - if (frameworkNames.includes('nuxt')) return 'nuxt'; - return 'vue'; - } - if (frameworkNames.includes('angular')) return 'angular'; - if (frameworkNames.includes('svelte')) return 'svelte'; - if (frameworkNames.includes('tauri')) return 'tauri'; - if (frameworkNames.includes('electron')) return 'electron'; - if (frameworkNames.includes('vite')) return 'vite'; - - return null; - } - - - private getNestedValue(obj: any, path: string[]): unknown { - let current = obj; - for (const key of path) { - if (current == null || typeof current !== 'object') { - return undefined; - } - current = current[key]; - } - return current; - } - - - private normalizePath(path: string): string { - return path.replace(/\\/g, '/'); - } - - // =========================================================================== - - // =========================================================================== - - - public registerPlugin(plugin: ProjectDetectionPlugin): void { - this.plugins.push(plugin); - this.plugins.sort((a, b) => b.priority - a.priority); - log.debug('Plugin registered', { pluginName: plugin.name }); - } - - // =========================================================================== - - // =========================================================================== - - - public clearCache(): void { - this.cache.clear(); - } - - - public invalidateCache(workspacePath: string): void { - const normalizedPath = this.normalizePath(workspacePath); - this.cache.delete(normalizedPath); - } - - - public getStats(): { - cacheSize: number; - pluginCount: number; - } { - return { - cacheSize: this.cache.size, - pluginCount: this.plugins.length, - }; - } -} - - -export const projectDetector = ProjectDetector.getInstance(); -export default ProjectDetector; diff --git a/src/web-ui/src/infrastructure/language-detection/index.ts b/src/web-ui/src/infrastructure/language-detection/index.ts index 24dd98c1e1..b206d3189b 100644 --- a/src/web-ui/src/infrastructure/language-detection/index.ts +++ b/src/web-ui/src/infrastructure/language-detection/index.ts @@ -1,11 +1,10 @@ /** - * Language and project detection. + * Language detection. */ // Core services export { languageDetector } from './core/LanguageDetector'; export { languageRegistry } from './core/LanguageRegistry'; -export { projectDetector } from './core/ProjectDetector'; // Types export type { @@ -14,20 +13,11 @@ export type { LanguageCategory, FileDetectionResult, - // Project - ProjectInfo, - ProjectType, - FrameworkInfo, - // Context DetectionContext, // Plugins LanguagePlugin, - ProjectDetectionPlugin, - - // LSP - LspConfig, // Events LanguageDetectionEvent, @@ -39,7 +29,6 @@ export { getMonacoLanguage, getFileIconType, getFileColor, - hasLspSupport, getPrismLanguage, getPrismLanguageFromAlias, getEditorType, diff --git a/src/web-ui/src/infrastructure/language-detection/types/index.ts b/src/web-ui/src/infrastructure/language-detection/types/index.ts index 2e6dc63c38..08ff1b6fd8 100644 --- a/src/web-ui/src/infrastructure/language-detection/types/index.ts +++ b/src/web-ui/src/infrastructure/language-detection/types/index.ts @@ -44,9 +44,6 @@ export interface Language { prismId?: string; - lspId?: string; - - textmateScope?: string; @@ -79,103 +76,6 @@ export interface Language { // ============================================================================ -export type ProjectType = - - | 'react' - | 'vue' - | 'angular' - | 'svelte' - | 'nextjs' - | 'nuxt' - | 'vite' - | 'webpack' - - | 'nodejs' - | 'express' - | 'nestjs' - | 'fastify' - - | 'tauri' - | 'electron' - - | 'react-native' - | 'flutter' - - | 'rust' - | 'cargo' - | 'go' - | 'python' - | 'django' - | 'flask' - | 'java' - | 'maven' - | 'gradle' - | 'dotnet' - - | 'monorepo' - | 'library' - | 'cli' - | 'unknown'; - - -export interface FrameworkInfo { - - name: string; - - - version?: string; - - - source?: string; -} - - -export interface ProjectInfo { - - type: ProjectType; - - - primaryLanguage: string; - - - languages: string[]; - - - languageStats: Record; - - - frameworks: FrameworkInfo[]; - - - packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun' | 'cargo' | 'pip' | 'poetry' | 'maven' | 'gradle' | 'go'; - - - buildTools?: string[]; - - - rootPath: string; - - - configFiles: string[]; - - - isMonorepo: boolean; - - - subProjects?: ProjectInfo[]; - - - confidence: number; - - - metadata?: Record; -} - -// ============================================================================ - -// ============================================================================ - - export interface DetectionContext { workspacePath?: string; @@ -187,12 +87,8 @@ export interface DetectionContext { fileContent?: string; - projectInfo?: ProjectInfo; - - hints?: { language?: string; - projectType?: ProjectType; }; } @@ -216,27 +112,6 @@ export interface FileDetectionResult { // ============================================================================ -export interface ProjectDetectionPlugin { - - id: string; - - - name: string; - - - supportedTypes: ProjectType[]; - - - priority: number; - - - detect(context: DetectionContext): Promise; - - - getConfigFiles(): string[]; -} - - export interface LanguagePlugin { id: string; @@ -256,26 +131,7 @@ export interface LanguagePlugin { // ============================================================================ -export interface LspConfig { - - serverId: string; - - - enabled: boolean; - - - initOptions?: Record; - - - workspaceConfig?: Record; -} - -// ============================================================================ - -// ============================================================================ - - export interface LanguageDetectionEvent { - type: 'project-detected' | 'language-detected' | 'plugin-registered'; + type: 'language-detected' | 'plugin-registered'; payload: unknown; } diff --git a/src/web-ui/src/infrastructure/language-detection/utils/helpers.ts b/src/web-ui/src/infrastructure/language-detection/utils/helpers.ts index 990080bdba..635767a606 100644 --- a/src/web-ui/src/infrastructure/language-detection/utils/helpers.ts +++ b/src/web-ui/src/infrastructure/language-detection/utils/helpers.ts @@ -27,11 +27,6 @@ export function getFileColor(filePathOrName: string): string | undefined { } -export function hasLspSupport(filePathOrName: string): boolean { - return languageDetector.hasLspSupport(filePathOrName); -} - - export function getLanguageById(languageId: string): Language | undefined { return languageDetector.getLanguage(languageId); } diff --git a/src/web-ui/src/infrastructure/peer-device/README.md b/src/web-ui/src/infrastructure/peer-device/README.md index 03f13b5767..d38bdb05b2 100644 --- a/src/web-ui/src/infrastructure/peer-device/README.md +++ b/src/web-ui/src/infrastructure/peer-device/README.md @@ -41,11 +41,9 @@ Still to migrate, in order: the interaction mailbox, then history positions. - Everything in `resetProductSurface()` must be **frontend-only**. `resetProductSurface` runs before the transport swap, so any backend call it makes lands on the device being *left*. `terminal_shutdown_all` and - `lsp_close_workspace` were exactly that bug: switching away killed the - PTYs and language servers an agent turn there was still using - (regression: 2026-08-14 multi-device switch). Use - `TerminalService.disconnect()` and - `WorkspaceLspManager.detachAllForSurfaceSwitch()`. + similar lifecycle calls can kill work an agent turn there is still using + (regression: 2026-08-14 multi-device switch). Use frontend-only listener + detachment such as `TerminalService.disconnect()`. - **Identity includes the device surface.** Workspace paths and session ids can be equal on different machines. FlowChat/workspace containers, state machines, processing status, pending messages, composer drafts, diff --git a/src/web-ui/src/infrastructure/peer-device/deviceSurfaceRouting.ts b/src/web-ui/src/infrastructure/peer-device/deviceSurfaceRouting.ts index b400d47f0f..5ccd6dbfa9 100644 --- a/src/web-ui/src/infrastructure/peer-device/deviceSurfaceRouting.ts +++ b/src/web-ui/src/infrastructure/peer-device/deviceSurfaceRouting.ts @@ -30,7 +30,6 @@ export const PEER_EVENT_WRAPPED_PAYLOAD_KEY = '__bitfunSourcePayload'; const SURFACE_SCOPED_EVENTS = new Set([ 'terminal_event', 'file-system-changed', - 'lsp-event', 'permission://event', 'account://settings-applied', 'ai://model-catalog-updated', diff --git a/src/web-ui/src/infrastructure/peer-device/peerDeviceSurfaceRuntime.ts b/src/web-ui/src/infrastructure/peer-device/peerDeviceSurfaceRuntime.ts index bdbe5a5601..31206f07fa 100644 --- a/src/web-ui/src/infrastructure/peer-device/peerDeviceSurfaceRuntime.ts +++ b/src/web-ui/src/infrastructure/peer-device/peerDeviceSurfaceRuntime.ts @@ -16,8 +16,6 @@ import { FlowChatManager } from '@/flow_chat/services/FlowChatManager'; import { useSceneStore } from '@/app/stores/sceneStore'; import { clearAgentCanvasForPeerSwitch } from '@/app/components/panels/content-canvas/stores'; import { editorManager } from '@/tools/editor/services/EditorManager'; -import { WorkspaceLspManager } from '@/tools/lsp/services/WorkspaceLspManager'; -import { lspAdapterManager } from '@/tools/lsp/services/LspAdapterManager'; import { TerminalService } from '@/tools/terminal/services/TerminalService'; import { createLogger } from '@/shared/utils/logger'; import { @@ -76,13 +74,6 @@ async function resetProductSurface(assertCurrent: () => void): Promise { } assertCurrent(); - try { - lspAdapterManager.disposeAll(); - WorkspaceLspManager.detachAllForSurfaceSwitch(); - } catch (error) { - log.warn('Failed to reset LSP during device surface switch', { error }); - } - try { editorManager.destroy(); } catch (error) { diff --git a/src/web-ui/src/locales/en-US/errors.json b/src/web-ui/src/locales/en-US/errors.json index 2cf15c2e2b..0b954e31c4 100644 --- a/src/web-ui/src/locales/en-US/errors.json +++ b/src/web-ui/src/locales/en-US/errors.json @@ -120,8 +120,7 @@ "openError": "Failed to open editor", "saveError": "Failed to save file", "formatError": "Formatting failed", - "parseError": "Failed to parse file", - "lspError": "Language server error" + "parseError": "Failed to parse file" }, "config": { "loadError": "Failed to load configuration", diff --git a/src/web-ui/src/locales/en-US/settings.json b/src/web-ui/src/locales/en-US/settings.json index c6f1a81255..d40535f75f 100644 --- a/src/web-ui/src/locales/en-US/settings.json +++ b/src/web-ui/src/locales/en-US/settings.json @@ -471,13 +471,6 @@ "minimap": "Show Minimap", "lineNumbers": "Show Line Numbers" }, - "lspConfig": { - "title": "LSP", - "subtitle": "Configure language servers", - "installedServers": "Installed Language Servers", - "noServers": "No language servers", - "noServersHint": "Language servers provide code intelligence features" - }, "terminalConfig": { "title": "Terminal", "subtitle": "Configure integrated terminal", diff --git a/src/web-ui/src/locales/en-US/settings/editor.json b/src/web-ui/src/locales/en-US/settings/editor.json index 53c163bea2..e5e35874e7 100644 --- a/src/web-ui/src/locales/en-US/settings/editor.json +++ b/src/web-ui/src/locales/en-US/settings/editor.json @@ -95,7 +95,7 @@ }, "advanced": { "semanticHighlighting": "Semantic Highlighting", - "semanticHighlightingDesc": "Semantic-based code highlighting (requires LSP)", + "semanticHighlightingDesc": "Use semantic information to improve code highlighting", "bracketPairColorization": "Bracket Pair Colorization", "bracketPairColorizationDesc": "Use different colors for nested bracket pairs", "formatOnSave": "Format on Save", diff --git a/src/web-ui/src/locales/en-US/settings/lsp.json b/src/web-ui/src/locales/en-US/settings/lsp.json deleted file mode 100644 index 7d90d6762d..0000000000 --- a/src/web-ui/src/locales/en-US/settings/lsp.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "title": "LSP Plugin Management", - "subtitle": "Manage Language Server Protocol plugins", - "section": { - "settings": { - "title": "Basic Settings", - "description": "Runtime configuration for language services." - }, - "plugins": { - "title": "Language Plugins", - "description": "Manage installed language service plugins." - } - }, - "tabs": { - "plugins": "Plugin Management", - "help": "Help" - }, - "settings": { - "autoStart": "Auto-start LSP when workspace opens", - "autoStartDesc": "Automatically detect project type and start corresponding language server. When disabled, starts on first file open.", - "saveTooltip": "Save settings", - "cancelTooltip": "Cancel" - }, - "help": { - "aboutLsp": { - "title": "About LSP", - "content": "Language Server Protocol (LSP) provides intelligent code completion, go-to-definition, find references, and more. After installing LSP plugins for your language, the editor will automatically provide intelligent support." - }, - "pluginFormat": { - "title": "Plugin Format", - "content": "Plugin packages are .vcpkg files (ZIP format) containing LSP server binaries and configuration, supporting Windows, macOS, and Linux platforms." - }, - "pluginPath": { - "title": "Plugin Storage Path" - }, - "morePlugins": { - "title": "Need more plugins?", - "content": "Check out the", - "link": "Plugin Development Documentation", - "suffix": "to learn how to develop and contribute LSP plugins." - } - }, - "pluginList": { - "loading": "Loading plugins...", - "retry": "Retry", - "empty": { - "title": "No LSP plugins installed yet", - "hint": "Click the button below to install plugins for language support" - }, - "header": "Installed Plugins ({{count}})", - "initTooltip": "Initialize LSP", - "installTooltip": "Install Plugin", - "installButton": "Install Plugin", - "installing": "Installing...", - "refreshTooltip": "Refresh List", - "confirmUninstall": "Are you sure you want to uninstall plugin {{pluginId}}?", - "uninstallSuccess": "Plugin uninstalled successfully", - "uninstallFailed": "Failed to uninstall plugin", - "uninstall": "Uninstall Plugin", - "details": { - "author": "Author", - "languages": "Supported Languages", - "capabilities": "Capabilities", - "completion": "Completion", - "hover": "Hover", - "definition": "Definition", - "references": "References", - "formatting": "Formatting" - } - }, - "fileDialog": { - "pluginPackage": "BitFun Plugin Package" - }, - "editor": { - "goToDefinition": "Go to Definition", - "findAllReferences": "Find All References", - "goToFile": "Go to {{fileName}}" - }, - "server": { - "starting": "Starting {{language}} language server", - "startingMessage": "Starting server process...", - "startFailed": "{{language}} language server failed to start", - "checkConfig": "Please check configuration", - "error": "{{language}} language server error", - "languageService": "{{language}} language service" - }, - "messages": { - "settingsSaved": "Settings saved", - "saveSettingsFailed": "Failed to save settings", - "initSuccess": "LSP system initialized successfully", - "initFailed": "Initialization failed", - "installSuccess": "Plugin {{pluginId}} installed successfully!", - "installFailed": "Installation failed" - } -} diff --git a/src/web-ui/src/locales/en-US/tools.json b/src/web-ui/src/locales/en-US/tools.json index 56c65598af..a48bcd4e89 100644 --- a/src/web-ui/src/locales/en-US/tools.json +++ b/src/web-ui/src/locales/en-US/tools.json @@ -178,9 +178,6 @@ "indentSettings": "Indent Settings", "fileEncoding": "File Encoding", "selectLanguageMode": "Select Language Mode", - "lspConnected": "LSP Connected", - "lspConnecting": "LSP Connecting...", - "lspDisconnected": "LSP Disconnected", "selectionLinesChars": "{{lines}} lines, {{chars}} chars", "selectionChars": "{{count}} selected", "ln": "Ln", @@ -279,23 +276,6 @@ "resolveConflict": "Resolve Conflict" } }, - "lsp": { - "referencesPanel": { - "previewFallback": "(no preview)", - "emptyTitle": "No references found", - "emptyWithSymbol": "No references found for symbol \"{{symbol}}\"", - "emptyDescription": "No references found", - "titleWithSymbol": "References for \"{{symbol}}\"", - "title": "References", - "emptyLine": "(empty line)" - }, - "errors": { - "loadPluginsFailed": "Failed to load plugins", - "installPluginFailed": "Failed to install plugin", - "uninstallPluginFailed": "Failed to uninstall plugin", - "initializeFailed": "Failed to initialize LSP" - } - }, "fileTree": { "title": "File Explorer", "empty": "No files", diff --git a/src/web-ui/src/locales/zh-CN/errors.json b/src/web-ui/src/locales/zh-CN/errors.json index 92d3599ca8..7e79eac0c5 100644 --- a/src/web-ui/src/locales/zh-CN/errors.json +++ b/src/web-ui/src/locales/zh-CN/errors.json @@ -120,8 +120,7 @@ "openError": "打开编辑器失败", "saveError": "保存文件失败", "formatError": "格式化失败", - "parseError": "解析文件失败", - "lspError": "语言服务器错误" + "parseError": "解析文件失败" }, "config": { "loadError": "加载配置失败", diff --git a/src/web-ui/src/locales/zh-CN/settings.json b/src/web-ui/src/locales/zh-CN/settings.json index fc2b14dd31..abd3fb501c 100644 --- a/src/web-ui/src/locales/zh-CN/settings.json +++ b/src/web-ui/src/locales/zh-CN/settings.json @@ -471,13 +471,6 @@ "minimap": "显示缩略图", "lineNumbers": "显示行号" }, - "lspConfig": { - "title": "语言服务", - "subtitle": "配置语言服务器", - "installedServers": "已安装的语言服务器", - "noServers": "暂无语言服务器", - "noServersHint": "语言服务器提供代码智能功能" - }, "terminalConfig": { "title": "终端", "subtitle": "配置集成终端", diff --git a/src/web-ui/src/locales/zh-CN/settings/editor.json b/src/web-ui/src/locales/zh-CN/settings/editor.json index 3716ac4c34..58414a9f31 100644 --- a/src/web-ui/src/locales/zh-CN/settings/editor.json +++ b/src/web-ui/src/locales/zh-CN/settings/editor.json @@ -95,7 +95,7 @@ }, "advanced": { "semanticHighlighting": "语义高亮", - "semanticHighlightingDesc": "基于语义分析的代码高亮(需要 LSP 支持)", + "semanticHighlightingDesc": "使用语义信息改进代码高亮", "bracketPairColorization": "括号对着色", "bracketPairColorizationDesc": "用不同颜色区分嵌套的括号对", "formatOnSave": "保存时格式化", diff --git a/src/web-ui/src/locales/zh-CN/settings/lsp.json b/src/web-ui/src/locales/zh-CN/settings/lsp.json deleted file mode 100644 index 897292f63a..0000000000 --- a/src/web-ui/src/locales/zh-CN/settings/lsp.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "title": "LSP 插件管理", - "subtitle": "管理语言服务器协议插件", - "section": { - "settings": { - "title": "基础设置", - "description": "语言服务的运行参数配置。" - }, - "plugins": { - "title": "语言插件", - "description": "管理已安装的语言服务插件。" - } - }, - "tabs": { - "plugins": "插件管理", - "help": "帮助" - }, - "settings": { - "autoStart": "工作区打开时自动启动 LSP", - "autoStartDesc": "自动检测项目类型并启动对应的语言服务器,关闭后将在首次打开该类型文件时启动", - "saveTooltip": "保存设置", - "cancelTooltip": "取消" - }, - "help": { - "aboutLsp": { - "title": "关于 LSP", - "content": "Language Server Protocol(LSP)提供智能代码补全、跳转定义、查找引用等功能。安装对应语言的 LSP 插件后,编辑器将自动提供智能支持。" - }, - "pluginFormat": { - "title": "插件格式", - "content": "插件包格式为 .vcpkg 文件(ZIP 格式),包含 LSP 服务器二进制文件和配置信息,支持 Windows、macOS 和 Linux 平台。" - }, - "pluginPath": { - "title": "插件存储路径" - }, - "morePlugins": { - "title": "需要更多插件?", - "content": "查看", - "link": "插件开发文档", - "suffix": "了解如何开发和贡献 LSP 插件。" - } - }, - "pluginList": { - "loading": "正在加载插件...", - "retry": "重试", - "empty": { - "title": "还没有安装任何LSP插件", - "hint": "请点击下方按钮安装插件以获得语言支持" - }, - "header": "已安装的插件 ({{count}})", - "initTooltip": "初始化 LSP", - "installTooltip": "安装插件", - "installButton": "安装插件", - "installing": "正在安装...", - "refreshTooltip": "刷新列表", - "confirmUninstall": "确定要卸载插件 {{pluginId}} 吗?", - "uninstallSuccess": "插件卸载成功", - "uninstallFailed": "插件卸载失败", - "uninstall": "卸载插件", - "details": { - "author": "作者", - "languages": "支持语言", - "capabilities": "功能", - "completion": "补全", - "hover": "悬停", - "definition": "跳转", - "references": "引用", - "formatting": "格式化" - } - }, - "fileDialog": { - "pluginPackage": "BitFun 插件包" - }, - "editor": { - "goToDefinition": "转到定义", - "findAllReferences": "查找所有引用", - "goToFile": "转到 {{fileName}}" - }, - "server": { - "starting": "启动 {{language}} 语言服务器", - "startingMessage": "正在启动服务器进程...", - "startFailed": "{{language}} 语言服务器启动失败", - "checkConfig": "请检查配置", - "error": "{{language}} 语言服务器错误", - "languageService": "{{language}} 语言服务" - }, - "messages": { - "settingsSaved": "设置已保存", - "saveSettingsFailed": "保存设置失败", - "initSuccess": "LSP 系统初始化成功", - "initFailed": "初始化失败", - "installSuccess": "插件 {{pluginId}} 安装成功!", - "installFailed": "安装失败" - } -} diff --git a/src/web-ui/src/locales/zh-CN/tools.json b/src/web-ui/src/locales/zh-CN/tools.json index b4ba8a2835..f15b5f787b 100644 --- a/src/web-ui/src/locales/zh-CN/tools.json +++ b/src/web-ui/src/locales/zh-CN/tools.json @@ -178,9 +178,6 @@ "indentSettings": "缩进设置", "fileEncoding": "文件编码", "selectLanguageMode": "选择语言模式", - "lspConnected": "LSP 已连接", - "lspConnecting": "LSP 连接中...", - "lspDisconnected": "LSP 未连接", "selectionLinesChars": "{{lines}} 行, {{chars}} 字符", "selectionChars": "{{count}} 已选", "ln": "Ln", @@ -279,23 +276,6 @@ "resolveConflict": "解决冲突" } }, - "lsp": { - "referencesPanel": { - "previewFallback": "(无预览)", - "emptyTitle": "未找到引用", - "emptyWithSymbol": "符号 \"{{symbol}}\" 没有引用", - "emptyDescription": "没有找到引用", - "titleWithSymbol": "\"{{symbol}}\" 的引用", - "title": "引用列表", - "emptyLine": "(空行)" - }, - "errors": { - "loadPluginsFailed": "Failed to load plugins", - "installPluginFailed": "Failed to install plugin", - "uninstallPluginFailed": "Failed to uninstall plugin", - "initializeFailed": "Failed to initialize LSP" - } - }, "fileTree": { "title": "文件浏览器", "empty": "暂无文件", diff --git a/src/web-ui/src/locales/zh-TW/errors.json b/src/web-ui/src/locales/zh-TW/errors.json index 5967e96b20..12a45e3489 100644 --- a/src/web-ui/src/locales/zh-TW/errors.json +++ b/src/web-ui/src/locales/zh-TW/errors.json @@ -120,8 +120,7 @@ "openError": "開啟編輯器失敗", "saveError": "儲存檔案失敗", "formatError": "格式化失敗", - "parseError": "解析檔案失敗", - "lspError": "語言伺服器錯誤" + "parseError": "解析檔案失敗" }, "config": { "loadError": "載入設定失敗", diff --git a/src/web-ui/src/locales/zh-TW/settings.json b/src/web-ui/src/locales/zh-TW/settings.json index 0c8bf78863..a16d5e1672 100644 --- a/src/web-ui/src/locales/zh-TW/settings.json +++ b/src/web-ui/src/locales/zh-TW/settings.json @@ -471,13 +471,6 @@ "minimap": "顯示縮略圖", "lineNumbers": "顯示行號" }, - "lspConfig": { - "title": "語言服務", - "subtitle": "設定語言伺服器", - "installedServers": "已安裝的語言伺服器", - "noServers": "暫無語言伺服器", - "noServersHint": "語言伺服器提供代碼智能功能" - }, "terminalConfig": { "title": "終端", "subtitle": "設定集成終端", diff --git a/src/web-ui/src/locales/zh-TW/settings/editor.json b/src/web-ui/src/locales/zh-TW/settings/editor.json index 17d653ee5f..790a8dd585 100644 --- a/src/web-ui/src/locales/zh-TW/settings/editor.json +++ b/src/web-ui/src/locales/zh-TW/settings/editor.json @@ -95,7 +95,7 @@ }, "advanced": { "semanticHighlighting": "語義高亮", - "semanticHighlightingDesc": "基於語義分析的代碼高亮(需要 LSP 支持)", + "semanticHighlightingDesc": "使用語義資訊改進程式碼高亮", "bracketPairColorization": "括號對著色", "bracketPairColorizationDesc": "用不同顏色區分嵌套的括號對", "formatOnSave": "儲存時格式化", diff --git a/src/web-ui/src/locales/zh-TW/settings/lsp.json b/src/web-ui/src/locales/zh-TW/settings/lsp.json deleted file mode 100644 index e233629e92..0000000000 --- a/src/web-ui/src/locales/zh-TW/settings/lsp.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "title": "LSP 插件管理", - "subtitle": "管理語言伺服器協議插件", - "section": { - "settings": { - "title": "基礎設置", - "description": "語言服務的運行參數設定。" - }, - "plugins": { - "title": "語言插件", - "description": "管理已安裝的語言服務插件。" - } - }, - "tabs": { - "plugins": "插件管理", - "help": "幫助" - }, - "settings": { - "autoStart": "工作區開啟時自動啟動 LSP", - "autoStartDesc": "自動檢測項目類型並啟動對應的語言伺服器,關閉後將在首次開啟該類型檔案時啟動", - "saveTooltip": "儲存設置", - "cancelTooltip": "取消" - }, - "help": { - "aboutLsp": { - "title": "關於 LSP", - "content": "Language Server Protocol(LSP)提供智能代碼補全、跳轉定義、查找引用等功能。安裝對應語言的 LSP 插件後,編輯器將自動提供智能支持。" - }, - "pluginFormat": { - "title": "插件格式", - "content": "插件包格式為 .vcpkg 檔案(ZIP 格式),包含 LSP 伺服器二進制檔案和設定資訊,支持 Windows、macOS 和 Linux 平臺。" - }, - "pluginPath": { - "title": "插件存儲路徑" - }, - "morePlugins": { - "title": "需要更多插件?", - "content": "查看", - "link": "插件開發文檔", - "suffix": "瞭解如何開發和貢獻 LSP 插件。" - } - }, - "pluginList": { - "loading": "正在載入插件...", - "retry": "重試", - "empty": { - "title": "還沒有安裝任何LSP插件", - "hint": "請點擊下方按鈕安裝插件以獲得語言支持" - }, - "header": "已安裝的插件 ({{count}})", - "initTooltip": "初始化 LSP", - "installTooltip": "安裝插件", - "installButton": "安裝插件", - "installing": "正在安裝...", - "refreshTooltip": "重新整理列表", - "confirmUninstall": "確定要解除安裝插件 {{pluginId}} 嗎?", - "uninstallSuccess": "插件解除安裝成功", - "uninstallFailed": "插件解除安裝失敗", - "uninstall": "解除安裝插件", - "details": { - "author": "作者", - "languages": "支持語言", - "capabilities": "功能", - "completion": "補全", - "hover": "懸停", - "definition": "跳轉", - "references": "引用", - "formatting": "格式化" - } - }, - "fileDialog": { - "pluginPackage": "BitFun 插件包" - }, - "editor": { - "goToDefinition": "轉到定義", - "findAllReferences": "查找所有引用", - "goToFile": "轉到 {{fileName}}" - }, - "server": { - "starting": "啟動 {{language}} 語言伺服器", - "startingMessage": "正在啟動伺服器進程...", - "startFailed": "{{language}} 語言伺服器啟動失敗", - "checkConfig": "請檢查設定", - "error": "{{language}} 語言伺服器錯誤", - "languageService": "{{language}} 語言服務" - }, - "messages": { - "settingsSaved": "設置已儲存", - "saveSettingsFailed": "儲存設置失敗", - "initSuccess": "LSP 系統初始化成功", - "initFailed": "初始化失敗", - "installSuccess": "插件 {{pluginId}} 安裝成功!", - "installFailed": "安裝失敗" - } -} diff --git a/src/web-ui/src/locales/zh-TW/tools.json b/src/web-ui/src/locales/zh-TW/tools.json index 87dcbd314f..6ebb3c3872 100644 --- a/src/web-ui/src/locales/zh-TW/tools.json +++ b/src/web-ui/src/locales/zh-TW/tools.json @@ -178,9 +178,6 @@ "indentSettings": "縮進設置", "fileEncoding": "檔案編碼", "selectLanguageMode": "選擇語言模式", - "lspConnected": "LSP 已連接", - "lspConnecting": "LSP 連接中...", - "lspDisconnected": "LSP 未連接", "selectionLinesChars": "{{lines}} 行, {{chars}} 字符", "selectionChars": "{{count}} 已選", "ln": "Ln", @@ -279,23 +276,6 @@ "resolveConflict": "解決衝突" } }, - "lsp": { - "referencesPanel": { - "previewFallback": "(無預覽)", - "emptyTitle": "未找到引用", - "emptyWithSymbol": "符號 \"{{symbol}}\" 沒有引用", - "emptyDescription": "沒有找到引用", - "titleWithSymbol": "\"{{symbol}}\" 的引用", - "title": "引用列表", - "emptyLine": "(空行)" - }, - "errors": { - "loadPluginsFailed": "Failed to load plugins", - "installPluginFailed": "Failed to install plugin", - "uninstallPluginFailed": "Failed to uninstall plugin", - "initializeFailed": "Failed to initialize LSP" - } - }, "fileTree": { "title": "檔案瀏覽器", "empty": "暫無檔案", diff --git a/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts b/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts index dc15010c22..19d437e38d 100644 --- a/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts +++ b/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts @@ -19,6 +19,31 @@ import { createLogger } from '@/shared/utils/logger'; const log = createLogger('ContextResolver'); +const EDITOR_ACTION_IDS = [ + 'editor.action.formatDocument', + 'editor.action.revealDefinition', + 'editor.action.goToTypeDefinition', + 'editor.action.referenceSearch.trigger', + 'editor.action.rename', + 'editor.action.quickFix', + 'editor.action.quickOutline', + 'editor.action.wordHighlight.trigger' +] as const; + +function findAttachedMonacoEditor(target: HTMLElement, boundary: HTMLElement): any | null { + let currentElement: HTMLElement | null = target; + while (currentElement) { + if ((currentElement as any).__monacoEditor) { + return (currentElement as any).__monacoEditor; + } + if (currentElement === boundary) { + break; + } + currentElement = currentElement.parentElement; + } + return null; +} + export class ContextResolver { @@ -249,25 +274,17 @@ export class ContextResolver { let cursorPosition: { line: number; column: number } | undefined; let selectionRange: EditorContext['selectionRange']; + let supportedActionIds: string[] | undefined; try { const monacoGlobal = (window as any).monaco; + const attachedEditor = findAttachedMonacoEditor(base.targetElement, editorElement); - if (monacoGlobal?.editor) { - let targetEditor = null; - - - let currentElement: HTMLElement | null = editorElement; - while (currentElement && !targetEditor) { - if ((currentElement as any).__monacoEditor) { - targetEditor = (currentElement as any).__monacoEditor; - break; - } - currentElement = currentElement.parentElement; - } + if (attachedEditor || monacoGlobal?.editor) { + let targetEditor = attachedEditor; - if (!targetEditor) { + if (!targetEditor && monacoGlobal?.editor) { const editors = monacoGlobal.editor.getEditors?.() || []; for (const editor of editors) { const domNode = editor.getDomNode?.(); @@ -285,6 +302,10 @@ export class ContextResolver { if (targetEditor) { + supportedActionIds = EDITOR_ACTION_IDS.filter((actionId) => { + const action = targetEditor.getAction?.(actionId); + return action?.isSupported?.() === true; + }); try { if (typeof targetEditor.getTargetAtClientPoint === 'function') { @@ -366,6 +387,7 @@ export class ContextResolver { cursorPosition, selectedText, selectionRange, + supportedActionIds, isReadOnly }; } diff --git a/src/web-ui/src/shared/context-menu-system/providers/EditorMenuProvider.ts b/src/web-ui/src/shared/context-menu-system/providers/EditorMenuProvider.ts index fad629d261..4d930417fd 100644 --- a/src/web-ui/src/shared/context-menu-system/providers/EditorMenuProvider.ts +++ b/src/web-ui/src/shared/context-menu-system/providers/EditorMenuProvider.ts @@ -6,7 +6,6 @@ import { MenuContext, ContextType, EditorContext } from '../types/context.types' import { commandExecutor } from '../commands/CommandExecutor'; import { globalEventBus } from '@/infrastructure/event-bus'; import { i18nService } from '@/infrastructure/i18n'; -import { lspExtensionRegistry } from '@/tools/lsp/services/LspExtensionRegistry'; import type { CodeSnippetContext } from '@/shared/types/context'; import { useContextStore } from '@/shared/stores/contextStore'; @@ -74,6 +73,21 @@ function newSnippetContextId(): string { } return `code-snippet-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } + +function supportsEditorAction(context: EditorContext, actionId: string): boolean { + return context.supportedActionIds?.includes(actionId) === true; +} + +function appendMenuGroup(items: MenuItem[], separatorId: string, group: MenuItem[]): void { + if (group.length === 0) { + return; + } + if (items.length > 0 && !items[items.length - 1].separator) { + items.push({ id: separatorId, label: '', separator: true }); + } + items.push(...group); +} + export class EditorMenuProvider implements IMenuProvider { readonly id = 'editor'; readonly name = i18nService.t('common:contextMenu.editorMenu.name'); @@ -186,16 +200,12 @@ export class EditorMenuProvider implements IMenuProvider { }); } - - if (!editorContext.isReadOnly && editorContext.filePath - && lspExtensionRegistry.isFileSupported(editorContext.filePath)) { - items.push({ - id: 'editor-separator-2', - label: '', - separator: true - }); - - items.push({ + if ( + !editorContext.isReadOnly && + editorContext.filePath && + supportsEditorAction(editorContext, 'editor.action.formatDocument') + ) { + appendMenuGroup(items, 'editor-separator-2', [{ id: 'editor-format', label: i18nService.t('common:editor.formatDocument'), icon: 'Code', @@ -207,72 +217,68 @@ export class EditorMenuProvider implements IMenuProvider { editorId: editorContext.editorId }); } - }); + }]); } - // Only show LSP menu items when the file type is supported by an LSP server - const hasLspSupport = editorContext.filePath - && lspExtensionRegistry.isFileSupported(editorContext.filePath); - - if (editorContext.filePath && hasLspSupport) { - + if (editorContext.filePath) { const position = editorContext.cursorPosition || { line: 1, column: 1 }; - items.push({ - id: 'editor-separator-lsp', - label: '', - separator: true - }); + const navigationItems: MenuItem[] = []; - - items.push({ - id: 'editor-goto-definition', - label: i18nService.t('common:editor.goToDefinition'), - icon: 'Navigation', - shortcut: 'F12', - onClick: () => { - globalEventBus.emit('editor:goto-definition', { - filePath: editorContext.filePath, - line: position.line, - column: position.column, - editorId: editorContext.editorId - }); - } - }); + if (supportsEditorAction(editorContext, 'editor.action.revealDefinition')) { + navigationItems.push({ + id: 'editor-goto-definition', + label: i18nService.t('common:editor.goToDefinition'), + icon: 'Navigation', + shortcut: 'F12', + onClick: () => { + globalEventBus.emit('editor:goto-definition', { + filePath: editorContext.filePath, + line: position.line, + column: position.column, + editorId: editorContext.editorId + }); + } + }); + } - - items.push({ - id: 'editor-goto-type-definition', - label: i18nService.t('common:editor.goToTypeDefinition'), - icon: 'FileType', - onClick: () => { - globalEventBus.emit('editor:goto-type-definition', { - filePath: editorContext.filePath, - line: position.line, - column: position.column, - editorId: editorContext.editorId - }); - } - }); + if (supportsEditorAction(editorContext, 'editor.action.goToTypeDefinition')) { + navigationItems.push({ + id: 'editor-goto-type-definition', + label: i18nService.t('common:editor.goToTypeDefinition'), + icon: 'FileType', + onClick: () => { + globalEventBus.emit('editor:goto-type-definition', { + filePath: editorContext.filePath, + line: position.line, + column: position.column, + editorId: editorContext.editorId + }); + } + }); + } - - items.push({ - id: 'editor-find-references', - label: i18nService.t('common:editor.findAllReferences'), - icon: 'Search', - shortcut: 'Shift+F12', - onClick: () => { - globalEventBus.emit('editor:find-references', { - filePath: editorContext.filePath, - line: position.line, - column: position.column, - editorId: editorContext.editorId - }); - } - }); + if (supportsEditorAction(editorContext, 'editor.action.referenceSearch.trigger')) { + navigationItems.push({ + id: 'editor-find-references', + label: i18nService.t('common:editor.findAllReferences'), + icon: 'Search', + shortcut: 'Shift+F12', + onClick: () => { + globalEventBus.emit('editor:find-references', { + filePath: editorContext.filePath, + line: position.line, + column: position.column, + editorId: editorContext.editorId + }); + } + }); + } - - if (!editorContext.isReadOnly) { - items.push({ + if ( + !editorContext.isReadOnly && + supportsEditorAction(editorContext, 'editor.action.rename') + ) { + navigationItems.push({ id: 'editor-rename-symbol', label: i18nService.t('common:editor.renameSymbol'), icon: 'Edit', @@ -286,9 +292,13 @@ export class EditorMenuProvider implements IMenuProvider { }); } }); + } - - items.push({ + if ( + !editorContext.isReadOnly && + supportsEditorAction(editorContext, 'editor.action.quickFix') + ) { + navigationItems.push({ id: 'editor-code-action', label: i18nService.t('common:editor.quickFix'), icon: 'Lightbulb', @@ -304,40 +314,42 @@ export class EditorMenuProvider implements IMenuProvider { }); } - - items.push({ - id: 'editor-separator-more', - label: '', - separator: true - }); + appendMenuGroup(items, 'editor-separator-navigation', navigationItems); - items.push({ - id: 'editor-document-symbols', - label: i18nService.t('common:editor.goToSymbol'), - icon: 'List', - shortcut: 'Ctrl+Shift+O', - onClick: () => { - globalEventBus.emit('editor:document-symbols', { - filePath: editorContext.filePath, - editorId: editorContext.editorId - }); - } - }); + const moreItems: MenuItem[] = []; - - items.push({ - id: 'editor-document-highlight', - label: i18nService.t('common:editor.highlightAllOccurrences'), - icon: 'Highlighter', - onClick: () => { - globalEventBus.emit('editor:document-highlight', { - filePath: editorContext.filePath, - line: position.line, - column: position.column, - editorId: editorContext.editorId - }); - } - }); + if (supportsEditorAction(editorContext, 'editor.action.quickOutline')) { + moreItems.push({ + id: 'editor-document-symbols', + label: i18nService.t('common:editor.goToSymbol'), + icon: 'List', + shortcut: 'Ctrl+Shift+O', + onClick: () => { + globalEventBus.emit('editor:document-symbols', { + filePath: editorContext.filePath, + editorId: editorContext.editorId + }); + } + }); + } + + if (supportsEditorAction(editorContext, 'editor.action.wordHighlight.trigger')) { + moreItems.push({ + id: 'editor-document-highlight', + label: i18nService.t('common:editor.highlightAllOccurrences'), + icon: 'Highlighter', + onClick: () => { + globalEventBus.emit('editor:document-highlight', { + filePath: editorContext.filePath, + line: position.line, + column: position.column, + editorId: editorContext.editorId + }); + } + }); + } + + appendMenuGroup(items, 'editor-separator-more', moreItems); } return items; diff --git a/src/web-ui/src/shared/context-menu-system/types/context.types.ts b/src/web-ui/src/shared/context-menu-system/types/context.types.ts index ddc949bef4..e1bfbfd516 100644 --- a/src/web-ui/src/shared/context-menu-system/types/context.types.ts +++ b/src/web-ui/src/shared/context-menu-system/types/context.types.ts @@ -105,6 +105,9 @@ export interface EditorContext extends BaseContext { startColumn: number; endColumn: number; }; + + /** Monaco actions supported by the concrete editor at menu-open time. */ + supportedActionIds?: string[]; isReadOnly?: boolean; } diff --git a/src/web-ui/src/shared/services/EditorJumpService.ts b/src/web-ui/src/shared/services/EditorJumpService.ts index 941198c285..d90d6d0a76 100644 --- a/src/web-ui/src/shared/services/EditorJumpService.ts +++ b/src/web-ui/src/shared/services/EditorJumpService.ts @@ -57,7 +57,7 @@ class EditorJumpService { } - private getEditor(filePath: string): any | null { + getEditor(filePath: string): any | null { const normalized = this.normalizePath(filePath); return this.editors.get(normalized)?.editor || null; } diff --git a/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx b/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx index 3befbfd527..7b21ce285d 100644 --- a/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx +++ b/src/web-ui/src/tools/bitfun-canvas/BitfunCanvasPanel.tsx @@ -1058,7 +1058,6 @@ export const BitfunCanvasPanel: React.FC = ({ showBreadcrumb={false} showLineNumbers showMinimap - enableLsp={false} isActiveTab={sourceVisible} className="bitfun-canvas-panel__source-code-editor" /> diff --git a/src/web-ui/src/tools/editor/components/CodeEditor.tsx b/src/web-ui/src/tools/editor/components/CodeEditor.tsx index 3eba368dc7..441b1942aa 100644 --- a/src/web-ui/src/tools/editor/components/CodeEditor.tsx +++ b/src/web-ui/src/tools/editor/components/CodeEditor.tsx @@ -13,8 +13,6 @@ import { getMonacoRuntime, monacoApi } from '../services/monacoRuntime'; import { monacoModelManager } from '../services/MonacoModelManager'; import { activeEditTargetService, createMonacoEditTarget } from '../services/ActiveEditTargetService'; import { monacoAppearanceAdapter } from '@/infrastructure/appearance/adapters/MonacoAppearanceAdapter'; -import { useMonacoLsp } from '@/tools/lsp/hooks/useMonacoLsp'; -import { lspExtensionRegistry } from '@/tools/lsp/services/LspExtensionRegistry'; import { globalEventBus } from '@/infrastructure/event-bus'; import { configManager } from '@/infrastructure/config/services/ConfigManager'; import { EditorConfig as EditorConfigType } from '@/infrastructure/config/types'; @@ -81,8 +79,6 @@ export interface CodeEditorProps { onContentChange?: (content: string, hasChanges: boolean) => void; /** Save callback */ onSave?: (content: string) => void; - /** Enable LSP support */ - enableLsp?: boolean; /** Jump to line number (deprecated, use jumpToRange) */ jumpToLine?: number; /** Jump to column (deprecated, use jumpToRange) */ @@ -169,7 +165,6 @@ const CodeEditor: React.FC = ({ className = '', onContentChange, onSave, - enableLsp = true, jumpToLine, jumpToColumn, jumpToRange, @@ -230,9 +225,7 @@ const CodeEditor: React.FC = ({ const [detectedLanguage, setDetectedLanguage] = useState(() => { return fileName ? detectLanguageFromFileName(fileName) : language; }); - const [lspReady, setLspReady] = useState(false); const [monacoReady, setMonacoReady] = useState(false); - const [editorInstance, setEditorInstance] = useState(null); const [editorConfig, setEditorConfig] = useState>({ font_size: 14, font_family: "'Fira Code', Consolas, 'Courier New', monospace", @@ -620,14 +613,6 @@ const CodeEditor: React.FC = ({ }; }, [filePath, largeFileMode, shouldBlockLargeFileExpansionClick]); - useMonacoLsp( - editorInstance, - detectedLanguage, - filePath, - enableLsp && lspReady && monacoReady && !largeFileMode, - workspacePath - ); - useEffect(() => { if (!containerRef.current) { return; @@ -737,20 +722,12 @@ const CodeEditor: React.FC = ({ padding: false }, - hover: (() => { - // Only enable hover for languages with actual hover providers: - // - Languages supported by our LSP plugins - // - Languages with useful built-in Monaco hover (TS/JS) - const monacoHoverLanguages = ['typescript','javascript','typescriptreact','javascriptreact']; - const hasHoverSupport = lspExtensionRegistry.isFileSupported(filePath) - || monacoHoverLanguages.includes(detectedLanguage); - return { - enabled: hasHoverSupport, - delay: 100, - sticky: true, - above: false - }; - })(), + hover: { + enabled: !initialLargeFileMode, + delay: 100, + sticky: true, + above: false + }, quickSuggestions: { other: !initialLargeFileMode, @@ -795,7 +772,6 @@ const CodeEditor: React.FC = ({ editor = monacoApi.editor.create(container, editorOptions); editorRef.current = editor; - setEditorInstance(editor); const editTarget = createMonacoEditTarget(editor); const unbindEditTarget = activeEditTargetService.bindTarget(editTarget); const focusDisposable = editor.onDidFocusEditorText(() => { @@ -819,16 +795,7 @@ const CodeEditor: React.FC = ({ (container as any).__monacoEditor = editor; - if (model) { - const { lspDocumentService } = await import('@/tools/lsp/services/LspDocumentService'); - lspDocumentService.associateEditor(model.uri.toString(), editor); - } - - const hasContent = model && model.getValue().length > 0; setMonacoReady(true); - if (hasContent) { - setLspReady(true); - } const applyOptionsFromConfig = (c: Partial) => { const fs = c.font_size ?? 14; editor!.updateOptions({ @@ -1015,10 +982,6 @@ const CodeEditor: React.FC = ({ log.error('Failed to load EditorReadyManager', err); }); - if (!loadingRef.current && contentRef.current) { - setLspReady(true); - } - } catch (error) { log.error('Failed to initialize editor', error); setError(tRef.current('editor.codeEditor.initFailedWithMessage', { message: String(error) })); @@ -1046,15 +1009,8 @@ const CodeEditor: React.FC = ({ } if (editorRef.current) { - if (modelRef.current) { - import('@/tools/lsp/services/LspDocumentService').then(({ lspDocumentService }) => { - lspDocumentService.disassociateEditor(modelRef.current!.uri.toString()); - }); - } - editorRef.current.dispose(); editorRef.current = null; - setEditorInstance(null); } if (container) { @@ -1083,40 +1039,6 @@ const CodeEditor: React.FC = ({ } }, [monacoReady, applyExternalContentToModel]); - useEffect(() => { - if (!content || lspReady) { - return; - } - - let cancelled = false; - void (async () => { - if (!enableLsp || largeFileMode) { - if (!cancelled) { - setLspReady(true); - } - return; - } - - try { - const { ensureWorkspaceLspInitialized, initializeLsp } = await import('@/tools/lsp/initializeLsp'); - await initializeLsp(); - if (lspExtensionRegistry.isFileSupported(filePath)) { - await ensureWorkspaceLspInitialized(workspacePath); - } - } catch (error) { - log.warn('Failed to initialize LSP on editor open', { filePath, workspacePath, error }); - } - - if (!cancelled) { - setLspReady(true); - } - })(); - - return () => { - cancelled = true; - }; - }, [content, enableLsp, filePath, largeFileMode, lspReady, workspacePath]); - useEffect(() => { if (modelRef.current && monacoReady) { const currentLanguage = modelRef.current.getLanguageId(); @@ -2000,161 +1922,94 @@ const CodeEditor: React.FC = ({ const unsubscribers: Array<() => void> = []; - const unsubGotoDef = globalEventBus.on('editor:goto-definition', async (data: any) => { - const isMatch = isSamePath(data.filePath || '', filePath || ''); - - if (isMatch) { - try { - const position = editor.getPosition(); - if (!position) { - return; - } - - const model = editor.getModel(); - if (!model) { - return; - } - - const { GlobalAdapterRegistry } = await import('@/tools/lsp/services/MonacoLspAdapter'); - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - - if (!adapter) { - editor.trigger('keyboard', 'editor.action.revealDefinition', null); - return; - } - - const definition = await adapter.provideDefinition(model, position); - - if (!definition) { - return; - } - - const definitionUri = definition.uri.toString(); - const currentUri = model.uri.toString(); - - // Determine if it's a cross-file jump - if (definitionUri !== currentUri) { - // Cross-file jump: use unified file tab manager - const targetLine = definition.range.startLineNumber; - const targetColumn = definition.range.startColumn; - - // Use unified file tab manager to open file - const { fileTabManager } = await import('@/shared/services/FileTabManager'); - fileTabManager.openFileAndJump( - definitionUri, - targetLine, - targetColumn, - { workspacePath } - ); - } else { - // Same-file jump: use Monaco default behavior - editor.setPosition({ - lineNumber: definition.range.startLineNumber, - column: definition.range.startColumn - }); - editor.revealPositionInCenter({ - lineNumber: definition.range.startLineNumber, - column: definition.range.startColumn - }); - editor.focus(); - } - - } catch (error) { - log.error('Goto definition failed', error); - } + const matchesEventFile = (data: { filePath?: string }): boolean => + isSamePath(data.filePath || '', filePath || ''); + + const runSupportedEditorAction = async ( + actionId: string, + failureMessage: string + ): Promise => { + const action = editor.getAction(actionId); + if (!action?.isSupported()) { + return; + } + try { + await action.run(); + } catch (error) { + log.error(failureMessage, error); + } + }; + + const unsubGotoDef = globalEventBus.on('editor:goto-definition', (data: any) => { + if (matchesEventFile(data)) { + void runSupportedEditorAction( + 'editor.action.revealDefinition', + 'Goto definition failed' + ); } }); unsubscribers.push(unsubGotoDef); const unsubGotoTypeDef = globalEventBus.on('editor:goto-type-definition', (data: any) => { - if (data.filePath === filePath) { - try { - editor.trigger('context-menu', 'editor.action.goToTypeDefinition', null); - } catch (error) { - log.error('Failed to trigger goToTypeDefinition', error); - const action = editor.getAction('editor.action.goToTypeDefinition'); - if (action) { - action.run(); - } - } + if (matchesEventFile(data)) { + void runSupportedEditorAction( + 'editor.action.goToTypeDefinition', + 'Goto type definition failed' + ); } }); unsubscribers.push(unsubGotoTypeDef); - const unsubFindRefs = globalEventBus.on('editor:find-references', async (data: any) => { - if (data.filePath === filePath) { - try { - editor.trigger('context-menu', 'editor.action.referenceSearch.trigger', null); - } catch (error) { - log.error('Find references failed', error); - } + const unsubFindRefs = globalEventBus.on('editor:find-references', (data: any) => { + if (matchesEventFile(data)) { + void runSupportedEditorAction( + 'editor.action.referenceSearch.trigger', + 'Find references failed' + ); } }); unsubscribers.push(unsubFindRefs); const unsubRename = globalEventBus.on('editor:rename-symbol', (data: any) => { - if (data.filePath === filePath) { - try { - editor.trigger('context-menu', 'editor.action.rename', null); - } catch (error) { - log.error('Failed to trigger rename', error); - const action = editor.getAction('editor.action.rename'); - if (action) { - action.run(); - } - } + if (matchesEventFile(data)) { + void runSupportedEditorAction('editor.action.rename', 'Rename symbol failed'); } }); unsubscribers.push(unsubRename); const unsubFormat = globalEventBus.on('editor:format-document', (data: any) => { - if (data.filePath === filePath) { - try { - editor.trigger('context-menu', 'editor.action.formatDocument', null); - } catch (error) { - log.error('Failed to trigger formatDocument', error); - const action = editor.getAction('editor.action.formatDocument'); - if (action) { - action.run(); - } - } + if (matchesEventFile(data)) { + void runSupportedEditorAction( + 'editor.action.formatDocument', + 'Format document failed' + ); } }); unsubscribers.push(unsubFormat); const unsubCodeAction = globalEventBus.on('editor:code-action', (data: any) => { - if (data.filePath === filePath) { - try { - editor.trigger('context-menu', 'editor.action.quickFix', null); - } catch (error) { - log.error('Failed to trigger quickFix', error); - const action = editor.getAction('editor.action.quickFix'); - if (action) { - action.run(); - } - } + if (matchesEventFile(data)) { + void runSupportedEditorAction('editor.action.quickFix', 'Quick fix failed'); } }); unsubscribers.push(unsubCodeAction); const unsubDocSymbols = globalEventBus.on('editor:document-symbols', (data: any) => { - if (data.filePath === filePath) { - const action = editor.getAction('editor.action.quickOutline'); - if (action) { - action.run(); - } + if (matchesEventFile(data)) { + void runSupportedEditorAction( + 'editor.action.quickOutline', + 'Open document symbols failed' + ); } }); unsubscribers.push(unsubDocSymbols); const unsubDocHighlight = globalEventBus.on('editor:document-highlight', (data: any) => { - if (data.filePath === filePath) { - const position = editor.getPosition(); - if (position) { - editor.setPosition(position); - editor.focus(); - } + if (matchesEventFile(data)) { + void runSupportedEditorAction( + 'editor.action.wordHighlight.trigger', + 'Highlight occurrences failed' + ); } }); unsubscribers.push(unsubDocHighlight); @@ -2283,7 +2138,6 @@ const CodeEditor: React.FC = ({ const newLanguage = detectLanguageFromFileName(fileName); if (newLanguage !== detectedLanguage) { setDetectedLanguage(newLanguage); - setLspReady(false); } }, [fileName, detectedLanguage, detectLanguageFromFileName]); @@ -2363,11 +2217,6 @@ const CodeEditor: React.FC = ({ tabSize={editorConfig.tab_size || 2} insertSpaces={editorConfig.insert_spaces !== false} isReadOnly={readOnly} - lspStatus={ - enableLsp && lspExtensionRegistry.isFileSupported(filePath) - ? (lspReady ? 'connected' : 'connecting') - : undefined - } onPositionClick={(e) => openStatusBarPopover('position', e)} onIndentClick={(e) => openStatusBarPopover('indent', e)} onEncodingClick={(e) => openStatusBarPopover('encoding', e)} diff --git a/src/web-ui/src/tools/editor/components/DiffEditor.tsx b/src/web-ui/src/tools/editor/components/DiffEditor.tsx index 3b67883639..c8fad5bca8 100644 --- a/src/web-ui/src/tools/editor/components/DiffEditor.tsx +++ b/src/web-ui/src/tools/editor/components/DiffEditor.tsx @@ -7,7 +7,6 @@ import { monacoApi } from '../services/monacoRuntime'; import { monacoAppearanceAdapter } from '@/infrastructure/appearance/adapters/MonacoAppearanceAdapter'; import { configManager } from '@/infrastructure/config/services/ConfigManager'; import { EditorConfig as EditorConfigType } from '@/infrastructure/config/types'; -import { useMonacoLsp } from '@/tools/lsp/hooks/useMonacoLsp'; import { getMonacoLanguage } from '@/infrastructure/language-detection'; import { Tooltip, CubeLoading } from '@/component-library'; import { useNotification } from '@/shared/notification-system'; @@ -26,8 +25,6 @@ export interface DiffEditorProps { modifiedContent: string; /** File path */ filePath?: string; - /** Workspace path (reserved for future use) */ - workspacePath?: string; /** Repository path (for Git Diff) */ repositoryPath?: string; /** Programming language */ @@ -54,8 +51,6 @@ export interface DiffEditorProps { onSave?: (content: string) => void; /** Reveal line in modified editor (1-based) */ revealLine?: number; - /** Enable LSP (only for modified editor) */ - enableLsp?: boolean; /** Show +/- indicators before lines (default true) */ renderIndicators?: boolean; } @@ -64,7 +59,6 @@ export const DiffEditor: React.FC = ({ originalContent, modifiedContent, filePath, - workspacePath, repositoryPath: _repositoryPath, language: propLanguage, readOnly = false, @@ -77,7 +71,6 @@ export const DiffEditor: React.FC = ({ onRejectChange: _onRejectChange, enableCustomToolbar = false, revealLine, - enableLsp = true, renderIndicators = true, onSave }) => { @@ -104,7 +97,6 @@ export const DiffEditor: React.FC = ({ const changeListenerRef = useRef(null); const contentChangeListenerRef = useRef(null); const isUnmountedRef = useRef(false); - const [modifiedEditorInstance, setModifiedEditorInstance] = useState(null); const onSaveRef = useRef(onSave); const originalContentRuntimeRef = useRef(originalContent); const modifiedContentRuntimeRef = useRef(modifiedContent); @@ -138,14 +130,6 @@ export const DiffEditor: React.FC = ({ const detectedLanguage = useMemo(() => detectLanguage(filePath), [filePath, detectLanguage]); - useMonacoLsp( - modifiedEditorInstance, - detectedLanguage, - filePath || '', - Boolean(enableLsp && modifiedEditorInstance && filePath), - workspacePath - ); - useEffect(() => { const loadEditorConfig = async () => { try { @@ -269,7 +253,6 @@ export const DiffEditor: React.FC = ({ modified: modifiedModel }); - setModifiedEditorInstance(editor.getModifiedEditor()); setDiffEditor(editor); const originalEditor = editor.getOriginalEditor(); @@ -396,7 +379,6 @@ export const DiffEditor: React.FC = ({ originalModelRef.current = null; modifiedModelRef.current = null; - setModifiedEditorInstance(null); }; }, [filePath, detectedLanguage, renderSideBySide, readOnly, showMinimap]); diff --git a/src/web-ui/src/tools/editor/components/EditorStatusBar.appearance.ts b/src/web-ui/src/tools/editor/components/EditorStatusBar.appearance.ts index 5235153f1b..1606da83b6 100644 --- a/src/web-ui/src/tools/editor/components/EditorStatusBar.appearance.ts +++ b/src/web-ui/src/tools/editor/components/EditorStatusBar.appearance.ts @@ -3,6 +3,6 @@ export const editorStatusBarAppearanceDescriptor: AppearanceSurfaceDescriptor = id: 'editor-status-bar', parts: [ { id: 'root' }, { id: 'left' }, { id: 'right' }, { id: 'item' }, - { id: 'selection' }, { id: 'separator' }, { id: 'lsp' }, + { id: 'selection' }, { id: 'separator' }, ], }; diff --git a/src/web-ui/src/tools/editor/components/EditorStatusBar.scss b/src/web-ui/src/tools/editor/components/EditorStatusBar.scss index 47dd9a85fd..a497510ea9 100644 --- a/src/web-ui/src/tools/editor/components/EditorStatusBar.scss +++ b/src/web-ui/src/tools/editor/components/EditorStatusBar.scss @@ -89,43 +89,6 @@ $_section-gap: 12px; letter-spacing: 0.5px; } - // LSP Status - &__lsp { - padding: 2px 4px; - - &--connected { - color: var(--bf-appearance-token-color-success); - } - - &--connecting { - color: var(--bf-appearance-token-color-warning); - } - - &--disconnected { - color: var(--bf-appearance-token-color-text-muted); - opacity: 0.6; - } - } - - &__lsp-spinner { - animation: status-spin 1s linear infinite; - } -} - -// Animations -@keyframes status-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: reduce) { - .editor-status-bar__lsp-spinner { - animation: none; - } } // Responsive diff --git a/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx b/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx index 1e787b3540..5a97996fd9 100644 --- a/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx +++ b/src/web-ui/src/tools/editor/components/EditorStatusBar.tsx @@ -1,11 +1,6 @@ -/** Status bar for cursor position, language, encoding, and LSP status. */ +/** Status bar for cursor position, language, and encoding. */ import React from 'react'; -import { - AlertCircle, - Loader2, - Zap -} from 'lucide-react'; import { Tooltip } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n'; import './EditorStatusBar.scss'; @@ -33,8 +28,6 @@ export interface EditorStatusBarProps { isSaving?: boolean; /** Whether file is read-only */ isReadOnly?: boolean; - /** LSP connection status */ - lspStatus?: 'connected' | 'disconnected' | 'connecting'; /** Language click callback */ onLanguageClick?: (e: React.MouseEvent) => void; /** Encoding click callback */ @@ -90,33 +83,6 @@ const getLanguageDisplayName = (language: string): string => { return languageMap[language.toLowerCase()] || language; }; -const getLspStatusInfo = ( - status: 'connected' | 'disconnected' | 'connecting' | undefined, - t: (key: string) => string -) => { - switch (status) { - case 'connected': - return { - icon: , - className: 'editor-status-bar__lsp--connected', - title: t('editor.statusBar.lspConnected') - }; - case 'connecting': - return { - icon: , - className: 'editor-status-bar__lsp--connecting', - title: t('editor.statusBar.lspConnecting') - }; - case 'disconnected': - default: - return { - icon: , - className: 'editor-status-bar__lsp--disconnected', - title: t('editor.statusBar.lspDisconnected') - }; - } -}; - export const EditorStatusBar: React.FC = ({ line, column, @@ -127,14 +93,12 @@ export const EditorStatusBar: React.FC = ({ tabSize = 2, insertSpaces = true, isReadOnly = false, - lspStatus, onLanguageClick, onEncodingClick, onIndentClick, onPositionClick, }) => { const { t } = useI18n('tools'); - const lspInfo = getLspStatusInfo(lspStatus, t); // Build selection info text (updates with language changes). const getSelectionText = () => { @@ -211,19 +175,6 @@ export const EditorStatusBar: React.FC = ({ - {lspStatus && ( - <> -
-
- {lspInfo.icon} -
- - )}
); diff --git a/src/web-ui/src/tools/editor/config/presets.ts b/src/web-ui/src/tools/editor/config/presets.ts index 6d9b4df726..c138d2e8cd 100644 --- a/src/web-ui/src/tools/editor/config/presets.ts +++ b/src/web-ui/src/tools/editor/config/presets.ts @@ -7,7 +7,6 @@ import type { EditorPresetConfig, EditorPresetName } from './types'; /** Readonly preset: chat code blocks, doc preview, code display */ export const PRESET_READONLY: EditorPresetConfig = { readOnly: true, - enableLsp: false, contextmenu: false, links: true, folding: true, @@ -41,7 +40,6 @@ export const PRESET_READONLY: EditorPresetConfig = { /** Minimal preset: config editing, code snippets, small editors */ export const PRESET_MINIMAL: EditorPresetConfig = { readOnly: false, - enableLsp: false, contextmenu: true, links: true, folding: false, @@ -66,7 +64,6 @@ export const PRESET_MINIMAL: EditorPresetConfig = { /** Standard preset: general code editing */ export const PRESET_STANDARD: EditorPresetConfig = { readOnly: false, - enableLsp: true, contextmenu: true, links: true, folding: true, @@ -83,7 +80,6 @@ export const PRESET_STANDARD: EditorPresetConfig = { /** Full preset: main file editor with all features */ export const PRESET_FULL: EditorPresetConfig = { readOnly: false, - enableLsp: true, contextmenu: true, links: true, folding: true, @@ -122,7 +118,6 @@ export const PRESET_FULL: EditorPresetConfig = { /** Diff preset: code diff comparison */ export const PRESET_DIFF: EditorPresetConfig = { readOnly: false, - enableLsp: true, contextmenu: false, links: true, folding: false, diff --git a/src/web-ui/src/tools/editor/config/types.ts b/src/web-ui/src/tools/editor/config/types.ts index 7fd953cfed..afa8a1dde7 100644 --- a/src/web-ui/src/tools/editor/config/types.ts +++ b/src/web-ui/src/tools/editor/config/types.ts @@ -110,7 +110,6 @@ export type EditorPresetName = 'readonly' | 'minimal' | 'standard' | 'full' | 'd /** Preset config including runtime properties */ export interface EditorPresetConfig extends EditorConfigPartial { readOnly?: boolean; - enableLsp?: boolean; contextmenu?: boolean; links?: boolean; folding?: boolean; diff --git a/src/web-ui/src/tools/editor/core/MonacoDiffCore.tsx b/src/web-ui/src/tools/editor/core/MonacoDiffCore.tsx index 190d876e30..a75bba34f3 100644 --- a/src/web-ui/src/tools/editor/core/MonacoDiffCore.tsx +++ b/src/web-ui/src/tools/editor/core/MonacoDiffCore.tsx @@ -49,7 +49,6 @@ export const MonacoDiffCore = forwardRef renderIndicators = true, originalEditable = false, ignoreTrimWhitespace = false, - enableLsp: _enableLsp = false, showMinimap = false, onModifiedContentChange, onDiffChange, diff --git a/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx b/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx index a40be0acfe..7d3ad21c11 100644 --- a/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx +++ b/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx @@ -4,7 +4,7 @@ * Wraps monaco.editor.create(), integrates with MonacoModelManager, * exposes editor ref, proxies events, and calls ExtensionManager lifecycle hooks. * - * Does not include: file IO, LSP integration (via Extension), UI components. + * Does not include file IO or UI components. */ import { useEffect, useRef, useCallback, useState, forwardRef, useImperativeHandle } from 'react'; @@ -44,7 +44,6 @@ export const MonacoEditorCore = forwardRef { diff --git a/src/web-ui/src/tools/editor/core/types.ts b/src/web-ui/src/tools/editor/core/types.ts index bc7f0630be..07dc33eae2 100644 --- a/src/web-ui/src/tools/editor/core/types.ts +++ b/src/web-ui/src/tools/editor/core/types.ts @@ -6,7 +6,7 @@ import type * as monaco from 'monaco-editor'; import type { EditorPresetName, EditorConfigPartial } from '../config/types'; export interface MonacoEditorCoreProps { - /** File path for Model management and LSP */ + /** File path for model management */ filePath: string; workspacePath?: string; language?: string; @@ -19,7 +19,6 @@ export interface MonacoEditorCoreProps { readOnly?: boolean; // Features - enableLsp?: boolean; showLineNumbers?: boolean; showMinimap?: boolean; @@ -64,8 +63,6 @@ export interface MonacoDiffCoreProps { ignoreTrimWhitespace?: boolean; // Features - /** LSP for modified only */ - enableLsp?: boolean; showMinimap?: boolean; // Event callbacks diff --git a/src/web-ui/src/tools/editor/extensions/LspExtension.ts b/src/web-ui/src/tools/editor/extensions/LspExtension.ts deleted file mode 100644 index 1b99b863cd..0000000000 --- a/src/web-ui/src/tools/editor/extensions/LspExtension.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * LSP Extension - * - * Encapsulates LSP functionality as an editor extension. - * Auto-registers LSP adapter when editor is created. - * @module LspExtension - */ - -import type * as monaco from 'monaco-editor'; -import { createLogger } from '@/shared/utils/logger'; -import type { EditorExtension, EditorExtensionContext } from '../services/EditorExtensionManager'; -import { ExtensionPriority, type LspExtensionConfig } from './types'; - -const log = createLogger('LspExtension'); - -/** Languages with Monaco built-in services - skip external LSP for these */ -const MONACO_BUILTIN_LANGUAGES = [ - 'typescript', - 'javascript', - 'typescriptreact', - 'javascriptreact', - 'json', - 'html', - 'css', - 'scss', - 'less', -]; - -/** Create LSP extension instance */ -export function createLspExtension(config: LspExtensionConfig = { enabled: true }): EditorExtension { - const skipLanguages = new Set([ - ...MONACO_BUILTIN_LANGUAGES, - ...(config.skipLanguages || []), - ]); - - return { - id: 'bitfun.lsp', - name: 'LSP Extension', - priority: ExtensionPriority.HIGH, - - onEditorCreated( - editor: monaco.editor.IStandaloneCodeEditor, - model: monaco.editor.ITextModel, - context: EditorExtensionContext - ): void { - if (!config.enabled || !context.enableLsp) { - return; - } - - if (skipLanguages.has(context.language)) { - return; - } - - const workspacePath = context.workspacePath || config.workspacePath; - if (!workspacePath) { - log.warn('No workspace path provided, LSP will not be enabled'); - return; - } - - void (async () => { - try { - // Dynamic import to avoid circular dependencies - const { lspAdapterManager } = await import('@/tools/lsp/services/LspAdapterManager'); - const { lspExtensionRegistry } = await import('@/tools/lsp/services/LspExtensionRegistry'); - - if (!lspExtensionRegistry.isInitialized()) { - log.warn('LSP extension registry not initialized yet'); - return; - } - - if (!lspExtensionRegistry.isLanguageSupported(context.language)) { - return; - } - - lspAdapterManager.getOrCreateAdapter( - model, - context.language, - context.filePath, - workspacePath - ); - - lspAdapterManager.registerEditor(model, editor); - } catch (error) { - log.error('Failed to initialize LSP', error); - } - })(); - }, - - onEditorWillDispose( - editor: monaco.editor.IStandaloneCodeEditor, - model: monaco.editor.ITextModel, - _context: EditorExtensionContext - ): void { - void import('@/tools/lsp/services/LspAdapterManager') - .then(({ lspAdapterManager }) => { - lspAdapterManager.unregisterEditor(model, editor); - }) - .catch(error => { - log.error('Failed to dispose LSP editor binding', error); - }); - }, - }; -} - -/** Default LSP extension instance */ -export const lspExtension = createLspExtension(); - -export default lspExtension; diff --git a/src/web-ui/src/tools/editor/extensions/index.ts b/src/web-ui/src/tools/editor/extensions/index.ts index 9960d50c3a..47a22a0658 100644 --- a/src/web-ui/src/tools/editor/extensions/index.ts +++ b/src/web-ui/src/tools/editor/extensions/index.ts @@ -6,7 +6,6 @@ export { ExtensionPriority, type EditorExtension, type EditorExtensionContext, - type LspExtensionConfig, type AiCompletionExtensionConfig, type TabCompletionExtensionConfig, type ExtensionState, @@ -14,5 +13,3 @@ export { type DecorationsExtension, type CommandsExtension, } from './types'; - -export { createLspExtension, lspExtension } from './LspExtension'; diff --git a/src/web-ui/src/tools/editor/extensions/types.ts b/src/web-ui/src/tools/editor/extensions/types.ts index 91c0a52b80..278364802e 100644 --- a/src/web-ui/src/tools/editor/extensions/types.ts +++ b/src/web-ui/src/tools/editor/extensions/types.ts @@ -14,7 +14,7 @@ export type { EditorExtension, EditorExtensionContext } from '../services/Editor export const ExtensionPriority = { /** Highest priority - core functionality */ CRITICAL: 0, - /** High priority - language services like LSP */ + /** High priority - language services */ HIGH: 10, /** Medium priority - AI completion and enhancements */ MEDIUM: 50, @@ -24,16 +24,6 @@ export const ExtensionPriority = { OPTIONAL: 200, } as const; -/** LSP extension configuration */ -export interface LspExtensionConfig { - /** Whether enabled */ - enabled: boolean; - /** Workspace path */ - workspacePath?: string; - /** Languages to skip (use Monaco built-in services) */ - skipLanguages?: string[]; -} - /** AI completion extension configuration (reserved) */ export interface AiCompletionExtensionConfig { /** Whether enabled */ diff --git a/src/web-ui/src/tools/editor/hooks/index.ts b/src/web-ui/src/tools/editor/hooks/index.ts index 5a36a869bf..3f97842cdf 100644 --- a/src/web-ui/src/tools/editor/hooks/index.ts +++ b/src/web-ui/src/tools/editor/hooks/index.ts @@ -8,4 +8,3 @@ export { useSimpleEditorOptions, useDiffEditorOptions, } from './useEditorOptions'; -export { useLspInitialization } from './useLspIntegration'; diff --git a/src/web-ui/src/tools/editor/hooks/useLspIntegration.ts b/src/web-ui/src/tools/editor/hooks/useLspIntegration.ts deleted file mode 100644 index 908512a1e2..0000000000 --- a/src/web-ui/src/tools/editor/hooks/useLspIntegration.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * LSP Integration Hook - Provides convenient LSP integration for the editor. - * @module useLspIntegration - */ - -import { useEffect } from 'react'; -import { createLogger } from '@/shared/utils/logger'; -import { lspService } from '@/tools/lsp/services/LspService'; - -const log = createLogger('useLspIntegration'); - -/** - * Initializes LSP and sets the workspace root. - * Falls back gracefully if LSP fails - editor continues without language services. - */ -export function useLspInitialization(workspacePath?: string) { - useEffect(() => { - const initLsp = async () => { - try { - await lspService.initialize(); - - if (workspacePath) { - await lspService.openWorkspace(workspacePath); - } - } catch (error) { - log.warn('Failed to initialize LSP', error); - } - }; - - initLsp(); - }, [workspacePath]); -} - - - - - - - - - - - - - - - diff --git a/src/web-ui/src/tools/editor/index.ts b/src/web-ui/src/tools/editor/index.ts index 66adb8555c..03f4f3542c 100644 --- a/src/web-ui/src/tools/editor/index.ts +++ b/src/web-ui/src/tools/editor/index.ts @@ -65,12 +65,9 @@ export type { export { ExtensionPriority, - createLspExtension, - lspExtension, } from './extensions'; export type { - LspExtensionConfig, AiCompletionExtensionConfig, TabCompletionExtensionConfig, } from './extensions'; @@ -82,8 +79,6 @@ export { useDiffEditorOptions, } from './hooks'; -export { useLspInitialization } from './hooks/useLspIntegration'; - export { default as CodeEditor } from './components/CodeEditor'; export type { CodeEditorProps } from './components/CodeEditor'; @@ -116,10 +111,6 @@ export async function initializeEditorFeature(): Promise { try { const { monacoInitManager } = await import('./services/MonacoInitManager'); await monacoInitManager.initialize(); - - const { editorExtensionManager } = await import('./services/EditorExtensionManager'); - const { lspExtension } = await import('./extensions/LspExtension'); - editorExtensionManager.register(lspExtension); } catch (error) { log.error('Failed to initialize editor feature', error); throw error; @@ -129,7 +120,7 @@ export async function initializeEditorFeature(): Promise { export const EditorFeatureMetadata = { name: 'Editor', version: '2.0.0', - description: 'Code and markdown editing with Monaco and LSP support', + description: 'Code and markdown editing with Monaco', dependencies: ['core'], capabilities: [ 'code-editing', @@ -137,7 +128,6 @@ export const EditorFeatureMetadata = { 'diff-editing', 'syntax-highlighting', 'code-completion', - 'lsp-integration', 'multi-file-editing', 'search-replace', 'format-document', diff --git a/src/web-ui/src/tools/editor/services/EditorExtensionManager.ts b/src/web-ui/src/tools/editor/services/EditorExtensionManager.ts index 556e4e66c7..cb21cf756b 100644 --- a/src/web-ui/src/tools/editor/services/EditorExtensionManager.ts +++ b/src/web-ui/src/tools/editor/services/EditorExtensionManager.ts @@ -54,7 +54,6 @@ export interface EditorExtensionContext { language: string; workspacePath?: string; readOnly: boolean; - enableLsp: boolean; } interface ExtensionRegistration { diff --git a/src/web-ui/src/tools/editor/services/EditorReadyManager.ts b/src/web-ui/src/tools/editor/services/EditorReadyManager.ts index 53eddac972..3d74ccb513 100644 --- a/src/web-ui/src/tools/editor/services/EditorReadyManager.ts +++ b/src/web-ui/src/tools/editor/services/EditorReadyManager.ts @@ -125,20 +125,8 @@ class EditorReadyManager { private async getExistingEditor(filePath: string): Promise { try { - const { GlobalAdapterRegistry } = await import('@/tools/lsp/services/MonacoLspAdapter'); - const { pathToUri } = await import('@/shared/utils/pathUtils'); - const uri = pathToUri(filePath); - - const adapter = GlobalAdapterRegistry.get(uri); - - if (adapter) { - const editor = adapter.getActiveEditor(); - if (editor) { - return editor; - } - } - - return null; + const { editorJumpService } = await import('@/shared/services/EditorJumpService'); + return editorJumpService.getEditor(filePath); } catch (error) { log.warn('Failed to find existing editor', { filePath, error }); return null; diff --git a/src/web-ui/src/tools/index.ts b/src/web-ui/src/tools/index.ts index 5d57fb8e3c..32ec3d6158 100644 --- a/src/web-ui/src/tools/index.ts +++ b/src/web-ui/src/tools/index.ts @@ -4,7 +4,6 @@ export { initializeAllTools } from './initializeTools'; export * from './editor'; export * from './file-system'; export * from './git'; -export * from './lsp'; export * from './mermaid-editor'; export * from './snapshot_system'; export * from './terminal'; diff --git a/src/web-ui/src/tools/initializeTools.ts b/src/web-ui/src/tools/initializeTools.ts index f64fc014fb..a08c8e51d1 100644 --- a/src/web-ui/src/tools/initializeTools.ts +++ b/src/web-ui/src/tools/initializeTools.ts @@ -8,13 +8,11 @@ import { createLogger } from '@/shared/utils/logger'; import { initializeGit } from './git/initializeGit'; -import { initializeLsp } from './lsp/initializeLsp'; const log = createLogger('Tools'); export async function initializeAllTools(): Promise { try { - await initializeLsp(); initializeGit(); log.info('All tool modules initialized'); } catch (error) { diff --git a/src/web-ui/src/tools/lsp/README.md b/src/web-ui/src/tools/lsp/README.md deleted file mode 100644 index 7422532348..0000000000 --- a/src/web-ui/src/tools/lsp/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# LSP (Language Server Protocol) - -BitFun’s plugin-based LSP implementation: completions, hover, definition, references, formatting, diagnostics, and related editor features. - -## Scope - -This frontend directory is responsible for: - -- Monaco-facing integration hooks and services -- plugin management UI -- shared frontend types and exports - -Backend lifecycle management, plugin loading, and JSON-RPC transport live outside this directory. - -## Architecture (high level) - -- **Backend (Rust)**: owns server lifecycle, plugin loading, JSON-RPC transport, and workspace-scoped state. -- **Frontend (TypeScript/React)**: integrates LSP features into Monaco and exposes a small API surface. - -Frontend layout: - -```text -src/tools/lsp/ -├── services/ # backend calls + Monaco integration -├── hooks/ # React-facing hooks -├── components/ # UI (plugin list, references panel) -├── types/ # shared type definitions -└── index.ts # exports + initialize helper -``` - -## Usage - -### Enable Monaco LSP integration - -```ts -import { useMonacoLsp } from '@/tools/lsp'; - -useMonacoLsp(editor, languageId, filePath, true, workspacePath); -``` - -Notes: - -- `workspacePath` is required to enable non-builtin Monaco languages. -- Builtin Monaco languages such as `typescript`, `javascript`, `typescriptreact`, and `javascriptreact` use a different integration path. - -### Plugin management (UI or hook) - -```ts -import { useLspPlugins } from '@/tools/lsp'; - -const { plugins, loading, error, installPlugin, uninstallPlugin, reload } = useLspPlugins(); -``` - -Or use the ready-made component: - -```tsx -import { LspPluginList } from '@/tools/lsp'; - - -``` - -## Plugin package format - -Plugin packages are `.vcpkg` files (ZIP under the hood): - -```text -my-language-lsp-1.0.0.vcpkg -├── manifest.json -├── bin/ -│ ├── win-x64/... -│ ├── darwin-x64/... -│ └── linux-x64/... -└── config/ (optional) -``` - -`manifest.json` example: - -```json -{ - "id": "typescript-lsp", - "name": "TypeScript Language Server", - "version": "1.0.0", - "author": "Microsoft", - "description": "TypeScript and JavaScript language support", - "server": { - "command": "bin/${platform}-${arch}/typescript-language-server", - "args": ["--stdio"], - "env": {} - }, - "languages": ["typescript", "javascript"], - "file_extensions": [".ts", ".tsx", ".js", ".jsx", ".mjs"], - "capabilities": { - "completion": true, - "hover": true, - "definition": true, - "references": true, - "rename": true, - "formatting": true, - "diagnostics": true - }, - "min_bitfun_version": "1.0.0" -} -``` - -## Debugging - -- `initializeLsp()` sets up the extension registry and workspace initializer. -- `window.LspDiag` is installed as a lightweight debugging helper (see [index.ts](index.ts)). - -## Constraints and Notes - -- Keep package format and manifest fields aligned with the backend plugin loader. -- When adding new frontend LSP capabilities, verify whether the backend protocol already supports them. -- Keep this README focused on the frontend surface; backend implementation details should be documented closer to the Rust side. diff --git a/src/web-ui/src/tools/lsp/appearance.ts b/src/web-ui/src/tools/lsp/appearance.ts deleted file mode 100644 index 36cfa3f5cc..0000000000 --- a/src/web-ui/src/tools/lsp/appearance.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance'; - -export const lspAppearanceDescriptor: AppearanceSurfaceDescriptor = { - id: 'lsp', - parts: [ - { id: 'pluginList' }, - { id: 'empty' }, - { id: 'items' }, - { id: 'plugin' }, - { id: 'pluginHeader' }, - { id: 'pluginDetails' }, - ], - states: [ - { id: 'empty', selector: { kind: 'self', suffix: '[data-bf-state~="empty"]' } }, - { id: 'expanded', selector: { kind: 'self', suffix: '[data-bf-state~="expanded"]' } }, - ], -}; diff --git a/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss b/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss deleted file mode 100644 index bd24310625..0000000000 --- a/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss +++ /dev/null @@ -1,451 +0,0 @@ -/** - * LSP plugin list styles (BitFun design tokens). - */ - -@use '../../../../component-library/styles/tokens' as *; - -.lsp-plugin-list { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - padding: 0; - background: transparent; - color: var(--bf-appearance-token-color-text-primary); - font-family: $font-family-sans; - - // States: loading/error/empty - &__loading, - &__error, - &__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - flex: 1; - min-height: 300px; - gap: $size-gap-4; - padding: $size-gap-8; - text-align: center; - color: var(--bf-appearance-token-color-text-muted); - border: 1px dashed var(--bf-appearance-token-border-base); - border-radius: $size-radius-sm; - transition: border-color $motion-base $easing-standard; - - &:hover { - border-color: var(--bf-appearance-token-border-medium); - } - - p { - margin: 0; - color: var(--bf-appearance-token-color-text-secondary); - font-size: var(--bf-appearance-token-font-size-sm); - } - - svg { - opacity: 0.5; - } - - .hint { - font-size: var(--bf-appearance-token-font-size-xs); - color: var(--bf-appearance-token-color-text-muted); - } - - button { - margin-top: $size-gap-2; - min-width: 160px; - } - } - - &__error { - border-color: var(--bf-appearance-token-color-error); - - svg { - color: var(--bf-appearance-token-color-error); - opacity: 1; - } - - p { - color: var(--bf-appearance-token-color-error); - } - } - - // Header - &__header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 0 $size-gap-4 0; - border-bottom: 1px dashed var(--bf-appearance-token-border-medium); - margin-bottom: $size-gap-4; - flex-shrink: 0; - - h3 { - margin: 0; - font-size: var(--bf-appearance-token-font-size-base); - font-weight: $font-weight-semibold; - color: var(--bf-appearance-token-color-text-primary); - } - } - - &__header-actions { - display: flex; - gap: $size-gap-2; - align-items: center; - } - - &__spinning { - animation: lsp-plugin-list-spin 1s linear infinite; - } - - // List - &__items { - flex: 1; - display: flex; - flex-direction: column; - gap: $size-gap-3; - overflow-y: auto; - overflow-x: hidden; - padding: 0; - min-height: 0; - } -} - -// Item card -.lsp-plugin-item { - overflow: hidden; - - &.is-expanded { - background: var(--bf-appearance-token-element-bg-soft) !important; - } - - &__header { - display: flex; - align-items: center; - gap: $size-gap-2; - padding: $size-gap-3; - cursor: pointer; - user-select: none; - transition: background $motion-base $easing-standard; - - &:hover { - background: var(--bf-appearance-token-element-bg-subtle); - } - } - - &__toggle { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - color: var(--bf-appearance-token-color-text-muted); - transition: color $motion-fast $easing-standard; - - .is-expanded & { - color: var(--bf-appearance-token-color-accent-500); - } - } - - &__icon { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: var(--bf-appearance-token-color-accent-100); - border-radius: $size-radius-sm; - color: var(--bf-appearance-token-color-accent-500); - transition: background $motion-base $easing-standard; - - svg { - width: 14px; - height: 14px; - } - - .lsp-plugin-item:hover & { - background: var(--bf-appearance-token-color-accent-200); - } - } - - &__main { - flex: 1; - min-width: 0; - display: flex; - align-items: baseline; - gap: $size-gap-2; - } - - &__name { - font-size: var(--bf-appearance-token-font-size-base); - font-weight: $font-weight-semibold; - color: var(--bf-appearance-token-color-text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__version { - font-size: var(--bf-appearance-token-font-size-xs); - font-family: $font-family-mono; - color: var(--bf-appearance-token-color-text-muted); - flex-shrink: 0; - } - - &__badges { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: $size-gap-2; - flex-shrink: 0; - } - - &__badge { - padding: 4px $size-gap-2; - background: var(--bf-appearance-token-color-accent-100); - color: var(--bf-appearance-token-color-accent-500); - border-radius: $size-radius-sm; - font-size: var(--bf-appearance-token-font-size-xs); - font-family: $font-family-mono; - font-weight: $font-weight-medium; - white-space: nowrap; - border: none; - } - - &__badge-more { - padding: 4px $size-gap-2; - font-size: var(--bf-appearance-token-font-size-xs); - font-weight: $font-weight-medium; - color: var(--bf-appearance-token-color-text-muted); - } - - &__details { - padding: $size-gap-4; - border-top: 1px dashed var(--bf-appearance-token-border-medium); - display: flex; - flex-direction: column; - gap: $size-gap-3; - animation: lsp-plugin-item-slide-down 0.2s $easing-standard; - } - - &__section { - display: flex; - flex-direction: column; - gap: $size-gap-1; - } - - &__description { - margin: 0; - font-size: var(--bf-appearance-token-font-size-sm); - color: var(--bf-appearance-token-color-text-secondary); - line-height: $line-height-relaxed; - } - - &__label { - font-size: var(--bf-appearance-token-font-size-xs); - font-weight: $font-weight-medium; - color: var(--bf-appearance-token-color-text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - } - - &__value { - font-size: var(--bf-appearance-token-font-size-sm); - color: var(--bf-appearance-token-color-text-primary); - } - - &__tags { - display: flex; - flex-wrap: wrap; - gap: $size-gap-2; - } - - &__tag { - padding: 4px $size-gap-2; - background: var(--bf-appearance-token-element-bg-soft); - border: none; - border-radius: $size-radius-sm; - font-size: var(--bf-appearance-token-font-size-xs); - font-family: $font-family-mono; - color: var(--bf-appearance-token-color-text-primary); - transition: background-color $motion-fast $easing-standard; - - &:hover { - background: var(--bf-appearance-token-element-bg-medium); - } - } - - &__capabilities { - display: flex; - flex-wrap: wrap; - gap: $size-gap-2; - } - - &__capability { - display: flex; - align-items: center; - gap: 4px; - padding: 4px $size-gap-2; - background: var(--bf-appearance-token-color-accent-100); - border: none; - border-radius: $size-radius-sm; - font-size: var(--bf-appearance-token-font-size-xs); - color: var(--bf-appearance-token-color-accent-500); - - svg { - flex-shrink: 0; - } - } - - &__actions { - display: flex; - gap: $size-gap-2; - padding-top: $size-gap-2; - border-top: 1px dashed var(--bf-appearance-token-border-base); - margin-top: $size-gap-2; - } -} - -// Spinner -.spinner { - width: 40px; - height: 40px; - border: 3px solid var(--bf-appearance-token-border-base); - border-top-color: var(--bf-appearance-token-color-accent-500); - border-radius: 50%; - animation: lsp-plugin-list-spin 1s linear infinite; -} - -@keyframes lsp-plugin-list-spin { - to { - transform: rotate(360deg); - } -} - -@keyframes lsp-plugin-item-slide-down { - from { - opacity: 0; - transform: translateY(-8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@media (prefers-reduced-motion: reduce) { - .lsp-plugin-list__spinning, - .lsp-plugin-list .spinner, - .lsp-plugin-item__details { - animation: none; - } -} - -// Responsive -@container config-panel (max-width: 520px) { - .lsp-plugin-list { - &__loading, - &__error, - &__empty { - min-height: 200px; - padding: $size-gap-5; - gap: $size-gap-3; - - > svg { - width: 48px; - height: 48px; - } - - p { - font-size: var(--bf-appearance-token-font-size-sm); - } - } - - &__header { - padding: 0 0 $size-gap-3 0; - margin-bottom: $size-gap-3; - - h3 { - font-size: var(--bf-appearance-token-font-size-sm); - } - } - - &__items { - gap: $size-gap-2; - } - } - - .lsp-plugin-item { - &__header { - padding: $size-gap-2; - gap: $size-gap-2; - } - - &__icon { - width: 24px; - height: 24px; - - svg { - width: 12px; - height: 12px; - } - } - - &__name { - font-size: var(--bf-appearance-token-font-size-sm); - } - - &__version { - font-size: var(--bf-appearance-token-font-size-xs); - } - - &__badges { - display: none; - } - - &__details { - padding: $size-gap-3; - gap: $size-gap-2; - } - - &__description { - font-size: var(--bf-appearance-token-font-size-xs); - } - - &__label { - font-size: var(--bf-appearance-token-font-size-xs); - } - - &__value { - font-size: var(--bf-appearance-token-font-size-xs); - } - - &__tags { - gap: $size-gap-1; - } - - &__tag { - padding: 3px 6px; - font-size: var(--bf-appearance-token-font-size-xs); - } - - &__capabilities { - gap: $size-gap-1; - } - - &__capability { - padding: 3px 6px; - font-size: var(--bf-appearance-token-font-size-xs); - } - - &__actions { - gap: $size-gap-2; - padding-top: $size-gap-2; - margin-top: $size-gap-2; - } - } -} - -// >= 520px: default styles diff --git a/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.tsx b/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.tsx deleted file mode 100644 index 1b126d751b..0000000000 --- a/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.tsx +++ /dev/null @@ -1,236 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -/** - * LSP plugin list UI. - */ - -import React, { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Package, Trash2, CheckCircle, AlertCircle, ChevronDown, ChevronRight, Upload } from 'lucide-react'; -import { Button, Card, CardBody } from '@/component-library'; -import { useLspPlugins } from '../../hooks/useLsp'; -import type { LspPlugin } from '../../types'; -import { useNotification } from '@/shared/notification-system'; -import './LspPluginList.scss'; - -export interface LspPluginListProps { - className?: string; - onInitialize?: () => void; - onInstallPlugin?: () => void; - isInitializing?: boolean; - isInstalling?: boolean; - /** Passes the internal reload function to the parent after mount. */ - onMountReload?: (reload: () => void) => void; -} - -export const LspPluginList: React.FC = ({ - className, - onInstallPlugin, - isInstalling = false, - onMountReload, -}) => { - const { t } = useTranslation('settings/lsp'); - const { plugins, loading, error, reload, uninstallPlugin } = useLspPlugins(); - const [expandedPlugins, setExpandedPlugins] = useState>(new Set()); - const notification = useNotification(); - - useEffect(() => { - onMountReload?.(reload); - }, [reload, onMountReload]); - - const togglePlugin = (pluginId: string) => { - setExpandedPlugins(prev => { - const next = new Set(prev); - if (next.has(pluginId)) { - next.delete(pluginId); - } else { - next.add(pluginId); - } - return next; - }); - }; - - const handleUninstall = async (pluginId: string) => { - if (!confirm(t('pluginList.confirmUninstall', { pluginId }))) { - return; - } - - const success = await uninstallPlugin(pluginId); - if (success) { - notification.success(t('pluginList.uninstallSuccess')); - } else { - notification.error(t('pluginList.uninstallFailed')); - } - }; - - if (loading) { - return ( -
-
-
-

{t('pluginList.loading')}

-
-
- ); - } - - if (error) { - return ( -
-
- -

{error}

- -
-
- ); - } - - if (plugins.length === 0) { - return ( -
-
- - {onInstallPlugin && ( - - )} -
-
- ); - } - - return ( -
-
- {plugins.map(plugin => ( - togglePlugin(plugin.id)} - onUninstall={() => handleUninstall(plugin.id)} - t={t} - /> - ))} -
-
- ); -}; -interface PluginItemProps { - plugin: LspPlugin; - isExpanded: boolean; - onToggle: () => void; - onUninstall: () => void; - t: (key: string) => string; -} - -const PluginItem: React.FC = ({ plugin, isExpanded, onToggle, onUninstall, t }) => { - return ( - -
-
- {isExpanded ? : } -
- -
- -
- -
- {plugin.name} - v{plugin.version} -
- -
- {plugin.languages.slice(0, 2).map(lang => ( - {lang} - ))} - {plugin.languages.length > 2 && ( - +{plugin.languages.length - 2} - )} -
-
- - {isExpanded && ( - -
-

{plugin.description}

-
- -
-
{t('pluginList.details.author')}
-
{plugin.author}
-
- -
-
{t('pluginList.details.languages')}
-
- {plugin.languages.map(lang => ( - {lang} - ))} -
-
- -
-
{t('pluginList.details.capabilities')}
-
- {plugin.capabilities.completion && ( - - - {t('pluginList.details.completion')} - - )} - {plugin.capabilities.hover && ( - - - {t('pluginList.details.hover')} - - )} - {plugin.capabilities.definition && ( - - - {t('pluginList.details.definition')} - - )} - {plugin.capabilities.references && ( - - - {t('pluginList.details.references')} - - )} - {plugin.capabilities.formatting && ( - - - {t('pluginList.details.formatting')} - - )} -
-
- -
- -
-
- )} -
- ); -}; diff --git a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.appearance.ts b/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.appearance.ts deleted file mode 100644 index 8972f4373f..0000000000 --- a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.appearance.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance'; - -export const referencesPanelAppearanceDescriptor: AppearanceSurfaceDescriptor = { - id: 'references-panel', - parts: [ - { id: 'root' }, { id: 'header' }, { id: 'title' }, { id: 'close' }, - { id: 'empty' }, { id: 'content' }, { id: 'fileGroup' }, { id: 'fileHeader' }, - { id: 'filePath' }, { id: 'fileCount' }, { id: 'list' }, { id: 'item' }, - { id: 'line' }, { id: 'preview' }, - ], - states: [ - { id: 'empty', selector: { kind: 'self', suffix: '[data-bf-state~="empty"]' } }, - ], -}; diff --git a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss b/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss deleted file mode 100644 index cbfc8bd082..0000000000 --- a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss +++ /dev/null @@ -1,296 +0,0 @@ -/** - * References panel styles (BitFun design tokens). - */ - -@use '@/component-library/styles/tokens.scss' as *; -@use '@/component-library/styles/overlay-surfaces' as surfaces; - -.references-panel-container { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: $z-modal; -} - -.references-panel { - @include surfaces.floating-surface; - - --references-panel-file-icon-rgb: 127, 219, 202; - --references-panel-accent-shadow-rgb: 59, 130, 246; - --references-panel-mark-rgb: 255, 215, 0; - - position: fixed; - width: 500px; - max-width: 90vw; - min-width: 300px; - - overflow: hidden; - pointer-events: all; - - font-family: $font-family-sans; - font-size: var(--bf-appearance-token-font-size-sm); - line-height: $line-height-base; - - opacity: 0; - transform: scale(0.95) translateY(-4px); - animation: references-panel-fade-in $motion-fast $easing-standard forwards; - - @keyframes references-panel-fade-in { - from { - opacity: 0; - transform: scale(0.95) translateY(-4px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } - } -} - -// Header -.references-panel__header { - display: flex; - align-items: center; - justify-content: space-between; - padding-block: $size-gap-2; - padding-inline: $size-gap-4 $size-gap-2; - background: var(--bf-appearance-token-element-bg-soft); - border-bottom: 1px solid var(--bf-appearance-token-border-base); -} - -.references-panel__title { - display: flex; - align-items: center; - gap: $size-gap-2; - font-size: var(--bf-appearance-token-font-size-sm); - font-weight: $font-weight-semibold; - color: var(--bf-appearance-token-color-text-secondary); - - svg { - color: var(--bf-appearance-token-color-accent-500); - } -} - -.references-panel__count { - margin-left: $size-gap-2; - font-size: var(--bf-appearance-token-font-size-xs); - font-weight: $font-weight-normal; - color: var(--bf-appearance-token-color-text-muted); -} - -// Content -.references-panel__content { - overflow-y: auto; - overflow-x: hidden; - max-height: inherit; - padding: $size-gap-1; - -} - -// Empty state -.references-panel__empty { - padding: $size-gap-8 $size-gap-4; - text-align: center; - font-size: var(--bf-appearance-token-font-size-sm); - color: var(--bf-appearance-token-color-text-muted); -} - -// File groups -.references-panel__file-group { - margin-bottom: $size-gap-2; - border-radius: $size-radius-sm; - overflow: hidden; - background: var(--bf-appearance-token-element-bg-subtle); - - &:last-child { - margin-bottom: 0; - } -} - -.references-panel__file-header { - display: flex; - align-items: center; - gap: $size-gap-2; - padding: $size-gap-2 $size-gap-3; - background: var(--bf-appearance-token-element-bg-soft); - border-bottom: 1px solid var(--bf-appearance-token-border-subtle); - cursor: default; - - svg { - flex-shrink: 0; - width: 16px; - height: 16px; - color: rgb(var(--references-panel-file-icon-rgb)); - opacity: 0.9; - } -} - -.references-panel__file-path { - flex: 1; - min-width: 0; - font-size: var(--bf-appearance-token-font-size-xs); - font-weight: $font-weight-medium; - color: var(--bf-appearance-token-color-text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.references-panel__file-count { - flex-shrink: 0; - padding: 2px $size-gap-2; - background: var(--bf-appearance-token-color-accent-100); - border-radius: $size-radius-full; - font-size: 11px; - font-weight: $font-weight-semibold; - color: var(--bf-appearance-token-color-accent-500); -} - -// Reference list -.references-panel__reference-list { - position: relative; -} - -.references-panel__reference-item { - display: flex; - align-items: center; - gap: $size-gap-2; - padding: $size-gap-2 $size-gap-3; - margin: 1px 0; - border-radius: $size-radius-sm; - cursor: pointer; - transition: - transform $motion-instant cubic-bezier(0.23, 1, 0.32, 1), - background-color $motion-instant ease, - color $motion-instant ease, - box-shadow $motion-instant ease; - user-select: none; - position: relative; - min-height: 32px; - color: var(--bf-appearance-token-color-text-secondary); - - &:hover { - background: linear-gradient(135deg, - var(--bf-appearance-token-color-accent-200) 0%, - var(--bf-appearance-token-color-accent-100) 50%, - var(--bf-appearance-token-color-accent-200) 100% - ); - color: var(--bf-appearance-token-color-text-primary); - - box-shadow: - inset 0 0 0 1px rgba(var(--references-panel-accent-shadow-rgb), 0.2), - 0 2px 8px rgba(var(--references-panel-accent-shadow-rgb), 0.1); - - .references-panel__reference-icon { - opacity: 1; - color: var(--bf-appearance-token-color-accent-500); - } - - .references-panel__reference-preview { - color: var(--bf-appearance-token-color-text-primary); - } - } - - &:active { - background: var(--bf-appearance-token-color-accent-300); - transform: scale(0.98); - } -} - -.references-panel__reference-icon { - width: 14px; - height: 14px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - color: var(--bf-appearance-token-color-text-muted); - opacity: 0.7; - transition: - transform $motion-instant ease, - color $motion-instant ease, - opacity $motion-instant ease; -} - -.references-panel__reference-line { - flex-shrink: 0; - width: 40px; - font-size: 11px; - font-family: $font-family-mono; - color: var(--bf-appearance-token-color-text-muted); - text-align: right; - user-select: none; - opacity: 0.8; -} - -.references-panel__reference-preview { - flex: 1; - font-size: var(--bf-appearance-token-font-size-xs); - font-family: $font-family-mono; - color: var(--bf-appearance-token-color-text-secondary); - line-height: $line-height-base; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: color $motion-instant ease; - - mark { - background: rgba(var(--references-panel-mark-rgb), 0.2); - color: rgb(var(--references-panel-mark-rgb)); - font-weight: $font-weight-semibold; - } -} - -// Responsive -@media (max-width: 768px) { - .references-panel { - min-width: 280px; - font-size: var(--bf-appearance-token-font-size-lg); - } - - .references-panel__reference-item { - padding: $size-gap-3 $size-gap-4; - min-height: 44px; - } -} - -// Accessibility -@media (prefers-contrast: high) { - .references-panel { - border-width: 2px; - border-color: rgba(var(--bf-appearance-token-color-static-white-rgb), 0.5); - box-shadow: - var(--bf-appearance-token-shadow-xl), - 0 0 0 2px rgba(var(--bf-appearance-token-color-static-black-rgb), 0.9); - } - - .references-panel__reference-item:hover { - background: var(--bf-appearance-token-color-accent-300); - border: 1px solid rgba(var(--references-panel-accent-shadow-rgb), 0.5); - } -} - -@media (prefers-reduced-motion: reduce) { - .references-panel { - transform: none; - animation: references-panel-reduced-fade-in $motion-instant ease forwards; - } - - .references-panel__reference-item { - transition: background-color $motion-instant ease; - } - - @keyframes references-panel-reduced-fade-in { - from { opacity: 0; } - to { opacity: 1; } - } -} - -.references-panel__reference-item:focus-visible { - outline: 2px solid var(--bf-appearance-token-color-accent-500); - outline-offset: -2px; - border-radius: $size-radius-sm; -} diff --git a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.tsx b/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.tsx deleted file mode 100644 index 478af417b0..0000000000 --- a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.tsx +++ /dev/null @@ -1,308 +0,0 @@ -/** - * References panel shown as a floating overlay. - */ - -import React, { useCallback, useMemo } from 'react'; -import { createRoot, Root } from 'react-dom/client'; -import { FileText, ChevronRight } from 'lucide-react'; -import { createLogger } from '@/shared/utils/logger'; -import { PopupCloseButton } from '@/component-library'; -import { useI18n } from '@/infrastructure/i18n'; -import './ReferencesPanel.scss'; - -const log = createLogger('ReferencesPanel'); - -export interface ReferenceLocation { - uri: string; - range: { - start: { line: number; character: number }; - end: { line: number; character: number }; - }; - /** Optional line preview text. */ - text?: string; -} - -interface GroupedReferences { - filePath: string; - fileName: string; - references: Array<{ - location: ReferenceLocation; - lineNumber: number; - preview: string; - }>; -} - -export interface ReferencesPanelProps { - /** Reference locations to render. */ - references: ReferenceLocation[]; - /** Symbol name used for the query (optional). */ - symbolName?: string; - /** Panel anchor position (viewport coords). */ - position: { x: number; y: number }; - /** Close callback. */ - onClose: () => void; - /** Click callback for a single reference entry. */ - onReferenceClick: (reference: ReferenceLocation) => void; - /** Max panel height in px. */ - maxHeight?: number; -} - -export const ReferencesPanel: React.FC = ({ - references, - symbolName, - position, - onClose, - onReferenceClick, - maxHeight = 400, -}) => { - const { t } = useI18n(['tools', 'common']); - const groupedReferences = useMemo(() => { - const groups = new Map(); - - references.forEach((ref) => { - const filePath = ref.uri; - const fileName = extractFileName(filePath); - - if (!groups.has(filePath)) { - groups.set(filePath, { - filePath, - fileName, - references: [], - }); - } - - const group = groups.get(filePath)!; - group.references.push({ - location: ref, - lineNumber: ref.range.start.line + 1, - preview: ref.text || t('lsp.referencesPanel.previewFallback'), - }); - }); - - const sortedGroups = Array.from(groups.values()); - sortedGroups.forEach((group) => { - group.references.sort((a, b) => a.lineNumber - b.lineNumber); - }); - sortedGroups.sort((a, b) => a.fileName.localeCompare(b.fileName)); - - return sortedGroups; - }, [references, t]); - - function extractFileName(uri: string): string { - const match = uri.match(/[^/\\]+$/); - return match ? match[0] : uri; - } - - const handleReferenceClick = useCallback( - (ref: ReferenceLocation) => { - onReferenceClick(ref); - }, - [onReferenceClick] - ); - - const panelStyle = useMemo(() => { - const style: React.CSSProperties = { - maxHeight: `${maxHeight}px`, - }; - - const viewportWidth = window.innerWidth; - const panelWidth = 500; - - if (position.x + panelWidth > viewportWidth - 20) { - style.right = `${viewportWidth - position.x}px`; - } else { - style.left = `${position.x}px`; - } - - const viewportHeight = window.innerHeight; - if (position.y + maxHeight > viewportHeight - 20) { - style.bottom = `${viewportHeight - position.y}px`; - } else { - style.top = `${position.y}px`; - } - - return style; - }, [position, maxHeight]); - - if (references.length === 0) { - return ( -
-
-
- - {t('lsp.referencesPanel.emptyTitle')} -
- -
-
- {symbolName - ? t('lsp.referencesPanel.emptyWithSymbol', { symbol: symbolName }) - : t('lsp.referencesPanel.emptyDescription')} -
-
- ); - } - - return ( -
-
-
- - - {symbolName - ? t('lsp.referencesPanel.titleWithSymbol', { symbol: symbolName }) - : t('lsp.referencesPanel.title')} - ({references.length}) - -
- -
- -
- {groupedReferences.map((group) => ( -
-
- -
- {group.filePath.replace(/^file:\/\/\//, '')} -
-
- {group.references.length} -
-
- -
- {group.references.map((ref, index) => ( -
handleReferenceClick(ref.location)} - > - -
- {ref.lineNumber} -
-
- {ref.preview.trim() || t('lsp.referencesPanel.emptyLine')} -
-
- ))} -
-
- ))} -
-
- ); -}; - -/** - * References Panel Controller - * Creates and manages the panel under document.body. - */ -export class ReferencesPanelController { - private container: HTMLDivElement | null = null; - private root: Root | null = null; - - /** - * Show the references panel. - */ - show( - references: ReferenceLocation[], - position: { x: number; y: number }, - options: { - symbolName?: string; - onReferenceClick: (ref: ReferenceLocation) => void; - } - ): void { - if (!this.container) { - this.container = document.createElement('div'); - this.container.className = 'references-panel-container'; - - this.container.style.position = 'fixed'; - this.container.style.top = '0'; - this.container.style.left = '0'; - this.container.style.width = '100%'; - this.container.style.height = '100%'; - this.container.style.pointerEvents = 'none'; - this.container.style.zIndex = '99999'; - - document.body.appendChild(this.container); - - this.root = createRoot(this.container); - - document.addEventListener('mousedown', this.handleOutsideClick); - document.addEventListener('keydown', this.handleEscapeKey); - } - - if (this.root) { - this.root.render( - this.hide()} - onReferenceClick={options.onReferenceClick} - /> - ); - } else { - log.error('Root is null, cannot render'); - } - } - - /** - * Close when clicking outside. - */ - private handleOutsideClick = (event: MouseEvent) => { - if (this.container && !this.container.contains(event.target as Node)) { - this.hide(); - } - }; - - /** - * Close on Escape. - */ - private handleEscapeKey = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - this.hide(); - } - }; - - /** - * Hide the panel. - */ - hide(): void { - document.removeEventListener('mousedown', this.handleOutsideClick); - document.removeEventListener('keydown', this.handleEscapeKey); - - if (this.root) { - this.root.unmount(); - this.root = null; - } - - if (this.container) { - document.body.removeChild(this.container); - this.container = null; - } - } - - /** - * Whether the panel is currently visible. - */ - isVisible(): boolean { - return this.container !== null; - } -} diff --git a/src/web-ui/src/tools/lsp/components/ReferencesPanel/index.ts b/src/web-ui/src/tools/lsp/components/ReferencesPanel/index.ts deleted file mode 100644 index fd34dd8be0..0000000000 --- a/src/web-ui/src/tools/lsp/components/ReferencesPanel/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ReferencesPanel, ReferencesPanelController } from './ReferencesPanel'; -export type { ReferencesPanelProps, ReferenceLocation } from './ReferencesPanel'; - diff --git a/src/web-ui/src/tools/lsp/hooks/useLsp.ts b/src/web-ui/src/tools/lsp/hooks/useLsp.ts deleted file mode 100644 index d5c583b635..0000000000 --- a/src/web-ui/src/tools/lsp/hooks/useLsp.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * LSP hooks used by React components. - */ - -import { useState, useEffect, useCallback } from 'react'; -import { lspService } from '../services/LspService'; -import type { LspPlugin } from '../types'; -import { createLogger } from '@/shared/utils/logger'; -import { useI18n } from '@/infrastructure/i18n'; - -const log = createLogger('useLsp'); - -/** - * List and manage installed LSP plugins. - */ -export function useLspPlugins() { - const { t } = useI18n('tools'); - const [plugins, setPlugins] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const loadPlugins = useCallback(async () => { - setLoading(true); - setError(null); - - try { - await lspService.initialize(); - - const pluginList = await lspService.listPlugins(); - setPlugins(pluginList); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : t('lsp.errors.loadPluginsFailed'); - setError(errorMessage); - log.error('Failed to load plugins', { error: err }); - } finally { - setLoading(false); - } - }, [t]); - - useEffect(() => { - loadPlugins(); - }, [loadPlugins]); - - const installPlugin = useCallback(async (packagePath: string) => { - try { - await lspService.installPlugin(packagePath); - await loadPlugins(); - return true; - } catch (err) { - setError(err instanceof Error ? err.message : t('lsp.errors.installPluginFailed')); - return false; - } - }, [loadPlugins, t]); - - const uninstallPlugin = useCallback(async (pluginId: string) => { - try { - await lspService.uninstallPlugin(pluginId); - await loadPlugins(); - return true; - } catch (err) { - setError(err instanceof Error ? err.message : t('lsp.errors.uninstallPluginFailed')); - return false; - } - }, [loadPlugins, t]); - - return { - plugins, - loading, - error, - reload: loadPlugins, - installPlugin, - uninstallPlugin - }; -} - -/** - * Initialize LSP, optionally setting a workspace root. - */ -export function useLspInit(workspacePath?: string) { - const { t } = useI18n('tools'); - const [initialized, setInitialized] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - const init = async () => { - try { - await lspService.initialize(); - - if (workspacePath) { - await lspService.openWorkspace(workspacePath); - } - - setInitialized(true); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : t('lsp.errors.initializeFailed'); - setError(errorMessage); - log.error('Failed to initialize LSP', { workspacePath, error: err }); - } - }; - - init(); - }, [workspacePath, t]); - - return { initialized, error }; -} - diff --git a/src/web-ui/src/tools/lsp/hooks/useMonacoLsp.ts b/src/web-ui/src/tools/lsp/hooks/useMonacoLsp.ts deleted file mode 100644 index 434b59305a..0000000000 --- a/src/web-ui/src/tools/lsp/hooks/useMonacoLsp.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Monaco editor LSP hook. - * - * Design: - * - One adapter per model (long-lived). - * - Editors are registered/unregistered to the adapter (short-lived). - * - Switching tabs should not destroy the adapter, so LSP state can persist. - */ - -import { useEffect, useRef } from 'react'; -import type * as monaco from 'monaco-editor'; -import { lspAdapterManager } from '../services/LspAdapterManager'; -import { lspExtensionRegistry } from '../services/LspExtensionRegistry'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('useMonacoLsp'); - -export function useMonacoLsp( - editor: monaco.editor.IStandaloneCodeEditor | null, - language: string, - filePath: string, - enabled: boolean = true, - workspacePath?: string -) { - const isRegisteredRef = useRef(false); - - useEffect(() => { - if (!editor || !enabled) { - return; - } - - const model = editor.getModel(); - if (!model) { - return; - } - - if (!workspacePath) { - log.warn('No workspace path provided, LSP will not be enabled'); - return; - } - - const monacoBuiltinLanguages = [ - 'typescript', - 'javascript', - 'typescriptreact', - 'javascriptreact', - 'json', - 'html', - 'css', - 'scss', - 'less', - ]; - if (monacoBuiltinLanguages.includes(language.toLowerCase())) { - return; - } - - if (!lspExtensionRegistry.isInitialized()) { - log.warn('LSP extension registry not initialized yet'); - return; - } - - if (!lspExtensionRegistry.isLanguageSupported(language)) { - return; - } - - lspAdapterManager.getOrCreateAdapter( - model, - language, - filePath, - workspacePath - ); - - lspAdapterManager.registerEditor(model, editor); - isRegisteredRef.current = true; - - return () => { - if (isRegisteredRef.current) { - lspAdapterManager.unregisterEditor(model, editor); - isRegisteredRef.current = false; - } - }; - }, [editor, language, filePath, enabled, workspacePath]); -} diff --git a/src/web-ui/src/tools/lsp/index.ts b/src/web-ui/src/tools/lsp/index.ts deleted file mode 100644 index 1d73cf135e..0000000000 --- a/src/web-ui/src/tools/lsp/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * LSP module exports and initialization helpers. - */ - -// Services -export { lspService, LspService } from './services/LspService'; -export { MonacoLspAdapter, GlobalAdapterRegistry } from './services/MonacoLspAdapter'; -export { lspAdapterManager } from './services/LspAdapterManager'; -export { WorkspaceLspManager } from './services/WorkspaceLspManager'; -export { workspaceLspInitializer } from './services/WorkspaceLspInitializer'; -export { LspDiagnostics } from './services/LspDiagnostics'; -export { HoverPositionCalculator } from './services/HoverPositionCalculator'; -export { lspDocumentService } from './services/LspDocumentService'; -export { lspExtensionRegistry } from './services/LspExtensionRegistry'; -export type { SupportedExtensionsResponse } from './services/LspExtensionRegistry'; -export { lspRefreshManager, LspRefreshManager } from './services/LspRefreshManager'; - -export type { PositionCalculatorOptions, PositionResult } from './services/HoverPositionCalculator'; - -// Hooks -export { useLspPlugins, useLspInit } from './hooks/useLsp'; -export { useMonacoLsp } from './hooks/useMonacoLsp'; - -// Components -export { LspPluginList } from './components/LspPluginList/LspPluginList'; -export type { LspPluginListProps } from './components/LspPluginList/LspPluginList'; - -// Types -export type { - LspPlugin, - ServerConfig, - CapabilitiesConfig, - CompletionItem, - Position, - Range, - Diagnostic, - HoverInfo, - Location, - TextEdit, -} from './types'; - -export { CompletionItemKind, DiagnosticSeverity } from './types'; -export { lspConfigService } from './services/LspConfigService'; - -// Initialization -export { initializeLsp } from './initializeLsp'; diff --git a/src/web-ui/src/tools/lsp/initializeLsp.test.ts b/src/web-ui/src/tools/lsp/initializeLsp.test.ts deleted file mode 100644 index 5b2ef86d3f..0000000000 --- a/src/web-ui/src/tools/lsp/initializeLsp.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -const registryMock = vi.hoisted(() => ({ - initialize: vi.fn(), -})); - -const workspaceInitializerMock = vi.hoisted(() => ({ - start: vi.fn(), - initializeWorkspace: vi.fn(), -})); - -vi.mock('./services/LspDiagnostics', () => ({})); -vi.mock('./services/LspExtensionRegistry', () => ({ - lspExtensionRegistry: registryMock, -})); -vi.mock('./services/WorkspaceLspInitializer', () => ({ - workspaceLspInitializer: workspaceInitializerMock, -})); - -describe('initializeLsp', () => { - beforeEach(() => { - vi.resetModules(); - vi.clearAllMocks(); - registryMock.initialize.mockResolvedValue(undefined); - workspaceInitializerMock.initializeWorkspace.mockResolvedValue(undefined); - }); - - it('initializes only lightweight LSP configuration by default', async () => { - const { initializeLsp } = await import('./initializeLsp'); - - await initializeLsp(); - - expect(registryMock.initialize).toHaveBeenCalledTimes(1); - expect(workspaceInitializerMock.start).not.toHaveBeenCalled(); - expect(workspaceInitializerMock.initializeWorkspace).not.toHaveBeenCalled(); - }); - - it('initializes a workspace only when LSP is needed', async () => { - const { ensureWorkspaceLspInitialized } = await import('./initializeLsp'); - - await ensureWorkspaceLspInitialized('D:/workspace/BitFun'); - - expect(registryMock.initialize).toHaveBeenCalledTimes(1); - expect(workspaceInitializerMock.start).not.toHaveBeenCalled(); - expect(workspaceInitializerMock.initializeWorkspace).toHaveBeenCalledWith( - 'D:/workspace/BitFun', - { prestartServers: false } - ); - }); -}); diff --git a/src/web-ui/src/tools/lsp/initializeLsp.ts b/src/web-ui/src/tools/lsp/initializeLsp.ts deleted file mode 100644 index f205708546..0000000000 --- a/src/web-ui/src/tools/lsp/initializeLsp.ts +++ /dev/null @@ -1,218 +0,0 @@ -/** - * LSP module startup path. - * - * Keep this file narrower than the public LSP barrel so application startup - * imports only the bootstrapping surface. - */ - -import { createLogger } from '@/shared/utils/logger'; - -import './services/LspDiagnostics'; -import { lspExtensionRegistry } from './services/LspExtensionRegistry'; - -const log = createLogger('LSP'); -let lspInitializationPromise: Promise | null = null; - -export async function initializeLsp(): Promise { - if (lspInitializationPromise) { - return lspInitializationPromise; - } - - lspInitializationPromise = initializeLspOnce(); - return lspInitializationPromise; -} - -async function initializeLspOnce(): Promise { - try { - await lspExtensionRegistry.initialize(); - installLspDiagnosticTools(); - } catch (error) { - lspInitializationPromise = null; - log.error('Failed to initialize LSP module', { error }); - } -} - -export async function ensureWorkspaceLspInitialized(workspacePath?: string): Promise { - await initializeLsp(); - if (!workspacePath) { - return; - } - - const { workspaceLspInitializer } = await import('./services/WorkspaceLspInitializer'); - await workspaceLspInitializer.initializeWorkspace(workspacePath, { - prestartServers: false, - }); -} - -/** - * Install a small set of global diagnostic helpers for debugging. - * - * Exposes `window.LspDiag` with methods like `.check()` and `.testDiagnostics()`. - */ -function installLspDiagnosticTools() { - const lspDiag = { - async enable() { - const { MonacoLspAdapter } = await loadMonacoLspDiagnostics(); - MonacoLspAdapter.enableDiagnosticMode(); - }, - - async disable() { - const { MonacoLspAdapter } = await loadMonacoLspDiagnostics(); - MonacoLspAdapter.disableDiagnosticMode(); - }, - - async check() { - const monaco = (window as any).monaco; - if (!monaco) { - log.error('Monaco Editor not loaded'); - return; - } - - const { GlobalAdapterRegistry } = await loadMonacoLspDiagnostics(); - const languages = monaco.languages.getLanguages().map((l: any) => l.id); - const adapters = GlobalAdapterRegistry.adapters; - const adapterUris = Array.from(adapters.keys()); - - log.info('LSP status check', { - registeredLanguages: languages.length, - languages, - activeAdapters: adapters.size, - adapterUris, - }); - - for (const [, adapter] of adapters.entries()) { - (adapter as any).getDiagnosticInfo?.(); - } - }, - - async testInlayHints() { - const { GlobalAdapterRegistry } = await loadMonacoLspDiagnostics(); - const adapters = GlobalAdapterRegistry.adapters; - if (adapters.size === 0) { - log.warn('No active LSP Adapter'); - return; - } - - const adapter = adapters.values().next().value; - const model = (adapter as any).model; - const range = new (window as any).monaco.Range(1, 1, model.getLineCount(), 1); - - try { - const result = await (adapter as any).provideInlayHints(model, range); - - if (result.hints && result.hints.length > 0) { - const hintPositions = result.hints.slice(0, 5).map((hint: any, i: number) => { - const labelText = typeof hint.label === 'string' - ? hint.label - : hint.label.map((p: any) => p.label || p.value).join(''); - return { - index: i + 1, - line: hint.position.lineNumber, - label: labelText, - }; - }); - - log.info('Inlay Hints test result', { - hintCount: result.hints.length, - hintPositions, - moreHints: result.hints.length > 5 ? result.hints.length - 5 : 0, - note: 'Monaco Editor may not display them automatically', - reasons: [ - 'Editor needs to be active', - 'Viewport needs to include hint lines', - 'Monaco version needs to support it', - ], - }); - } - } catch (error) { - log.error('Inlay Hints test failed', { error }); - } - }, - - async forceRefreshInlayHints() { - const monaco = (window as any).monaco; - if (!monaco) { - log.error('Monaco not loaded'); - return; - } - - const { GlobalAdapterRegistry } = await loadMonacoLspDiagnostics(); - const adapters = GlobalAdapterRegistry.adapters; - if (adapters.size === 0) { - log.warn('No active LSP Adapter'); - return; - } - - for (const [uri, adapter] of adapters.entries()) { - try { - const editor = (adapter as any).editor; - if (!editor) { - continue; - } - - const action = editor.getAction('editor.action.inlayHints.refresh'); - if (action) { - action.run(); - } - } catch (error) { - log.error('Failed to refresh inlay hints', { uri, error }); - } - } - }, - - testDiagnostics() { - const monaco = (window as any).monaco; - if (!monaco) { - log.error('Monaco not loaded'); - return; - } - - const models = monaco.editor.getModels(); - const documents = models.map((model: any, i: number) => { - const markers = monaco.editor.getModelMarkers({ resource: model.uri }); - const lspMarkers = markers.filter((m: any) => m.source === 'lsp'); - - return { - index: i + 1, - uri: model.uri.toString(), - language: model.getLanguageId(), - lineCount: model.getLineCount(), - totalMarkers: markers.length, - lspMarkers: lspMarkers.length > 0 ? lspMarkers.map((m: any, j: number) => ({ - index: j + 1, - line: m.startLineNumber, - severity: m.severity, - message: m.message, - })) : [], - }; - }); - - log.info('Diagnostics test', { - documentCount: models.length, - documents, - }); - }, - - help() { - const commands = [ - { command: '.enable()', description: 'Enable verbose logging' }, - { command: '.disable()', description: 'Disable verbose logging' }, - { command: '.check()', description: 'Check LSP system status' }, - { command: '.testInlayHints()', description: 'Test Inlay Hints' }, - { command: '.forceRefreshInlayHints()', description: 'Force refresh Inlay Hints' }, - { command: '.testDiagnostics()', description: 'Test diagnostics' }, - { command: '.help()', description: 'Show this help' }, - ]; - - log.info('LSP Diagnostic Tools Help', { commands }); - }, - }; - - if (typeof window !== 'undefined') { - (window as any).LspDiag = lspDiag; - } -} - -function loadMonacoLspDiagnostics() { - return import('./services/MonacoLspAdapter'); -} diff --git a/src/web-ui/src/tools/lsp/services/HoverPositionCalculator.ts b/src/web-ui/src/tools/lsp/services/HoverPositionCalculator.ts deleted file mode 100644 index 1eeeda6250..0000000000 --- a/src/web-ui/src/tools/lsp/services/HoverPositionCalculator.ts +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Hover position calculator. - * - * Uses Monaco's `getScrolledVisiblePosition` to anchor to the visible cursor - * position (scroll-aware), and then applies simple viewport-aware flipping and - * clamping (similar to Floating UI / VSCode hover behavior). - */ - -import type * as monaco from 'monaco-editor'; -import { monacoApi } from '@/tools/editor/services/monacoRuntime'; - -export interface PositionResult { - /** X in viewport coordinates. */ - x: number; - /** Y in viewport coordinates. */ - y: number; - /** Whether the panel is placed above the cursor. */ - isAbove: boolean; - /** Whether the panel is placed to the left of the cursor. */ - isLeft: boolean; - /** Max allowed width. */ - maxWidth: number; - /** Max allowed height. */ - maxHeight: number; -} - -export interface PositionCalculatorOptions { - /** Estimated hover width (default: 480px). */ - estimatedWidth?: number; - /** Estimated hover height (default: 200px). */ - estimatedHeight?: number; - /** Vertical offset from cursor (default: 10px). */ - verticalOffset?: number; - /** Horizontal offset from cursor (default: 0px). */ - horizontalOffset?: number; - /** Margin from viewport edges (default: 20px). */ - margin?: number; - /** Max width (default: 600px). */ - maxWidth?: number; - /** Max height (default: 400px). */ - maxHeight?: number; - /** Prefer placing above (default: false). */ - preferAbove?: boolean; -} - -/** - * Hover Position Calculator - */ -export class HoverPositionCalculator { - private editor: monaco.editor.IStandaloneCodeEditor; - private options: Required; - - constructor( - editor: monaco.editor.IStandaloneCodeEditor, - options: PositionCalculatorOptions = {} - ) { - this.editor = editor; - this.options = { - estimatedWidth: options.estimatedWidth ?? 480, - estimatedHeight: options.estimatedHeight ?? 200, - verticalOffset: options.verticalOffset ?? 10, - horizontalOffset: options.horizontalOffset ?? 0, - margin: options.margin ?? 20, - maxWidth: options.maxWidth ?? 600, - maxHeight: options.maxHeight ?? 400, - preferAbove: options.preferAbove ?? false - }; - } - - /** - * Compute best position for an overlay anchored at a Monaco position. - */ - public calculate( - position: monaco.Position, - contentWidth?: number, - contentHeight?: number - ): PositionResult | null { - const model = this.editor.getModel(); - if (!model) return null; - - const width = contentWidth ?? this.options.estimatedWidth; - const height = contentHeight ?? this.options.estimatedHeight; - - const editorDom = this.editor.getDomNode(); - if (!editorDom) return null; - - const editorRect = editorDom.getBoundingClientRect(); - - const visiblePosition = this.editor.getScrolledVisiblePosition(position); - if (!visiblePosition) return null; - - const cursorX = editorRect.left + visiblePosition.left; - const cursorY = editorRect.top + visiblePosition.top; - - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - const lineHeight = this.editor.getOption(monacoApi.editor.EditorOption.lineHeight); - - const spaceBelow = viewportHeight - (cursorY + lineHeight); - const spaceAbove = cursorY; - const spaceRight = viewportWidth - cursorX; - const spaceLeft = cursorX; - - let isAbove = this.options.preferAbove; - let isLeft = false; - - if (!this.options.preferAbove) { - if (spaceBelow < height + this.options.margin) { - if (spaceAbove > height + this.options.margin) { - isAbove = true; - } - } - } else { - if (spaceAbove < height + this.options.margin) { - if (spaceBelow > height + this.options.margin) { - isAbove = false; - } - } - } - - if (spaceRight < width + this.options.margin) { - if (spaceLeft > width + this.options.margin) { - isLeft = true; - } - } - - let x = cursorX + this.options.horizontalOffset; - let y: number; - - if (isAbove) { - y = cursorY - height - this.options.verticalOffset; - } else { - y = cursorY + lineHeight + this.options.verticalOffset; - } - - if (isLeft) { - x = cursorX - width - this.options.horizontalOffset; - } - - if (x < this.options.margin) { - x = this.options.margin; - } else if (x + width > viewportWidth - this.options.margin) { - x = viewportWidth - width - this.options.margin; - } - - if (y < this.options.margin) { - y = this.options.margin; - } else if (y + height > viewportHeight - this.options.margin) { - y = viewportHeight - height - this.options.margin; - } - - const maxWidth = Math.min( - this.options.maxWidth, - viewportWidth - 2 * this.options.margin - ); - - const maxHeight = Math.min( - this.options.maxHeight, - isAbove ? spaceAbove - this.options.margin : spaceBelow - this.options.margin - ); - - return { - x, - y, - isAbove, - isLeft, - maxWidth, - maxHeight - }; - } - - /** - * Compute a Monaco ContentWidget position. - */ - public calculateForContentWidget( - position: monaco.Position - ): monaco.editor.IContentWidgetPosition { - return { - position: position, - preference: [ - monacoApi.editor.ContentWidgetPositionPreference.BELOW, - monacoApi.editor.ContentWidgetPositionPreference.ABOVE - ] - }; - } - - /** - * Update options. - */ - public updateOptions(options: Partial): void { - Object.assign(this.options, options); - } -} - -/** Utility: detect whether an element is out of the viewport. */ -export function isOutOfViewport(element: HTMLElement): { - top: boolean; - right: boolean; - bottom: boolean; - left: boolean; -} { - const rect = element.getBoundingClientRect(); - - return { - top: rect.top < 0, - right: rect.right > window.innerWidth, - bottom: rect.bottom > window.innerHeight, - left: rect.left < 0 - }; -} - -/** Utility: adjust an element position to keep it inside the viewport. */ -export function adjustToViewport(element: HTMLElement, margin: number = 20): void { - const rect = element.getBoundingClientRect(); - const style = element.style; - - let left = parseInt(style.left) || rect.left; - let top = parseInt(style.top) || rect.top; - - if (rect.right > window.innerWidth - margin) { - left = window.innerWidth - rect.width - margin; - } - - if (rect.left < margin) { - left = margin; - } - - if (rect.bottom > window.innerHeight - margin) { - top = window.innerHeight - rect.height - margin; - } - - if (rect.top < margin) { - top = margin; - } - - style.left = `${left}px`; - style.top = `${top}px`; -} - - diff --git a/src/web-ui/src/tools/lsp/services/LspAdapterManager.ts b/src/web-ui/src/tools/lsp/services/LspAdapterManager.ts deleted file mode 100644 index c9fd774625..0000000000 --- a/src/web-ui/src/tools/lsp/services/LspAdapterManager.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * LSP Adapter Manager - * - * Manages `MonacoLspAdapter` instances with a 1:1 binding to Monaco models. - * - * - One adapter per model (long-lived). - * - An adapter can serve multiple editors. - * - Adapters are disposed when the model is disposed. - */ - -import type * as monaco from 'monaco-editor'; -import { MonacoLspAdapter } from './MonacoLspAdapter'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('LspAdapterManager'); - -interface AdapterMetadata { - adapter: MonacoLspAdapter; - model: monaco.editor.ITextModel; - language: string; - workspacePath: string; - createdAt: number; - editorCount: number; -} - -class LspAdapterManager { - private static instance: LspAdapterManager; - - /** Adapter metadata keyed by model URI. */ - private adapters = new Map(); - - /** Model dispose listeners keyed by model URI. */ - private modelDisposeListeners = new Map(); - - private constructor() {} - - public static getInstance(): LspAdapterManager { - if (!LspAdapterManager.instance) { - LspAdapterManager.instance = new LspAdapterManager(); - } - return LspAdapterManager.instance; - } - - /** Get or create an adapter bound to a model. */ - public getOrCreateAdapter( - model: monaco.editor.ITextModel, - language: string, - filePath: string, - workspacePath: string - ): MonacoLspAdapter { - const uri = model.uri.toString(); - - const existing = this.adapters.get(uri); - if (existing) { - return existing.adapter; - } - - const adapter = new MonacoLspAdapter( - model, - language, - filePath, - workspacePath - ); - - const metadata: AdapterMetadata = { - adapter, - model, - language, - workspacePath, - createdAt: Date.now(), - editorCount: 0 - }; - this.adapters.set(uri, metadata); - - const disposeListener = model.onWillDispose(() => { - this.disposeAdapter(uri); - }); - this.modelDisposeListeners.set(uri, disposeListener); - - return adapter; - } - - /** Get an existing adapter (no creation). */ - public getAdapter(uri: string): MonacoLspAdapter | undefined { - return this.adapters.get(uri)?.adapter; - } - - /** Register an editor to an adapter. */ - public registerEditor( - model: monaco.editor.ITextModel, - editor: monaco.editor.IStandaloneCodeEditor - ): void { - const uri = model.uri.toString(); - const metadata = this.adapters.get(uri); - - if (!metadata) { - log.warn('Cannot register editor: adapter not found', { uri }); - return; - } - - metadata.adapter.registerEditor(editor); - metadata.editorCount++; - } - - /** Unregister an editor from an adapter. */ - public unregisterEditor( - model: monaco.editor.ITextModel, - editor: monaco.editor.IStandaloneCodeEditor - ): void { - const uri = model.uri.toString(); - const metadata = this.adapters.get(uri); - - if (!metadata) { - log.warn('Cannot unregister editor: adapter not found', { uri }); - return; - } - - metadata.adapter.unregisterEditor(editor); - metadata.editorCount = Math.max(0, metadata.editorCount - 1); - } - - private disposeAdapter(uri: string): void { - const metadata = this.adapters.get(uri); - if (!metadata) { - return; - } - - metadata.adapter.dispose(); - - const disposeListener = this.modelDisposeListeners.get(uri); - if (disposeListener) { - disposeListener.dispose(); - this.modelDisposeListeners.delete(uri); - } - - this.adapters.delete(uri); - } - - /** Get statistics. */ - public getStatistics(): { - totalAdapters: number; - activeAdapters: number; - totalEditors: number; - adapters: Array<{ - uri: string; - language: string; - editorCount: number; - age: number; - }>; - } { - let activeAdapters = 0; - let totalEditors = 0; - const adapters: any[] = []; - - this.adapters.forEach((metadata, uri) => { - if (metadata.editorCount > 0) { - activeAdapters++; - } - totalEditors += metadata.editorCount; - - adapters.push({ - uri, - language: metadata.language, - editorCount: metadata.editorCount, - age: Date.now() - metadata.createdAt - }); - }); - - return { - totalAdapters: this.adapters.size, - activeAdapters, - totalEditors, - adapters - }; - } - - public disposeAll(): void { - const uris = Array.from(this.adapters.keys()); - uris.forEach(uri => this.disposeAdapter(uri)); - } -} - -// Singleton export -export const lspAdapterManager = LspAdapterManager.getInstance(); -export default LspAdapterManager; - diff --git a/src/web-ui/src/tools/lsp/services/LspConfigService.ts b/src/web-ui/src/tools/lsp/services/LspConfigService.ts deleted file mode 100644 index 87ff426a83..0000000000 --- a/src/web-ui/src/tools/lsp/services/LspConfigService.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * LSP config service (user-facing settings). - */ - -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('LspConfigService'); - -export interface LspSettings { - autoStartEnabled: boolean; -} - -const DEFAULT_LSP_SETTINGS: LspSettings = { - autoStartEnabled: true -}; - -const LSP_SETTINGS_KEY = 'bitfun_lsp_settings'; - -class LspConfigService { - private static instance: LspConfigService; - - private constructor() {} - - static getInstance(): LspConfigService { - if (!this.instance) { - this.instance = new LspConfigService(); - } - return this.instance; - } - - getSettings(): LspSettings { - try { - const saved = localStorage.getItem(LSP_SETTINGS_KEY); - if (saved) { - const parsed = JSON.parse(saved); - return { ...DEFAULT_LSP_SETTINGS, ...parsed }; - } - } catch (error) { - log.error('Failed to load settings', { error }); - } - return DEFAULT_LSP_SETTINGS; - } - - saveSettings(settings: LspSettings): void { - try { - localStorage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); - } catch (error) { - log.error('Failed to save settings', { error }); - throw error; - } - } - - isAutoStartEnabled(): boolean { - return this.getSettings().autoStartEnabled; - } -} - -export const lspConfigService = LspConfigService.getInstance(); - diff --git a/src/web-ui/src/tools/lsp/services/LspDiagnostics.ts b/src/web-ui/src/tools/lsp/services/LspDiagnostics.ts deleted file mode 100644 index 8356fe1952..0000000000 --- a/src/web-ui/src/tools/lsp/services/LspDiagnostics.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * LSP diagnostics helpers (primarily for debugging). - */ - -import { api } from '@/infrastructure/api/service-api/ApiClient'; -import { createLogger } from '@/shared/utils/logger'; -import { measureAsync } from '@/shared/utils/timing'; - -const log = createLogger('LspDiagnostics'); - -export interface LspDiagnosticInfo { - backendInitialized: boolean; - workspaceManagers: string[]; - error?: string; -} - -export class LspDiagnostics { - /** Check backend LSP status. */ - static async checkBackendStatus(workspacePath: string): Promise { - try { - const result = await measureAsync(() => api.invoke('lsp_get_all_server_states', { - request: { workspacePath }, - })); - log.debug('Got server states', { - workspacePath, - durationMs: result.durationMs, - }); - - return { - backendInitialized: true, - workspaceManagers: [workspacePath], - }; - } catch (error) { - log.error('Failed to get server states', { workspacePath, error }); - - return { - backendInitialized: false, - workspaceManagers: [], - error: error instanceof Error ? error.message : String(error) - }; - } - } - - /** Test a basic backend API call. */ - static async testBasicConnection(): Promise { - try { - await api.invoke('get_health_status'); - return true; - } catch (error) { - log.error('Basic connection test failed', { error }); - return false; - } - } - - /** Run a fuller diagnostic routine and log results. */ - static async runFullDiagnostics(workspacePath: string): Promise { - log.info('Running full LSP diagnostics', { workspacePath }); - - const basicConnection = await this.testBasicConnection(); - log.info('Basic connection test', { passed: basicConnection }); - - if (!basicConnection) { - log.error('Basic Tauri connection failed, cannot proceed with diagnostics'); - return; - } - - const backendStatus = await this.checkBackendStatus(workspacePath); - log.info('Backend status', { - initialized: backendStatus.backendInitialized, - workspaceManagers: backendStatus.workspaceManagers, - error: backendStatus.error - }); - - if (backendStatus.backendInitialized) { - log.info('LSP backend is operational', { - troubleshooting: [ - 'TypeScript LSP plugin is installed', - 'Server startup logs in backend', - 'File paths and URIs are correct' - ] - }); - } else { - log.error('LSP backend is not operational', { - fixes: [ - 'Restart the application', - 'Check backend logs for initialization errors', - 'Ensure workspace is properly opened' - ], - error: backendStatus.error - }); - } - - log.info('Diagnostics complete'); - } -} - -if (typeof window !== 'undefined') { - (window as any).LspDiagnostics = LspDiagnostics; -} diff --git a/src/web-ui/src/tools/lsp/services/LspDocumentService.ts b/src/web-ui/src/tools/lsp/services/LspDocumentService.ts deleted file mode 100644 index df78230d32..0000000000 --- a/src/web-ui/src/tools/lsp/services/LspDocumentService.ts +++ /dev/null @@ -1,326 +0,0 @@ -/** - * Connects Monaco model lifecycle to LSP document sync. - * - * Sends didOpen/didChange/didClose and triggers lightweight UI refreshes - * (e.g. semantic tokens, inlay hints) after edits. - */ - -import { monacoModelManager, type ModelCreatedEvent, type ModelContentChangedEvent, type ModelContentReadyEvent, type ModelDisposedEvent } from '@/tools/editor/services/MonacoModelManager'; -import { WorkspaceLspManager } from './WorkspaceLspManager'; -import { lspRefreshManager } from './LspRefreshManager'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('LspDocumentService'); - -interface DocumentSyncState { - /** Document URI. */ - uri: string; - /** File path. */ - filePath: string; - /** Language ID. */ - language: string; - /** Workspace root path. */ - workspacePath?: string; - /** Whether didOpen has been sent. */ - isOpen: boolean; - /** Last synced content. */ - lastSyncedContent: string; - /** Debounce timer for content changes. */ - changeTimer?: NodeJS.Timeout; - /** Associated editor instance (used to trigger refresh actions). */ - editor?: any; // monaco.editor.IStandaloneCodeEditor -} - -class LspDocumentService { - private static instance: LspDocumentService; - - /** Per-document sync state. */ - private documentStates = new Map(); - - /** Debounce delay for content changes (ms). */ - private readonly CHANGE_DEBOUNCE = 300; - - /** Batch sync timers by URI. */ - private syncTimers = new Map(); - /** Batch window (ms). */ - private readonly SYNC_DELAY = 100; - - /** Unsubscribe callbacks. */ - private unsubscribeFunctions: Array<() => void> = []; - - private constructor() { - this.setupEventListeners(); - } - - public static getInstance(): LspDocumentService { - if (!LspDocumentService.instance) { - LspDocumentService.instance = new LspDocumentService(); - } - return LspDocumentService.instance; - } - - private setupEventListeners(): void { - const unsubCreated = monacoModelManager.onModelCreated((event) => { - this.handleModelCreated(event); - }); - this.unsubscribeFunctions.push(unsubCreated); - - const unsubReady = monacoModelManager.onModelContentReady((event) => { - this.handleModelContentReady(event); - }); - this.unsubscribeFunctions.push(unsubReady); - - const unsubChanged = monacoModelManager.onModelContentChanged((event) => { - this.handleModelContentChanged(event); - }); - this.unsubscribeFunctions.push(unsubChanged); - - const unsubDisposed = monacoModelManager.onModelDisposed((event) => { - this.handleModelDisposed(event); - }); - this.unsubscribeFunctions.push(unsubDisposed); - } - - private handleModelCreated(event: ModelCreatedEvent): void { - const state: DocumentSyncState = { - uri: event.uri, - filePath: event.filePath, - language: event.language, - isOpen: false, - lastSyncedContent: '' - }; - - this.documentStates.set(event.uri, state); - - // Intentionally wait for content-ready before sending didOpen. - } - - private async handleModelContentReady(event: ModelContentReadyEvent): Promise { - const state = this.documentStates.get(event.uri); - if (!state) { - log.warn('Document state not found', { uri: event.uri }); - return; - } - - if (state.isOpen) { - return; - } - - const workspacePath = this.detectWorkspacePath(event.filePath); - state.workspacePath = workspacePath; - - if (!workspacePath) { - log.warn('No workspace detected', { filePath: event.filePath }); - return; - } - - const manager = WorkspaceLspManager.getOrCreate(workspacePath); - - try { - const openResult = await manager.openDocument( - event.uri, - state.language, - event.content - ); - - if (!openResult.opened) { - return; - } - - state.isOpen = true; - state.lastSyncedContent = event.content; - } catch (error) { - log.error('Failed to open document', { uri: event.uri, filePath: event.filePath, error }); - } - } - - private handleModelContentChanged(event: ModelContentChangedEvent): void { - const state = this.documentStates.get(event.uri); - if (!state) { - return; - } - - if (!state.isOpen) { - return; - } - - if (state.changeTimer) { - clearTimeout(state.changeTimer); - } - - state.changeTimer = setTimeout(() => { - this.syncContentChange(event, state); - state.changeTimer = undefined; - }, this.CHANGE_DEBOUNCE); - } - - /** Associate an editor instance with a document URI. */ - public associateEditor(uri: string, editor: any): void { - const state = this.documentStates.get(uri); - if (state) { - state.editor = editor; - } - } - - public disassociateEditor(uri: string): void { - const state = this.documentStates.get(uri); - if (state) { - state.editor = undefined; - } - } - - /** - * Sync content changes to LSP with a small batching window to reduce IPC calls. - */ - private async syncContentChange( - event: ModelContentChangedEvent, - state: DocumentSyncState - ): Promise { - if (event.content === state.lastSyncedContent) { - return; - } - - if (!state.workspacePath) { - return; - } - - const manager = WorkspaceLspManager.get(state.workspacePath); - if (!manager) { - return; - } - - const oldTimer = this.syncTimers.get(event.uri); - if (oldTimer) { - clearTimeout(oldTimer); - } - - const timer = setTimeout(async () => { - try { - await manager.changeDocument(event.uri, event.content); - - state.lastSyncedContent = event.content; - - if (state.editor) { - lspRefreshManager.onDocumentChange(event.uri, state.editor, { - refreshSemanticTokens: true, - refreshInlayHints: true, - refreshDiagnostics: false - }); - } - } catch (error) { - log.error('Failed to sync content', { uri: event.uri, error }); - } finally { - this.syncTimers.delete(event.uri); - } - }, this.SYNC_DELAY); - - this.syncTimers.set(event.uri, timer); - } - - /** Ensure any pending batched sync is flushed before closing. */ - private async flushPendingSync(uri: string): Promise { - const timer = this.syncTimers.get(uri); - if (!timer) { - return; - } - - clearTimeout(timer); - this.syncTimers.delete(uri); - - const state = this.documentStates.get(uri); - if (!state || !state.workspacePath) { - return; - } - - const manager = WorkspaceLspManager.get(state.workspacePath); - if (!manager) { - return; - } - - try { - await manager.changeDocument(uri, state.lastSyncedContent); - } catch (error) { - log.error('Failed to flush pending sync', { uri, error }); - } - } - - private async handleModelDisposed(event: ModelDisposedEvent): Promise { - const state = this.documentStates.get(event.uri); - if (!state) { - return; - } - - await this.flushPendingSync(event.uri); - - if (state.changeTimer) { - clearTimeout(state.changeTimer); - } - - if (state.isOpen && state.workspacePath) { - const manager = WorkspaceLspManager.get(state.workspacePath); - if (manager) { - try { - await manager.closeDocument(event.uri); - } catch (error) { - log.error('Failed to close document', { uri: event.uri, error }); - } - } - } - - this.documentStates.delete(event.uri); - } - - private detectWorkspacePath(filePath: string): string | undefined { - const metadata = monacoModelManager.getModelMetadata(filePath); - if (metadata?.workspacePath) { - return metadata.workspacePath; - } - - const normalizedPath = filePath.replace(/\\/g, '/'); - const lastSlashIndex = normalizedPath.lastIndexOf('/'); - if (lastSlashIndex > 0) { - const parentDir = normalizedPath.substring(0, lastSlashIndex); - return parentDir; - } - - return undefined; - } - - public getDocumentState(uri: string): DocumentSyncState | undefined { - return this.documentStates.get(uri); - } - - public getOpenDocuments(): string[] { - const openDocs: string[] = []; - this.documentStates.forEach((state, uri) => { - if (state.isOpen) { - openDocs.push(uri); - } - }); - return openDocs; - } - - public async dispose(): Promise { - const flushPromises = Array.from(this.syncTimers.keys()).map(uri => - this.flushPendingSync(uri) - ); - await Promise.all(flushPromises); - - this.unsubscribeFunctions.forEach(unsub => unsub()); - this.unsubscribeFunctions = []; - - this.documentStates.forEach(state => { - if (state.changeTimer) { - clearTimeout(state.changeTimer); - } - }); - - this.documentStates.clear(); - this.syncTimers.clear(); - } -} - -// Singleton export -export const lspDocumentService = LspDocumentService.getInstance(); -export default LspDocumentService; - diff --git a/src/web-ui/src/tools/lsp/services/LspExtensionRegistry.ts b/src/web-ui/src/tools/lsp/services/LspExtensionRegistry.ts deleted file mode 100644 index 06d6fc322e..0000000000 --- a/src/web-ui/src/tools/lsp/services/LspExtensionRegistry.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Registry of file extensions and languages supported by installed LSP plugins. - * - * Loaded once and cached to avoid querying the backend on every file open. - */ - -import { api } from '@/infrastructure/api/service-api/ApiClient'; -import { createLogger } from '@/shared/utils/logger'; -import { measureAsync } from '@/shared/utils/timing'; - -const log = createLogger('LspExtensionRegistry'); - -export interface SupportedExtensionsResponse { - /** Extension-to-language map (e.g. ".ts" -> "typescript"). */ - extensionToLanguage: Record; - /** All supported language IDs. */ - supportedLanguages: string[]; -} - -class LspExtensionRegistry { - private static instance: LspExtensionRegistry; - private extensionToLanguage: Map = new Map(); - private supportedLanguages: Set = new Set(); - private initialized = false; - private initializing = false; - private initPromise: Promise | null = null; - - private constructor() {} - - static getInstance(): LspExtensionRegistry { - if (!LspExtensionRegistry.instance) { - LspExtensionRegistry.instance = new LspExtensionRegistry(); - } - return LspExtensionRegistry.instance; - } - - /** Initialize from backend once. */ - async initialize(): Promise { - if (this.initialized) { - return; - } - - if (this.initializing && this.initPromise) { - return this.initPromise; - } - - this.initializing = true; - this.initPromise = this._doInitialize(); - - try { - await this.initPromise; - } finally { - this.initializing = false; - this.initPromise = null; - } - } - - private async _doInitialize(): Promise { - try { - const result = await measureAsync(() => - api.invoke('lsp_get_supported_extensions') - ); - const response = result.value; - - this.extensionToLanguage.clear(); - this.supportedLanguages.clear(); - - for (const [ext, lang] of Object.entries(response.extensionToLanguage)) { - const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`; - this.extensionToLanguage.set(normalizedExt.toLowerCase(), lang); - } - - for (const lang of response.supportedLanguages) { - this.supportedLanguages.add(lang.toLowerCase()); - } - - this.initialized = true; - log.debug('Initialized successfully', { - durationMs: result.durationMs, - extensionCount: this.extensionToLanguage.size, - languageCount: this.supportedLanguages.size - }); - } catch (error) { - log.error('Failed to initialize', { error }); - this.initialized = true; - } - } - - /** Whether a file path is supported by any installed LSP plugin. */ - isFileSupported(filePath: string): boolean { - if (!this.initialized) { - return false; - } - - const ext = this.getFileExtension(filePath); - if (!ext) { - return false; - } - - return this.extensionToLanguage.has(ext); - } - - /** Whether a language ID is supported by any installed LSP plugin. */ - isLanguageSupported(language: string): boolean { - if (!this.initialized) { - return false; - } - - return this.supportedLanguages.has(language.toLowerCase()); - } - - /** Map a file path to a language ID (or null if not supported). */ - getLanguageByFilePath(filePath: string): string | null { - if (!this.initialized) { - return null; - } - - const ext = this.getFileExtension(filePath); - if (!ext) { - return null; - } - - return this.extensionToLanguage.get(ext) || null; - } - - /** Get all supported extensions. */ - getSupportedExtensions(): string[] { - return Array.from(this.extensionToLanguage.keys()); - } - - /** Get all supported languages. */ - getSupportedLanguages(): string[] { - return Array.from(this.supportedLanguages); - } - - /** Extract a lowercase extension with dot (e.g. ".ts"). */ - private getFileExtension(filePath: string): string | null { - const lastDot = filePath.lastIndexOf('.'); - const lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); - - // No extension, or dot is in a folder segment. - if (lastDot === -1 || lastDot < lastSlash) { - return null; - } - - return filePath.substring(lastDot).toLowerCase(); - } - - /** Reload (e.g. after installing a new plugin). */ - async reload(): Promise { - this.initialized = false; - await this.initialize(); - } - - isInitialized(): boolean { - return this.initialized; - } -} - -// Singleton export -export const lspExtensionRegistry = LspExtensionRegistry.getInstance(); - diff --git a/src/web-ui/src/tools/lsp/services/LspRefreshManager.ts b/src/web-ui/src/tools/lsp/services/LspRefreshManager.ts deleted file mode 100644 index f372f72a88..0000000000 --- a/src/web-ui/src/tools/lsp/services/LspRefreshManager.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Centralized refresh scheduler for LSP-related Monaco features. - * - * Keeps refreshes debounced/throttled to avoid redundant work. - */ - -import type * as monaco from 'monaco-editor'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('LspRefreshManager'); - -interface RefreshTimers { - semanticTokens?: NodeJS.Timeout; - inlayHints?: NodeJS.Timeout; - diagnostics?: NodeJS.Timeout; -} - -interface RefreshConfig { - /** didChange debounce delay (ms). */ - didChange: number; - /** Extra delay for semantic tokens after didChange (ms). */ - semanticTokens: number; - /** Inlay hints refresh delay (ms). */ - inlayHints: number; - /** Diagnostics refresh delay (ms). */ - diagnostics: number; -} - -/** Default refresh timings. */ -const DEFAULT_REFRESH_CONFIG: RefreshConfig = { - didChange: 300, - semanticTokens: 200, - inlayHints: 400, - diagnostics: 100, -}; - -/** LSP refresh manager (singleton). */ -export class LspRefreshManager { - private static instance: LspRefreshManager; - - /** Refresh timers per document URI. */ - private refreshTimers = new Map(); - - /** Refresh config. */ - private config: RefreshConfig; - - /** Cancelled request IDs (used to skip stale work). */ - private cancelledRequests = new Set(); - - private constructor(config?: Partial) { - this.config = { ...DEFAULT_REFRESH_CONFIG, ...config }; - } - - public static getInstance(config?: Partial): LspRefreshManager { - if (!LspRefreshManager.instance) { - LspRefreshManager.instance = new LspRefreshManager(config); - } - return LspRefreshManager.instance; - } - - /** - * Main entrypoint to be called after document content changes. - */ - public onDocumentChange( - uri: string, - editor: monaco.editor.IStandaloneCodeEditor | null, - options?: { - /** Refresh semantic tokens. */ - refreshSemanticTokens?: boolean; - /** Refresh inlay hints. */ - refreshInlayHints?: boolean; - /** Refresh diagnostics. */ - refreshDiagnostics?: boolean; - } - ): void { - const opts = { - refreshSemanticTokens: true, - refreshInlayHints: true, - // Diagnostics are typically pushed by the LSP server. - refreshDiagnostics: false, - ...options - }; - - if (!editor) { - return; - } - - const timers = this.refreshTimers.get(uri) || {}; - - if (opts.refreshSemanticTokens) { - if (timers.semanticTokens) { - clearTimeout(timers.semanticTokens); - } - - timers.semanticTokens = setTimeout(() => { - this.refreshSemanticTokens(editor); - delete timers.semanticTokens; - }, this.config.semanticTokens); - } - - if (opts.refreshInlayHints) { - if (timers.inlayHints) { - clearTimeout(timers.inlayHints); - } - - timers.inlayHints = setTimeout(() => { - this.refreshInlayHints(editor); - delete timers.inlayHints; - }, this.config.inlayHints); - } - - if (opts.refreshDiagnostics) { - if (timers.diagnostics) { - clearTimeout(timers.diagnostics); - } - - timers.diagnostics = setTimeout(() => { - this.refreshDiagnostics(editor); - delete timers.diagnostics; - }, this.config.diagnostics); - } - - this.refreshTimers.set(uri, timers); - } - - /** - * Refresh semantic tokens. - * - * Uses a few internal fallbacks because Monaco doesn't expose a stable public API - * to flush semantic token caches in all versions. - */ - private refreshSemanticTokens(editor: monaco.editor.IStandaloneCodeEditor): void { - try { - const model = editor.getModel(); - if (!model) { - return; - } - - const editorAny = editor as any; - const modelAny = model as any; - - if (editorAny._modelData?.model?.tokenization) { - try { - const tokenization = editorAny._modelData.model.tokenization; - if (tokenization.flushTokens) { - tokenization.flushTokens(); - return; - } - } catch (_error) { - // silent - } - } - - if (modelAny._tokenization?.resetTokenization) { - modelAny._tokenization.resetTokenization(); - return; - } - - if (modelAny._resetTokenization) { - modelAny._resetTokenization(); - return; - } - - } catch (error) { - log.error('Failed to refresh semantic tokens', { error }); - } - } - - private refreshInlayHints(editor: monaco.editor.IStandaloneCodeEditor): void { - try { - const action = editor.getAction('editor.action.inlayHints.refresh'); - if (action) { - action.run(); - } - } catch (error) { - log.error('Failed to refresh inlay hints', { error }); - } - } - - private refreshDiagnostics(editor: monaco.editor.IStandaloneCodeEditor): void { - try { - const model = editor.getModel(); - if (!model) return; - - const monaco = (window as any).monaco; - if (monaco?.editor?.setModelMarkers) { - const uri = model.uri; - const currentMarkers = monaco.editor.getModelMarkers({ resource: uri }); - monaco.editor.setModelMarkers(model, 'lsp', currentMarkers); - } - } catch (_error) { - // silent - } - } - - /** Refresh semantic tokens and inlay hints immediately (no debounce). */ - public forceRefresh( - _uri: string, - editor: monaco.editor.IStandaloneCodeEditor | null - ): void { - if (!editor) return; - - this.refreshSemanticTokens(editor); - this.refreshInlayHints(editor); - } - - /** Cancel all scheduled refresh work for a document. */ - public cancelRefresh(documentUri: string): void { - const timers = this.refreshTimers.get(documentUri); - if (!timers) return; - - if (timers.semanticTokens) { - clearTimeout(timers.semanticTokens); - delete timers.semanticTokens; - } - - if (timers.inlayHints) { - clearTimeout(timers.inlayHints); - delete timers.inlayHints; - } - - if (timers.diagnostics) { - clearTimeout(timers.diagnostics); - delete timers.diagnostics; - } - - this.refreshTimers.delete(documentUri); - } - - public dispose(): void { - for (const [_uri, timers] of this.refreshTimers.entries()) { - if (timers.semanticTokens) clearTimeout(timers.semanticTokens); - if (timers.inlayHints) clearTimeout(timers.inlayHints); - if (timers.diagnostics) clearTimeout(timers.diagnostics); - } - - this.refreshTimers.clear(); - this.cancelledRequests.clear(); - } - - public updateConfig(config: Partial): void { - this.config = { ...this.config, ...config }; - } - - public getConfig(): Readonly { - return { ...this.config }; - } - - public getDebugInfo(): { - activeTimers: number; - timersByUri: Record; - config: RefreshConfig; - } { - const timersByUri: Record = {}; - let activeTimers = 0; - - for (const [uri, timers] of this.refreshTimers.entries()) { - const active: string[] = []; - if (timers.semanticTokens) active.push('semanticTokens'); - if (timers.inlayHints) active.push('inlayHints'); - if (timers.diagnostics) active.push('diagnostics'); - - if (active.length > 0) { - timersByUri[uri] = active; - activeTimers += active.length; - } - } - - return { - activeTimers, - timersByUri, - config: this.getConfig() - }; - } -} - -// Singleton export -export const lspRefreshManager = LspRefreshManager.getInstance(); - -// Global debug helper -if (typeof window !== 'undefined') { - (window as any).lspRefreshManager = lspRefreshManager; -} diff --git a/src/web-ui/src/tools/lsp/services/LspService.ts b/src/web-ui/src/tools/lsp/services/LspService.ts deleted file mode 100644 index f2f3957c40..0000000000 --- a/src/web-ui/src/tools/lsp/services/LspService.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Thin frontend wrapper around backend LSP APIs. - */ - -import { api } from '@/infrastructure/api/service-api/ApiClient'; -import { createLogger } from '@/shared/utils/logger'; -import { measureAsync } from '@/shared/utils/timing'; -import type { LspPlugin, CompletionItem, TextEdit } from '../types'; - -const log = createLogger('LspService'); - -export class LspService { - private static instance: LspService; - private initialized = false; - - private constructor() {} - - static getInstance(): LspService { - if (!LspService.instance) { - LspService.instance = new LspService(); - } - return LspService.instance; - } - - /** Initialize the backend LSP system once. */ - async initialize(): Promise { - if (this.initialized) { - return; - } - - try { - const result = await measureAsync(() => api.invoke('lsp_initialize')); - this.initialized = true; - log.info('LSP system initialized', { durationMs: result.durationMs }); - } catch (error) { - log.error('Failed to initialize LSP system', error); - throw error; - } - } - - /** Open a workspace (supersedes the old setWorkspaceRoot flow). */ - async openWorkspace(workspacePath: string): Promise { - try { - await api.invoke('lsp_open_workspace', { - request: { workspace_path: workspacePath } - }); - log.debug('Workspace opened', { workspacePath }); - } catch (error) { - log.error('Failed to open workspace', error); - throw error; - } - } - - /** Start a language server for a given file path. */ - async startServerForFile(filePath: string): Promise { - try { - const result = await measureAsync(() => api.invoke('lsp_start_server_for_file', { - request: { filePath } - })); - log.debug('LSP server started for file', { filePath, durationMs: result.durationMs }); - } catch (error) { - log.error('Failed to start LSP server for file', error); - throw error; - } - } - - /** Stop a language server. */ - async stopServer(language: string): Promise { - try { - await api.invoke('lsp_stop_server', { - request: { language } - }); - log.debug('LSP server stopped', { language }); - } catch (error) { - log.error('Failed to stop LSP server', error); - throw error; - } - } - - /** Notify didOpen. */ - async didOpen(language: string, uri: string, text: string): Promise { - try { - await api.invoke('lsp_did_open', { - request: { language, uri, text } - }); - } catch (error) { - log.error('Failed to notify document open', error); - throw error; - } - } - - /** Notify didChange. */ - async didChange(language: string, uri: string, version: number, text: string): Promise { - try { - await api.invoke('lsp_did_change', { - request: { language, uri, version, text } - }); - } catch (error) { - log.error('Failed to notify document change', error); - throw error; - } - } - - /** Notify didSave. */ - async didSave(language: string, uri: string): Promise { - try { - await api.invoke('lsp_did_save', { - request: { language, uri } - }); - } catch (error) { - log.error('Failed to notify document save', error); - throw error; - } - } - - /** Notify didClose. */ - async didClose(language: string, uri: string): Promise { - try { - await api.invoke('lsp_did_close', { - request: { language, uri } - }); - } catch (error) { - log.error('Failed to notify document close', error); - throw error; - } - } - - /** Request completion items. */ - async getCompletions( - language: string, - uri: string, - line: number, - character: number - ): Promise { - try { - const result = await measureAsync(() => api.invoke('lsp_get_completions', { - request: { language, uri, line, character } - }) as Promise); - log.debug('Got completions', { - count: Array.isArray(result.value) ? result.value.length : 0, - durationMs: result.durationMs, - }); - return result.value; - } catch (error) { - log.error('Failed to get completions', error); - throw error; - } - } - - /** Request hover info. */ - async getHover( - language: string, - uri: string, - line: number, - character: number - ): Promise { - try { - const result = await api.invoke('lsp_get_hover', { - request: { language, uri, line, character } - }); - return result; - } catch (error) { - log.error('Failed to get hover', error); - throw error; - } - } - - /** Request definition location(s). */ - async gotoDefinition( - language: string, - uri: string, - line: number, - character: number - ): Promise { - try { - const result = await measureAsync(() => api.invoke('lsp_goto_definition', { - request: { language, uri, line, character } - })); - log.debug('Found definition', { durationMs: result.durationMs }); - return result.value; - } catch (error) { - log.error('Failed to go to definition', error); - throw error; - } - } - - /** Request references. */ - async findReferences( - language: string, - uri: string, - line: number, - character: number - ): Promise { - try { - const result = await measureAsync(() => api.invoke('lsp_find_references', { - request: { language, uri, line, character } - })); - const count = Array.isArray(result.value) ? result.value.length : 0; - log.debug('Found references', { count, durationMs: result.durationMs }); - return result.value; - } catch (error) { - log.error('Failed to find references', error); - throw error; - } - } - - /** Request full-document formatting edits. */ - async formatDocument( - language: string, - uri: string, - tabSize?: number, - insertSpaces?: boolean - ): Promise { - try { - const result = await measureAsync(() => api.invoke('lsp_format_document', { - request: { language, uri, tabSize, insertSpaces } - }) as Promise); - const count = Array.isArray(result.value) ? result.value.length : 0; - log.debug('Document formatted', { editCount: count, durationMs: result.durationMs }); - return result.value; - } catch (error) { - log.error('Failed to format document', error); - throw error; - } - } - - /** Install an LSP plugin package. */ - async installPlugin(packagePath: string): Promise { - try { - const result = await measureAsync(() => api.invoke('lsp_install_plugin', { - request: { packagePath } - }) as Promise); - log.info('Plugin installed', { pluginId: result.value, durationMs: result.durationMs }); - return result.value; - } catch (error) { - log.error('Failed to install plugin', error); - throw error; - } - } - - /** Uninstall an installed plugin by ID. */ - async uninstallPlugin(pluginId: string): Promise { - try { - await api.invoke('lsp_uninstall_plugin', { - request: { pluginId } - }); - log.info('Plugin uninstalled', { pluginId }); - } catch (error) { - log.error('Failed to uninstall plugin', error); - throw error; - } - } - - /** List installed plugins. */ - async listPlugins(): Promise { - try { - const plugins = await api.invoke('lsp_list_plugins') as LspPlugin[]; - const count = Array.isArray(plugins) ? plugins.length : 0; - log.debug('Listed plugins', { count }); - return plugins; - } catch (error) { - log.error('Failed to list LSP plugins', error); - throw error; - } - } - - /** Get plugin info by ID. */ - async getPlugin(pluginId: string): Promise { - return await api.invoke('lsp_get_plugin', { - request: { pluginId } - }); - } - - /** Get server capabilities for a language. */ - async getServerCapabilities(language: string): Promise { - return await api.invoke('lsp_get_server_capabilities', { - request: { language } - }); - } - - /** Convert a file path to a file:// URI. */ - static filePathToUri(filePath: string): string { - // Windows paths - if (filePath.match(/^[a-zA-Z]:/)) { - return `file:///${filePath.replace(/\\/g, '/')}`; - } - // Unix paths - return `file://${filePath}`; - } - - /** Convert a file:// URI back to a file path. */ - static uriToFilePath(uri: string): string { - if (uri.startsWith('file:///')) { - const path = uri.substring(8); - // Windows paths - if (path.match(/^[a-zA-Z]:/)) { - return path.replace(/\//g, '\\'); - } - return path; - } - if (uri.startsWith('file://')) { - return uri.substring(7); - } - return uri; - } -} - -// Singleton export -export const lspService = LspService.getInstance(); diff --git a/src/web-ui/src/tools/lsp/services/MonacoLspAdapter.ts b/src/web-ui/src/tools/lsp/services/MonacoLspAdapter.ts deleted file mode 100644 index 26ba371c4a..0000000000 --- a/src/web-ui/src/tools/lsp/services/MonacoLspAdapter.ts +++ /dev/null @@ -1,1718 +0,0 @@ -/** - * Monaco LSP adapter. - * - * Registers Monaco language feature providers and routes requests through - * `WorkspaceLspManager`. Instances are model-scoped and managed externally. - */ - -import type * as monaco from 'monaco-editor'; -import { monacoApi } from '@/tools/editor/services/monacoRuntime'; -import { WorkspaceLspManager } from './WorkspaceLspManager'; -import { getMonacoLanguage } from '@/infrastructure/language-detection'; -import { createLogger } from '@/shared/utils/logger'; -import { i18nService } from '@/infrastructure/i18n'; -import type { CompletionItem } from '../types'; - -const log = createLogger('MonacoLspAdapter'); - - -export class GlobalAdapterRegistry { - static adapters = new Map(); - private static registeredLanguages = new Set(); - - private static globalProviderDisposables: monaco.IDisposable[] = []; - - static register(modelUri: string, adapter: MonacoLspAdapter): void { - this.adapters.set(modelUri, adapter); - } - - static unregister(modelUri: string): void { - this.adapters.delete(modelUri); - } - - static get(modelUri: string): MonacoLspAdapter | undefined { - return this.adapters.get(modelUri); - } - - static getAllUris(): string[] { - return Array.from(this.adapters.keys()); - } - - static isLanguageRegistered(language: string): boolean { - return this.registeredLanguages.has(language); - } - - static markLanguageRegistered(language: string): void { - this.registeredLanguages.add(language); - } - - - static addGlobalProvider(provider: monaco.IDisposable): void { - this.globalProviderDisposables.push(provider); - } - - - static disposeAllProviders(): void { - for (const disposable of this.globalProviderDisposables) { - disposable.dispose(); - } - this.globalProviderDisposables = []; - this.registeredLanguages.clear(); - } -} - -export class MonacoLspAdapter { - - public static diagnosticMode = false; - - private model: monaco.editor.ITextModel; - private editors = new Set(); - private language: string; - private serverLanguage: string; - private uri: string; - private disposables: monaco.IDisposable[] = []; - private workspaceManager: WorkspaceLspManager; - - - private completionCache = new Map(); - private readonly CACHE_TTL = 5000; - - /** Whether the LSP server has responded successfully at least once. */ - private _serverReady = false; - /** Timestamp of last server readiness probe (to avoid spamming). */ - private _lastReadinessProbe = 0; - /** Minimum interval between server readiness probes (ms). */ - private readonly READINESS_PROBE_INTERVAL = 5000; - - - private pendingCrossFileJump: { - targetPath: string; - targetUri: string; - fileName: string; - targetLine: number; - targetColumn: number; - position: { lineNumber: number; column: number }; - } | null = null; - - - constructor( - model: monaco.editor.ITextModel, - language: string, - filePath: string, - workspacePath: string - ) { - this.model = model; - - - const modelLanguage = this.model.getLanguageId(); - this.language = modelLanguage || language; - - - this.serverLanguage = this.resolveServerLanguage(this.language); - this.uri = this.filePathToUri(filePath); - this.workspaceManager = WorkspaceLspManager.getOrCreate(workspacePath); - - - const modelUri = this.model.uri.toString(); - GlobalAdapterRegistry.register(modelUri, this); - - this.initialize(); - } - - - public registerEditor(editor: monaco.editor.IStandaloneCodeEditor): void { - if (this.editors.has(editor)) { - return; - } - - this.editors.add(editor); - - - this.setupEditorListeners(editor); - } - - - public unregisterEditor(editor: monaco.editor.IStandaloneCodeEditor): void { - if (!this.editors.has(editor)) { - return; - } - - this.editors.delete(editor); - - - } - - - public getActiveEditor(): monaco.editor.IStandaloneCodeEditor | null { - - const iterator = this.editors.values(); - const result = iterator.next(); - return result.done ? null : result.value; - } - - - private resolveServerLanguage(language: string): string { - - if (language === 'c') { - return 'cpp'; - } - - if (language === 'javascript') { - return 'typescript'; - } - return language; - } - - - private detectLanguageFromPath(filePath: string): string { - return getMonacoLanguage(filePath); - } - - - private filePathToUri(filePath: string): string { - if (filePath.startsWith('file://')) { - return filePath; - } - - - try { - if (filePath.includes('%')) { - filePath = decodeURIComponent(filePath); - } - } catch (err) { - log.warn('Failed to decode path', { filePath, error: err }); - } - - let path = filePath.replace(/\\/g, '/'); - if (!path.startsWith('/')) { - path = '/' + path; - } - - return `file://${path}`; - } - - - private async initialize() { - try { - - this.registerProviders(); - - - setTimeout(() => { - this.refreshInlayHints(); - this.refreshSemanticTokens(); - }, 500); - } catch (error) { - log.error('Failed to initialize', { error }); - } - } - - - - private registerProviders() { - const alreadyRegistered = GlobalAdapterRegistry.isLanguageRegistered(this.language); - - if (alreadyRegistered) { - - this.registerDiagnosticsListener(); - return; - } - - this.registerCompletionProvider(); - this.registerHoverProvider(); - this.registerDefinitionProvider(); - this.registerReferencesProvider(); - this.registerFormattingProvider(); - this.registerSignatureHelpProvider(); - this.registerRenameProvider(); - this.registerCodeActionProvider(); - this.registerDocumentSymbolProvider(); - this.registerWorkspaceSymbolProvider(); - this.registerDocumentHighlightProvider(); - this.registerInlayHintsProvider(); - this.registerSemanticTokensProvider(); - this.registerDiagnosticsListener(); - - GlobalAdapterRegistry.markLanguageRegistered(this.language); - } - - - private registerCompletionProvider() { - const provider = monacoApi.languages.registerCompletionItemProvider(this.language, { - triggerCharacters: ['.', ':', '<', '"', "'", '/', '@', '#'], - - provideCompletionItems: async (model, position) => { - - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - - if (!adapter) { - return { suggestions: [] }; - } - - return adapter.provideCompletions(model, position); - } - }); - - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - async provideCompletions(_model: monaco.editor.ITextModel, position: monaco.Position) { - try { - - const cacheKey = `${position.lineNumber}:${position.column}`; - const cached = this.completionCache.get(cacheKey); - if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { - return { - suggestions: this.convertCompletions(cached.items) - }; - } - - - const items = await this.workspaceManager.getCompletions( - this.serverLanguage, - this.uri, - position.lineNumber - 1, - position.column - 1 - ); - - // Server responded - mark as ready so hover can work immediately. - if (items && items.length > 0) { - this._serverReady = true; - } - - this.completionCache.set(cacheKey, { - items, - timestamp: Date.now() - }); - - - this.cleanupCache(); - - return { - suggestions: this.convertCompletions(items) - }; - } catch (error) { - log.error('Failed to get completions', { uri: this.uri, position, error }); - return { suggestions: [] }; - } - } - - - private registerHoverProvider() { - const provider = monacoApi.languages.registerHoverProvider(this.language, { - provideHover: async (model, position) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - - if (!adapter) { - return null; - } - - return adapter.provideHover(model, position); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - async provideHover(_model: monaco.editor.ITextModel, position: monaco.Position) { - // If server has never responded successfully, skip hover to avoid "Loading..." flicker. - // Periodically re-probe to detect when the server becomes ready. - if (!this._serverReady) { - const now = Date.now(); - if (now - this._lastReadinessProbe < this.READINESS_PROBE_INTERVAL) { - return null; - } - this._lastReadinessProbe = now; - } - - try { - const hover = await this.workspaceManager.getHover( - this.serverLanguage, - this.uri, - position.lineNumber - 1, - position.column - 1 - ); - - if (!hover) { - return null; - } - - // Server responded successfully - mark as ready for future hover requests. - this._serverReady = true; - - const contents: monaco.IMarkdownString[] = []; - - if (hover.contents) { - if (typeof hover.contents === 'string') { - contents.push({ value: hover.contents }); - } else if (Array.isArray(hover.contents)) { - hover.contents.forEach((content: any) => { - if (typeof content === 'string') { - contents.push({ value: content }); - } else if (content.value) { - contents.push({ value: content.value, isTrusted: true }); - } - }); - } else if (hover.contents.value) { - contents.push({ value: hover.contents.value, isTrusted: true }); - } - } - - if (contents.length === 0) { - return null; - } - - const result: monaco.languages.Hover = { - contents, - range: hover.range ? new monacoApi.Range( - hover.range.start.line + 1, - hover.range.start.character + 1, - hover.range.end.line + 1, - hover.range.end.character + 1 - ) : undefined - }; - - return result; - } catch (error) { - log.error('Failed to get hover', { uri: this.uri, position, error }); - return null; - } - } - - - private registerDefinitionProvider() { - const provider = monacoApi.languages.registerDefinitionProvider(this.language, { - provideDefinition: async (model, position) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - - if (!adapter) { - return null; - } - - return adapter.provideDefinition(model, position); - } - }); - - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - async provideDefinition(_model: monaco.editor.ITextModel, position: monaco.Position) { - try { - const definition = await this.workspaceManager.gotoDefinition( - this.serverLanguage, - this.uri, - position.lineNumber - 1, - position.column - 1 - ); - - - let definitionItem = definition; - if (Array.isArray(definition)) { - if (definition.length === 0) { - return null; - } - - definitionItem = definition[0]; - } - - - const definitionUri = definitionItem.targetUri || definitionItem.uri; - if (!definitionUri) { - return null; - } - - const currentUri = this.uri; - - - const normalizeUriToPath = (uri: string): string => { - - let path = uri.replace(/^file:\/+/, ''); - - - path = path.replace(/^\/+([a-zA-Z]:)/, '$1'); - - - path = path.replace(/\\/g, '/'); - - - path = path.replace(/^([a-z]):/, (_match, letter) => letter.toUpperCase() + ':'); - - return path; - }; - - - const normalizeForComparison = (path: string): string => { - return path.toLowerCase(); - }; - - const definitionPath = normalizeUriToPath(definitionUri); - const currentPath = normalizeUriToPath(currentUri); - - - const isSameFile = normalizeForComparison(definitionPath) === normalizeForComparison(currentPath); - - - const range = definitionItem.targetSelectionRange || definitionItem.targetRange || definitionItem.range; - if (!range) { - return null; - } - - - const targetMonacoUri = monacoApi.Uri.parse(definitionUri); - const location: monaco.languages.Location = { - uri: targetMonacoUri, - range: new monacoApi.Range( - range.start.line + 1, - range.start.character + 1, - range.end.line + 1, - range.end.character + 1 - ) - }; - - if (!isSameFile) { - - const targetLine = range.start.line + 1; - const targetColumn = range.start.character + 1; - const fileName = definitionPath.split(/[/\\]/).pop() || ''; - - - - - this.pendingCrossFileJump = { - targetPath: definitionPath, - targetUri: definitionUri, - fileName, - targetLine, - targetColumn, - position: { - lineNumber: position.lineNumber, - column: position.column - } - }; - } else { - - this.pendingCrossFileJump = null; - } - - - return location; - } catch (error) { - log.error('Failed to goto definition', { uri: this.uri, position, error }); - return null; - } - } - - - private registerReferencesProvider() { - const provider = monacoApi.languages.registerReferenceProvider(this.language, { - provideReferences: async (model, position) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return []; - return adapter.provideReferences(model, position); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - async provideReferences(_model: monaco.editor.ITextModel, position: monaco.Position) { - try { - const references = await this.workspaceManager.findReferences( - this.serverLanguage, - this.uri, - position.lineNumber - 1, - position.column - 1 - ); - - if (!references || !Array.isArray(references)) return []; - - - await this.ensureModelsForReferences(references); - - return references.map((ref: any) => ({ - uri: monacoApi.Uri.parse(ref.uri), - range: new monacoApi.Range( - ref.range.start.line + 1, - ref.range.start.character + 1, - ref.range.end.line + 1, - ref.range.end.character + 1 - ) - })); - } catch (error) { - log.error('Failed to find references', { uri: this.uri, position, error }); - return []; - } - } - - - private async ensureModelsForReferences(references: any[]): Promise { - - const uniqueUris = new Set(); - for (const ref of references) { - if (ref.uri && ref.uri !== this.uri) { - uniqueUris.add(ref.uri); - } - } - - if (uniqueUris.size === 0) { - return; - } - - - const loadPromises = Array.from(uniqueUris).map(async (uriString) => { - try { - - const uri = monacoApi.Uri.parse(uriString); - const existingModel = monacoApi.editor.getModel(uri); - if (existingModel) { - return; - } - - - const { normalizePath } = await import('@/shared/utils/pathUtils'); - const filePath = normalizePath(uriString); - - - const { workspaceAPI } = await import('@/infrastructure/api'); - const content = await workspaceAPI.readFileContent(filePath); - - - const language = this.detectLanguageFromPath(filePath); - - - const { monacoModelManager } = await import('@/tools/editor/services/MonacoModelManager'); - monacoModelManager.getOrCreateModel( - filePath, - language, - content, - this.workspaceManager.getWorkspacePath() - ); - } catch (error) { - log.warn('Failed to create model for reference', { uri: uriString, error }); - - } - }); - - - await Promise.allSettled(loadPromises); - } - - - private registerFormattingProvider() { - const provider = monacoApi.languages.registerDocumentFormattingEditProvider(this.language, { - provideDocumentFormattingEdits: async (model) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return []; - return adapter.provideFormatting(model); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - async provideFormatting(_model: monaco.editor.ITextModel) { - try { - const edits = await this.workspaceManager.formatDocument( - this.serverLanguage, - this.uri, - 2, - true - ); - - if (!edits || !Array.isArray(edits)) return []; - - return edits.map((edit: any) => ({ - range: new monacoApi.Range( - edit.range.start.line + 1, - edit.range.start.character + 1, - edit.range.end.line + 1, - edit.range.end.character + 1 - ), - text: edit.newText - })); - } catch (error) { - log.error('Failed to format document', { uri: this.uri, error }); - return []; - } - } - - - private registerSignatureHelpProvider() { - const provider = monacoApi.languages.registerSignatureHelpProvider(this.language, { - signatureHelpTriggerCharacters: ['(', ','], - signatureHelpRetriggerCharacters: [')'], - - provideSignatureHelp: async (model, position) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return null; - return adapter.provideSignatureHelp(model, position); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - private registerRenameProvider() { - const provider = monacoApi.languages.registerRenameProvider(this.language, { - provideRenameEdits: async (model, position, newName) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return null; - return adapter.provideRenameEdits(model, position, newName); - }, - resolveRenameLocation: async (model, position) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return null; - return adapter.resolveRenameLocation(model, position); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - private registerCodeActionProvider() { - const provider = monacoApi.languages.registerCodeActionProvider(this.language, { - provideCodeActions: async (model, range, context) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return { actions: [], dispose: () => {} }; - return adapter.provideCodeActions(model, range, context); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - private registerDocumentSymbolProvider() { - const provider = monacoApi.languages.registerDocumentSymbolProvider(this.language, { - provideDocumentSymbols: async (model) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return []; - return adapter.provideDocumentSymbols(model); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - private registerWorkspaceSymbolProvider() { - - - - } - - - private registerDocumentHighlightProvider() { - const provider = monacoApi.languages.registerDocumentHighlightProvider(this.language, { - provideDocumentHighlights: async (model, position) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) return []; - return adapter.provideDocumentHighlights(model, position); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - private registerInlayHintsProvider() { - const provider = monacoApi.languages.registerInlayHintsProvider(this.language, { - provideInlayHints: async (model, range) => { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - if (!adapter) { - return { hints: [], dispose: () => {} }; - } - return adapter.provideInlayHints(model, range); - } - }); - GlobalAdapterRegistry.addGlobalProvider(provider); - } - - - async provideInlayHints(_model: monaco.editor.ITextModel, range: monaco.Range) { - try { - const hints = await this.workspaceManager.getInlayHints( - this.serverLanguage, - this.uri, - range.startLineNumber - 1, - range.startColumn - 1, - range.endLineNumber - 1, - range.endColumn - 1 - ); - - if (!hints || !Array.isArray(hints)) { - return { hints: [], dispose: () => {} }; - } - - - const monacoHints: monaco.languages.InlayHint[] = hints.map((hint: any) => { - - let label: string | monaco.languages.InlayHintLabelPart[]; - if (typeof hint.label === 'string') { - label = hint.label; - } else if (Array.isArray(hint.label)) { - label = hint.label.map((part: any) => ({ - label: part.value, - tooltip: part.tooltip, - location: part.location, - command: part.command - })); - } else { - label = String(hint.label); - } - - return { - label, - position: new monacoApi.Position( - hint.position.line + 1, - hint.position.character + 1 - ), - kind: hint.kind || monacoApi.languages.InlayHintKind.Type, - tooltip: hint.tooltip, - paddingLeft: hint.padding_left || false, - paddingRight: hint.padding_right || false, - }; - }); - - return { - hints: monacoHints, - dispose: () => {} - }; - } catch (error) { - log.error('Failed to get inlay hints', { uri: this.uri, range, error }); - return { hints: [], dispose: () => {} }; - } - } - - async provideSignatureHelp(_model: monaco.editor.ITextModel, position: monaco.Position) { - try { - const signatureHelp = await this.workspaceManager.getSignatureHelp( - this.serverLanguage, - this.uri, - position.lineNumber - 1, - position.column - 1 - ); - - if (!signatureHelp || !signatureHelp.signatures || signatureHelp.signatures.length === 0) { - return null; - } - - - return { - value: { - activeSignature: signatureHelp.activeSignature || 0, - activeParameter: signatureHelp.activeParameter || 0, - signatures: signatureHelp.signatures.map((sig: any) => ({ - label: sig.label, - documentation: sig.documentation, - parameters: sig.parameters?.map((param: any) => ({ - label: param.label, - documentation: param.documentation - })) || [] - })) - }, - dispose: () => {} // Noop dispose function - }; - } catch (error) { - log.error('Failed to get signature help', { uri: this.uri, position, error }); - return null; - } - } - - - async provideRenameEdits(_model: monaco.editor.ITextModel, position: monaco.Position, newName: string) { - try { - const edits = await this.workspaceManager.rename( - this.serverLanguage, - this.uri, - position.lineNumber - 1, - position.column - 1, - newName - ); - - if (!edits) { - return null; - } - - if (!edits.changes && !edits.documentChanges) { - return null; - } - - - const monacoEdits: any = { edits: [] }; - - - if (edits.changes) { - for (const [uri, textEdits] of Object.entries(edits.changes)) { - for (const edit of textEdits as any[]) { - monacoEdits.edits.push({ - resource: monacoApi.Uri.parse(uri), - textEdit: { - range: new monacoApi.Range( - edit.range.start.line + 1, - edit.range.start.character + 1, - edit.range.end.line + 1, - edit.range.end.character + 1 - ), - text: edit.newText - }, - versionId: undefined - }); - } - } - } - - - if (edits.documentChanges) { - for (const docChange of edits.documentChanges as any[]) { - if (docChange.textDocument && docChange.edits) { - // TextDocumentEdit - for (const edit of docChange.edits) { - monacoEdits.edits.push({ - resource: monacoApi.Uri.parse(docChange.textDocument.uri), - textEdit: { - range: new monacoApi.Range( - edit.range.start.line + 1, - edit.range.start.character + 1, - edit.range.end.line + 1, - edit.range.end.character + 1 - ), - text: edit.newText - }, - versionId: undefined - }); - } - } - } - } - - return monacoEdits; - } catch (error) { - log.error('Failed to get rename edits', { uri: this.uri, position, newName, error }); - return null; - } - } - - - async resolveRenameLocation(model: monaco.editor.ITextModel, position: monaco.Position) { - try { - - const word = model.getWordAtPosition(position); - if (!word) { - return null; - } - - return { - range: new monacoApi.Range( - position.lineNumber, - word.startColumn, - position.lineNumber, - word.endColumn - ), - text: word.word - }; - } catch (error) { - log.error('Failed to resolve rename location', { uri: this.uri, position, error }); - return null; - } - } - - - async provideCodeActions(_model: monaco.editor.ITextModel, range: monaco.Range, context: monaco.languages.CodeActionContext) { - try { - - const actions = await this.workspaceManager.getCodeActions( - this.serverLanguage, - this.uri, - { - start: { line: range.startLineNumber - 1, character: range.startColumn - 1 }, - end: { line: range.endLineNumber - 1, character: range.endColumn - 1 } - }, - { - diagnostics: context.markers.map(m => ({ - range: { - start: { line: m.startLineNumber - 1, character: m.startColumn - 1 }, - end: { line: m.endLineNumber - 1, character: m.endColumn - 1 } - }, - severity: m.severity, - message: m.message, - code: m.code, - source: m.source - })), - only: context.only ? [context.only] : undefined - } - ); - - if (!actions || !Array.isArray(actions)) { - return { actions: [], dispose: () => {} }; - } - - - const monacoActions = actions.map((action: any) => ({ - title: action.title, - kind: action.kind, - diagnostics: action.diagnostics, - edit: action.edit ? { - edits: Object.entries(action.edit.changes || {}).flatMap(([uri, textEdits]: [string, any]) => - textEdits.map((edit: any) => ({ - resource: monacoApi.Uri.parse(uri), - textEdit: { - range: new monacoApi.Range( - edit.range.start.line + 1, - edit.range.start.character + 1, - edit.range.end.line + 1, - edit.range.end.character + 1 - ), - text: edit.newText - }, - versionId: undefined - })) - ) - } : undefined, - command: action.command - })); - - return { - actions: monacoActions, - dispose: () => {} - }; - } catch (error) { - log.error('Failed to get code actions', { uri: this.uri, range, error }); - return { actions: [], dispose: () => {} }; - } - } - - - async provideDocumentSymbols(_model: monaco.editor.ITextModel) { - try { - - const symbols = await this.workspaceManager.getDocumentSymbols( - this.serverLanguage, - this.uri - ); - - if (!symbols || !Array.isArray(symbols)) { - return []; - } - - - const convertSymbol = (symbol: any): any => { - const result: any = { - name: symbol.name, - detail: symbol.detail || '', - kind: symbol.kind, - range: new monacoApi.Range( - symbol.range.start.line + 1, - symbol.range.start.character + 1, - symbol.range.end.line + 1, - symbol.range.end.character + 1 - ), - selectionRange: new monacoApi.Range( - symbol.selectionRange.start.line + 1, - symbol.selectionRange.start.character + 1, - symbol.selectionRange.end.line + 1, - symbol.selectionRange.end.character + 1 - ), - tags: symbol.tags || [] - }; - - if (symbol.children && Array.isArray(symbol.children)) { - result.children = symbol.children.map(convertSymbol); - } - - return result; - }; - - const monacoSymbols = symbols.map(convertSymbol); - return monacoSymbols; - } catch (error) { - log.error('Failed to get document symbols', { uri: this.uri, error }); - return []; - } - } - - - async provideWorkspaceSymbols(query: string) { - try { - const symbols = await this.workspaceManager.getWorkspaceSymbols(query); - - if (!symbols || !Array.isArray(symbols)) { - return []; - } - - - const monacoSymbols = symbols.map((symbol: any) => ({ - name: symbol.name, - kind: symbol.kind, - containerName: symbol.containerName, - location: { - uri: monacoApi.Uri.parse(symbol.location.uri), - range: new monacoApi.Range( - symbol.location.range.start.line + 1, - symbol.location.range.start.character + 1, - symbol.location.range.end.line + 1, - symbol.location.range.end.character + 1 - ) - } - })); - - return monacoSymbols; - } catch (error) { - log.error('Failed to search workspace symbols', { query, error }); - return []; - } - } - - - async provideDocumentHighlights(_model: monaco.editor.ITextModel, position: monaco.Position) { - try { - const highlights = await this.workspaceManager.getDocumentHighlight( - this.serverLanguage, - this.uri, - position.lineNumber - 1, - position.column - 1 - ); - - if (!highlights || !Array.isArray(highlights)) { - return []; - } - - - const monacoHighlights = highlights.map((highlight: any) => ({ - range: new monacoApi.Range( - highlight.range.start.line + 1, - highlight.range.start.character + 1, - highlight.range.end.line + 1, - highlight.range.end.character + 1 - ), - kind: highlight.kind || monacoApi.languages.DocumentHighlightKind.Text - })); - - return monacoHighlights; - } catch (error) { - log.error('Failed to get document highlights', { uri: this.uri, position, error }); - return []; - } - } - - - private registerSemanticTokensProvider() { - - const tokenTypes = [ - 'comment', // 0 - 'decorator', // 1 - 'enumMember', // 2 - 'enum', // 3 - 'function', // 4 - 'struct', // 5 - 'keyword', // 6 - 'macro', // 7 - 'method', // 8 - 'namespace', // 9 - 'parameter', // 10 - 'operator', // 11 - 'property', // 12 - 'property', // 13 - 'typeParameter', // 14 - 'type', // 15 - 'variable', // 16 - 'class', // 17 - 'interface', // 18 - 'number', // 19 - 'regexp', // 20 - 'modifier', // 21 - 'event', // 22 - 'label', // 23 - 'variable', // 24 - 'property', // 25 - 'function', // 26 - 'method', // 27 - 'macro', // 28 - 'type', // 29 - 'parameter', // 30 - 'property', // 31 - 'variable', // 32 - 'function', // 33 - 'method', // 34 - 'type', // 35 - 'interface', // 36 - 'property', // 37 - 'variable', // 38 - 'parameter', // 39 - 'function', // 40 - 'method', // 41 - 'type', // 42 - 'variable', // 43 - 'property', // 44 - 'enum', // 45 - 'class', // 46 - 'struct', // 47 - 'interface', // 48 - 'typeParameter', // 49 - 'parameter', // 50 - 'variable', // 51 - 'property', // 52 - 'function', // 53 - 'type', // 54 - 'method', // 55 - 'macro', // 56 - 'decorator', // 57 - 'enumMember', // 58 - 'comment', // 59 - ]; - - const tokenModifiers = [ - 'declaration', // 0 - 'definition', // 1 - 'readonly', // 2 - 'static', // 3 - 'deprecated', // 4 - 'abstract', // 5 - 'async', // 6 - 'modification', // 7 - 'documentation', // 8 - 'defaultLibrary', // 9 - ]; - - const disposable = monacoApi.languages.registerDocumentSemanticTokensProvider( - this.language, - { - getLegend: () => { - return { - tokenTypes, - tokenModifiers - }; - }, - - provideDocumentSemanticTokens: async (model) => { - try { - const modelUri = model.uri.toString(); - const adapter = GlobalAdapterRegistry.get(modelUri); - - if (!adapter) { - return null; - } - - const result = await adapter.provideSemanticTokens(model); - return result; - } catch (error) { - log.error('Failed to provide semantic tokens', { uri: model.uri.toString(), error }); - return null; - } - }, - - releaseDocumentSemanticTokens: (_resultId) => { - // cleanup - } - } - ); - - - GlobalAdapterRegistry.addGlobalProvider(disposable); - } - - - async provideSemanticTokens(_model: monaco.editor.ITextModel) { - try { - const semanticTokens = await this.workspaceManager.getSemanticTokens( - this.serverLanguage, - this.uri - ); - - if (!semanticTokens || !semanticTokens.data) { - return null; - } - - - const resultId = `${this.uri}-${Date.now()}`; - - return { - data: new Uint32Array(semanticTokens.data), - resultId: semanticTokens.resultId || resultId - }; - } catch (error) { - log.error('Failed to get semantic tokens', { uri: this.uri, error }); - return null; - } - } - - - - private registerDiagnosticsListener() { - - const listener = (diagnostics: any[]) => { - // Receiving diagnostics means the server is alive. - this._serverReady = true; - this.updateDiagnostics(diagnostics); - }; - - - this.workspaceManager.onDiagnostics(this.uri, listener); - } - - - private updateDiagnostics(diagnostics: any[]) { - try { - const markers = diagnostics.map(diag => ({ - severity: this.convertDiagnosticSeverity(diag.severity), - startLineNumber: diag.range.start.line + 1, - startColumn: diag.range.start.character + 1, - endLineNumber: diag.range.end.line + 1, - endColumn: diag.range.end.character + 1, - message: diag.message, - source: diag.source || 'LSP', - code: diag.code - })); - - monacoApi.editor.setModelMarkers(this.model, 'lsp', markers); - } catch (error) { - log.error('Failed to update diagnostics', { uri: this.uri, count: diagnostics.length, error }); - } - } - - private convertDiagnosticSeverity(severity?: number): monaco.MarkerSeverity { - - switch (severity) { - case 1: - return monacoApi.MarkerSeverity.Error; - case 2: - return monacoApi.MarkerSeverity.Warning; - case 3: - return monacoApi.MarkerSeverity.Info; - case 4: - return monacoApi.MarkerSeverity.Hint; - default: - return monacoApi.MarkerSeverity.Info; - } - } - - - private convertCompletions(items: CompletionItem[]): monaco.languages.CompletionItem[] { - return items.map(item => ({ - label: item.label, - kind: this.convertCompletionKind(item.kind), - detail: item.detail, - documentation: item.documentation, - insertText: item.insertText || item.label, - range: undefined as any - })); - } - - - private convertCompletionKind(kind?: number): monaco.languages.CompletionItemKind { - const map: Record = { - 1: monacoApi.languages.CompletionItemKind.Text, - 2: monacoApi.languages.CompletionItemKind.Method, - 3: monacoApi.languages.CompletionItemKind.Function, - 4: monacoApi.languages.CompletionItemKind.Constructor, - 5: monacoApi.languages.CompletionItemKind.Field, - 6: monacoApi.languages.CompletionItemKind.Variable, - 7: monacoApi.languages.CompletionItemKind.Class, - 8: monacoApi.languages.CompletionItemKind.Interface, - 9: monacoApi.languages.CompletionItemKind.Module, - 10: monacoApi.languages.CompletionItemKind.Property, - 11: monacoApi.languages.CompletionItemKind.Unit, - 12: monacoApi.languages.CompletionItemKind.Value, - 13: monacoApi.languages.CompletionItemKind.Enum, - 14: monacoApi.languages.CompletionItemKind.Keyword, - 15: monacoApi.languages.CompletionItemKind.Snippet, - 16: monacoApi.languages.CompletionItemKind.Color, - 17: monacoApi.languages.CompletionItemKind.File, - 18: monacoApi.languages.CompletionItemKind.Reference, - 19: monacoApi.languages.CompletionItemKind.Folder, - 20: monacoApi.languages.CompletionItemKind.EnumMember, - 21: monacoApi.languages.CompletionItemKind.Constant, - 22: monacoApi.languages.CompletionItemKind.Struct, - 23: monacoApi.languages.CompletionItemKind.Event, - 24: monacoApi.languages.CompletionItemKind.Operator, - 25: monacoApi.languages.CompletionItemKind.TypeParameter, - }; - - return kind ? map[kind] || monacoApi.languages.CompletionItemKind.Text : monacoApi.languages.CompletionItemKind.Text; - } - - - private cleanupCache() { - const now = Date.now(); - for (const [key, value] of this.completionCache.entries()) { - if (now - value.timestamp > this.CACHE_TTL) { - this.completionCache.delete(key); - } - } - } - - - async save() { - try { - await this.workspaceManager.saveDocument(this.uri); - } catch (error) { - log.error('Failed to save document', { uri: this.uri, error }); - } - } - - - private setupEditorListeners(editor: monaco.editor.IStandaloneCodeEditor): void { - - let crossFileDecorations: string[] = []; - let isCtrlPressed = false; - let currentHoverPosition: monaco.Position | null = null; - - - const globalKeyDownHandler = (e: KeyboardEvent) => { - if (e.ctrlKey || e.metaKey) { - if (!isCtrlPressed) { - isCtrlPressed = true; - - - if (currentHoverPosition) { - this.checkAndShowCrossFileDecoration(editor, currentHoverPosition, crossFileDecorations, isCtrlPressed) - .then((newDecorations) => { - crossFileDecorations = newDecorations; - }) - .catch(err => { - log.error('Error checking cross-file decoration', { error: err }); - }); - } - } - } - }; - - const globalKeyUpHandler = (e: KeyboardEvent) => { - if (!e.ctrlKey && !e.metaKey) { - if (isCtrlPressed) { - isCtrlPressed = false; - - crossFileDecorations = editor.deltaDecorations(crossFileDecorations, []); - } - } - }; - - - window.addEventListener('keydown', globalKeyDownHandler); - window.addEventListener('keyup', globalKeyUpHandler); - - - editor.onMouseMove((e) => { - const position = e.target.position; - currentHoverPosition = position; - - if (!position) { - if (crossFileDecorations.length > 0) { - crossFileDecorations = editor.deltaDecorations(crossFileDecorations, []); - } - return; - } - - if (!isCtrlPressed) { - - if (crossFileDecorations.length > 0) { - crossFileDecorations = editor.deltaDecorations(crossFileDecorations, []); - } - return; - } - - - this.checkAndShowCrossFileDecoration(editor, position, crossFileDecorations, isCtrlPressed) - .then((newDecorations) => { - crossFileDecorations = newDecorations; - }) - .catch(err => { - log.error('Error checking cross-file decoration', { error: err }); - }); - }); - - - editor.onMouseLeave(() => { - currentHoverPosition = null; - if (crossFileDecorations.length > 0) { - crossFileDecorations = editor.deltaDecorations(crossFileDecorations, []); - } - }); - - - editor.onMouseDown((e) => { - if (!e.target.position || !isCtrlPressed || !this.pendingCrossFileJump) return; - - const position = e.target.position; - - - if ( - position.lineNumber === this.pendingCrossFileJump.position.lineNumber && - position.column >= this.pendingCrossFileJump.position.column - 5 && - position.column <= this.pendingCrossFileJump.position.column + 10 - ) { - - e.event.preventDefault(); - e.event.stopPropagation(); - - - this.executeCrossFileJump(this.pendingCrossFileJump); - } - }); - - - const gotoDefinitionAction = editor.addAction({ - id: 'editor.action.revealDefinition', - label: i18nService.t('settings/lsp:editor.goToDefinition'), - keybindings: [monacoApi.KeyCode.F12], - contextMenuGroupId: 'navigation', - contextMenuOrder: 1.0, - run: async (editor) => { - const position = editor.getPosition(); - if (!position) return; - - - const definition = await this.provideDefinition(editor.getModel()!, position); - - - if (this.pendingCrossFileJump) { - - this.executeCrossFileJump(this.pendingCrossFileJump); - } else if (definition && !Array.isArray(definition)) { - - editor.setPosition({ - lineNumber: definition.range.startLineNumber, - column: definition.range.startColumn - }); - editor.revealPositionInCenter({ - lineNumber: definition.range.startLineNumber, - column: definition.range.startColumn - }); - } - } - }); - - this.disposables.push(gotoDefinitionAction); - - - editor.addAction({ - id: 'lsp.findReferences', - label: i18nService.t('settings/lsp:editor.findAllReferences'), - keybindings: [ - monacoApi.KeyMod.Shift | monacoApi.KeyMod.Alt | monacoApi.KeyCode.F12 - ], - contextMenuGroupId: 'navigation', - contextMenuOrder: 1.5, - run: async () => { - - editor.trigger('context-menu', 'editor.action.referenceSearch.trigger', null); - } - }); - - - - const modelDisposeListener = this.model.onWillDispose(() => { - window.removeEventListener('keydown', globalKeyDownHandler); - window.removeEventListener('keyup', globalKeyUpHandler); - }); - - - this.disposables.push(modelDisposeListener); - } - - - private refreshInlayHints() { - try { - - this.editors.forEach(editor => { - const action = editor.getAction('editor.action.inlayHints.refresh'); - if (action) { - action.run(); - } - }); - } catch (error) { - log.error('Failed to refresh inlay hints', error); - } - } - - - private refreshSemanticTokens() { - try { - - this.editors.forEach(editor => { - try { - - const action = editor.getAction('editor.action.semanticTokens.refresh'); - if (action) { - action.run(); - } - } catch (_error) { - // silent - } - }); - } catch (error) { - log.error('Failed to refresh semantic tokens', error); - } - } - - - public forceRefreshSemanticTokens() { - this.refreshSemanticTokens(); - } - - - private async checkAndShowCrossFileDecoration( - editor: monaco.editor.IStandaloneCodeEditor, - position: monaco.Position, - currentDecorations: string[], - isCtrlPressed: boolean - ): Promise { - try { - - if (!isCtrlPressed) { - return editor.deltaDecorations(currentDecorations, []); - } - - - let definition: monaco.languages.Location | monaco.languages.Location[] | null = null; - try { - definition = await this.provideDefinition(this.model, position); - } catch (err) { - log.error('provideDefinition threw error', { position, error: err }); - return editor.deltaDecorations(currentDecorations, []); - } - - if (!definition) { - return editor.deltaDecorations(currentDecorations, []); - } - - if (Array.isArray(definition)) { - return editor.deltaDecorations(currentDecorations, []); - } - - - if (this.pendingCrossFileJump) { - - const wordAtPosition = this.model.getWordAtPosition(position); - - if (wordAtPosition) { - const startColumn = wordAtPosition.startColumn; - const endColumn = wordAtPosition.endColumn; - - - const newDecorations: monaco.editor.IModelDeltaDecoration[] = [{ - range: new monacoApi.Range(position.lineNumber, startColumn, position.lineNumber, endColumn), - options: { - inlineClassName: 'monaco-cross-file-link', // CSS class for styling - hoverMessage: { value: i18nService.t('settings/lsp:editor.goToFile', { fileName: this.pendingCrossFileJump.fileName }) } - } - }]; - - return editor.deltaDecorations(currentDecorations, newDecorations); - } - } - - return editor.deltaDecorations(currentDecorations, []); - } catch (error) { - log.error('Error checking cross-file decoration', { position, error }); - return editor.deltaDecorations(currentDecorations, []); - } - } - - - private async executeCrossFileJump(jump: { - targetPath: string; - fileName: string; - targetLine: number; - targetColumn: number; - }) { - const isMarkdownFile = jump.fileName.toLowerCase().endsWith('.md'); - const editorType = isMarkdownFile ? 'markdown-editor' : 'code-editor'; - - - const { normalizePath } = await import('@/shared/utils/pathUtils'); - const normalizedPath = normalizePath(jump.targetPath); - - - window.dispatchEvent(new CustomEvent('agent-create-tab', { - detail: { - type: editorType, - title: jump.fileName, - data: { - filePath: normalizedPath, - fileName: jump.fileName, - workspacePath: this.workspaceManager.getWorkspacePath(), - jumpToLine: jump.targetLine, - jumpToColumn: jump.targetColumn - }, - metadata: { - duplicateCheckKey: normalizedPath - }, - checkDuplicate: true, - duplicateCheckKey: normalizedPath, - replaceExisting: false - } - })); - - - - - this.pendingCrossFileJump = null; - } - - - async dispose() { - - const modelUri = this.model.uri.toString(); - GlobalAdapterRegistry.unregister(modelUri); - - - for (const disposable of this.disposables) { - disposable.dispose(); - } - this.disposables = []; - - - this.completionCache.clear(); - } - - - static enableDiagnosticMode() { - MonacoLspAdapter.diagnosticMode = true; - log.info('Diagnostic mode enabled'); - } - - - static disableDiagnosticMode() { - MonacoLspAdapter.diagnosticMode = false; - log.info('Diagnostic mode disabled'); - } - - - getDiagnosticInfo() { - const info = { - uri: this.uri, - language: this.language, - serverLanguage: this.serverLanguage, - modelUri: this.model.uri.toString(), - modelLanguage: this.model.getLanguageId(), - lineCount: this.model.getLineCount(), - isDisposed: this.disposables.length === 0, - currentMarkers: monacoApi.editor.getModelMarkers({ resource: this.model.uri }), - workspacePath: this.workspaceManager.getWorkspacePath(), - }; - - log.debug('Diagnostic info', { - uri: info.uri, - language: info.language, - serverLanguage: info.serverLanguage, - modelUri: info.modelUri, - modelLanguage: info.modelLanguage, - lineCount: info.lineCount, - isDisposed: info.isDisposed, - workspacePath: info.workspacePath, - markerCount: info.currentMarkers.length, - lspMarkerCount: info.currentMarkers.filter(m => m.source === 'lsp').length - }); - - return info; - } -} diff --git a/src/web-ui/src/tools/lsp/services/MonacoLspBridge.ts b/src/web-ui/src/tools/lsp/services/MonacoLspBridge.ts deleted file mode 100644 index 0519ecba6e..0000000000 --- a/src/web-ui/src/tools/lsp/services/MonacoLspBridge.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/web-ui/src/tools/lsp/services/WorkspaceLspInitializer.ts b/src/web-ui/src/tools/lsp/services/WorkspaceLspInitializer.ts deleted file mode 100644 index e843f7de3e..0000000000 --- a/src/web-ui/src/tools/lsp/services/WorkspaceLspInitializer.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Workspace LSP initializer. - * - * Listens to workspace open/close/switch events and initializes the LSP system. - * Optionally pre-starts servers based on project detection. - */ - -import { workspaceManager } from '@/infrastructure/services/business/workspaceManager'; -import { WorkspaceLspManager } from './WorkspaceLspManager'; -import { lspConfigService } from './LspConfigService'; -import { api } from '@/infrastructure/api/service-api/ApiClient'; -import { createLogger } from '@/shared/utils/logger'; -import { isSurfaceChangedError } from '@/infrastructure/peer-device/deviceSurface'; - -const log = createLogger('WorkspaceLspInitializer'); - -interface ProjectInfo { - languages: string[]; - primaryLanguage?: string; - fileCount: Record; - projectTypes: string[]; - totalFiles: number; -} - -interface InitializeWorkspaceOptions { - prestartServers?: boolean; -} - -class WorkspaceLspInitializer { - private static instance: WorkspaceLspInitializer | null = null; - private removeListener: (() => void) | null = null; - - private constructor() { - } - - static getInstance(): WorkspaceLspInitializer { - if (!this.instance) { - this.instance = new WorkspaceLspInitializer(); - } - return this.instance; - } - - /** Start listening to workspace events. */ - start(): void { - if (this.removeListener) { - return; - } - - this.removeListener = workspaceManager.addEventListener(async (event) => { - switch (event.type) { - case 'workspace:opened': - await this.handleWorkspaceOpened(event.workspace.rootPath); - break; - - case 'workspace:closed': - await this.handleWorkspaceClosed(); - break; - - case 'workspace:switched': - await this.handleWorkspaceSwitched(event.workspace.rootPath); - break; - } - }); - } - - /** Stop listening. */ - stop(): void { - if (this.removeListener) { - this.removeListener(); - this.removeListener = null; - } - } - - private async handleWorkspaceOpened(workspacePath: string): Promise { - await this.initializeWorkspace(workspacePath, { prestartServers: true }); - } - - async initializeWorkspace( - workspacePath: string, - options: InitializeWorkspaceOptions = {} - ): Promise { - try { - const manager = WorkspaceLspManager.getOrCreate(workspacePath); - - await manager.initialize(); - - log.info('LSP initialized for workspace', { workspacePath }); - - if (!options.prestartServers || !lspConfigService.isAutoStartEnabled()) { - return; - } - - this.detectAndPreStartServers(workspacePath).catch(error => { - log.error('Failed to detect and pre-start servers', { workspacePath, error }); - }); - - } catch (error) { - logLspInitializationFailure(workspacePath, error); - } - } - - /** Detect project languages and pre-start corresponding servers (best-effort). */ - private async detectAndPreStartServers(workspacePath: string): Promise { - try { - const projectInfo = await api.invoke('lsp_detect_project', { - request: { workspacePath } - }); - - if (!projectInfo.languages || projectInfo.languages.length === 0) { - return; - } - - log.debug('Project detected', { workspacePath, languages: projectInfo.languages }); - - for (const language of projectInfo.languages) { - this.preStartServer(workspacePath, language).catch(error => { - log.error('Failed to pre-start server', { workspacePath, language, error }); - }); - } - - } catch (error) { - log.warn('Project detection failed', { workspacePath, error }); - } - } - - /** Pre-start a server for a given language (best-effort). */ - private async preStartServer(workspacePath: string, language: string): Promise { - try { - await api.invoke('lsp_prestart_server', { - request: { - workspacePath, - language - } - }); - - log.debug('Server pre-started', { workspacePath, language }); - } catch (error) { - log.error('Failed to pre-start server', { workspacePath, language, error }); - throw error; - } - } - - private async handleWorkspaceClosed(): Promise { - try { - // TODO: clean up the manager for the currently active workspace (if needed). - } catch (error) { - log.error('Failed to cleanup LSP', { error }); - } - } - - private async handleWorkspaceSwitched(workspacePath: string): Promise { - try { - const manager = WorkspaceLspManager.getOrCreate(workspacePath); - - await manager.initialize(); - - log.info('LSP initialized for switched workspace', { workspacePath }); - } catch (error) { - logLspInitializationFailure(workspacePath, error); - } - } -} - -// Singleton export -export const workspaceLspInitializer = WorkspaceLspInitializer.getInstance(); - -/** - * A device surface switch supersedes an in-flight LSP bootstrap. The manager - * belongs to the device we left, so abandoning is the correct outcome and must - * not be reported as a broken workspace. - */ -function logLspInitializationFailure(workspacePath: string, error: unknown): void { - if (isSurfaceChangedError(error)) { - log.debug('LSP bootstrap abandoned after a device surface switch', { workspacePath }); - return; - } - log.error('Failed to initialize LSP', { workspacePath, error }); -} diff --git a/src/web-ui/src/tools/lsp/services/WorkspaceLspManager.test.ts b/src/web-ui/src/tools/lsp/services/WorkspaceLspManager.test.ts deleted file mode 100644 index f73cac9377..0000000000 --- a/src/web-ui/src/tools/lsp/services/WorkspaceLspManager.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; - -const invokeMock = vi.hoisted(() => vi.fn()); -const listenMock = vi.hoisted(() => vi.fn(() => () => {})); - -vi.mock('@/infrastructure/api/service-api/ApiClient', () => ({ - api: { - invoke: invokeMock, - listen: listenMock, - }, -})); - -vi.mock('@/shared/notification-system', () => ({ - notificationService: { - progress: vi.fn(), - error: vi.fn(), - loading: vi.fn(() => ({ - updateMessage: vi.fn(), - complete: vi.fn() - })) - } -})); - -vi.mock('@/infrastructure/i18n', () => ({ - i18nService: { - t: vi.fn((key: string) => key) - } -})); - -import { WorkspaceLspManager } from './WorkspaceLspManager'; -import { - activateSurface, - isSurfaceChangedError, - resetDeviceSurfaceForTest, -} from '@/infrastructure/peer-device/deviceSurface'; - -function serverState(status: 'stopped' | 'starting' | 'running' | 'failed' | 'restarting') { - return { - status, - language: 'rust', - startedAt: null, - lastError: null, - restartCount: 0, - documentCount: 0 - }; -} - -describe('WorkspaceLspManager', () => { - afterEach(() => { - invokeMock.mockReset(); - listenMock.mockReset(); - listenMock.mockReturnValue(() => {}); - (WorkspaceLspManager as unknown as { - instances: Map; - }).instances.clear(); - resetDeviceSurfaceForTest(); - }); - - it('skips didOpen when the language server is stopped', async () => { - invokeMock.mockImplementation(async (command: string) => { - if (command === 'lsp_open_workspace') { - return undefined; - } - if (command === 'lsp_get_server_state') { - return serverState('stopped'); - } - if (command === 'lsp_open_document') { - throw new Error('didOpen should not be sent for a stopped server'); - } - return undefined; - }); - - const manager = WorkspaceLspManager.getOrCreate('D:\\workspace\\BitFun'); - - const result = await manager.openDocument( - 'file:///D:/workspace/BitFun/src/main.rs', - 'rust', - 'fn main() {}' - ); - - expect(result).toEqual({ - language: 'rust', - opened: false, - skippedReason: 'server-not-running', - serverStatus: 'stopped' - }); - expect(invokeMock).not.toHaveBeenCalledWith('lsp_open_document', expect.anything()); - }); - - it('sends didOpen when the language server is running', async () => { - invokeMock.mockImplementation(async (command: string) => { - if (command === 'lsp_open_workspace') { - return undefined; - } - if (command === 'lsp_get_server_state') { - return serverState('running'); - } - if (command === 'lsp_open_document') { - return undefined; - } - return undefined; - }); - - const manager = WorkspaceLspManager.getOrCreate('D:\\workspace\\BitFun'); - - const result = await manager.openDocument( - 'file:///D:/workspace/BitFun/src/main.rs', - 'rust', - 'fn main() {}' - ); - - expect(result).toEqual({ language: 'rust', opened: true }); - expect(invokeMock).toHaveBeenCalledWith('lsp_open_document', { - request: { - workspacePath: 'D:\\workspace\\BitFun', - uri: 'file:///D:/workspace/BitFun/src/main.rs', - language: 'rust', - content: 'fn main() {}' - } - }); - }); - - it('caches stopped server state to avoid repeated state queries during UI remounts', async () => { - invokeMock.mockImplementation(async (command: string) => { - if (command === 'lsp_open_workspace') { - return undefined; - } - if (command === 'lsp_get_server_state') { - return serverState('stopped'); - } - if (command === 'lsp_open_document') { - throw new Error('didOpen should not be sent for a stopped server'); - } - return undefined; - }); - - const manager = WorkspaceLspManager.getOrCreate('D:\\workspace\\BitFun'); - - await manager.openDocument('file:///D:/workspace/BitFun/src/main.rs', 'rust', 'fn main() {}'); - await manager.openDocument('file:///D:/workspace/BitFun/src/lib.rs', 'rust', 'pub fn lib() {}'); - - expect(invokeMock).toHaveBeenCalledTimes(2); - expect(invokeMock).toHaveBeenCalledWith('lsp_open_workspace', { - request: { workspacePath: 'D:\\workspace\\BitFun' } - }); - expect(invokeMock).toHaveBeenCalledWith('lsp_get_server_state', { - request: { - workspacePath: 'D:\\workspace\\BitFun', - language: 'rust' - } - }); - expect(invokeMock).not.toHaveBeenCalledWith('lsp_open_document', expect.anything()); - }); -}); - -describe('WorkspaceLspManager device surface scoping', () => { - afterEach(() => { - invokeMock.mockReset(); - listenMock.mockReset(); - listenMock.mockReturnValue(() => {}); - (WorkspaceLspManager as unknown as { - instances: Map; - }).instances.clear(); - resetDeviceSurfaceForTest(); - }); - - it('does not share a manager for the same workspace path across devices', () => { - const localManager = WorkspaceLspManager.getOrCreate('/Users/dev/BitFun'); - - activateSurface('peer-device-b'); - const peerManager = WorkspaceLspManager.getOrCreate('/Users/dev/BitFun'); - - expect(peerManager).not.toBe(localManager); - expect(peerManager.getSurfaceId()).toBe('peer-device-b'); - expect(WorkspaceLspManager.get('/Users/dev/BitFun')).toBe(peerManager); - }); - - it('detaches for a surface switch without sending anything to the device being left', async () => { - const unlisten = vi.fn(); - listenMock.mockReturnValue(unlisten); - invokeMock.mockResolvedValue(undefined); - - const manager = WorkspaceLspManager.getOrCreate('/Users/dev/BitFun'); - await manager.initialize(); - invokeMock.mockClear(); - - WorkspaceLspManager.detachAllForSurfaceSwitch(); - - expect(invokeMock).not.toHaveBeenCalled(); - expect(unlisten).toHaveBeenCalledTimes(1); - }); - - it('refuses to re-open a detached workspace on the device switched to', async () => { - invokeMock.mockResolvedValue(undefined); - - const manager = WorkspaceLspManager.getOrCreate('/Users/dev/BitFun'); - await manager.initialize(); - WorkspaceLspManager.detachAllForSurfaceSwitch(); - activateSurface('peer-device-b'); - invokeMock.mockClear(); - - const error = await manager.initialize().then(() => null, (reason: unknown) => reason); - - expect(isSurfaceChangedError(error)).toBe(true); - expect(invokeMock).not.toHaveBeenCalled(); - }); - - it('prevents every detached manager operation from reaching the next device', async () => { - invokeMock.mockResolvedValue(undefined); - - const manager = WorkspaceLspManager.getOrCreate('/Users/dev/BitFun'); - await manager.initialize(); - WorkspaceLspManager.detachAllForSurfaceSwitch(); - activateSurface('peer-device-b'); - invokeMock.mockClear(); - - await manager.changeDocument('file:///Users/dev/BitFun/src/main.rs', 'fn main() {}'); - - expect(invokeMock).not.toHaveBeenCalled(); - }); - - it('drops events that reach a manager after its surface was left', async () => { - let emitLspEvent: ((payload: unknown) => void) | null = null; - listenMock.mockImplementation((_event: string, callback: (payload: unknown) => void) => { - emitLspEvent = callback; - return () => {}; - }); - invokeMock.mockResolvedValue(undefined); - - const manager = WorkspaceLspManager.getOrCreate('/Users/dev/BitFun'); - await manager.initialize(); - - const diagnostics = vi.fn(); - manager.onDiagnostics('file:///Users/dev/BitFun/src/main.rs', diagnostics); - manager.detachForSurfaceSwitch(); - - emitLspEvent?.({ - type: 'Diagnostics', - data: { - workspace_path: '/Users/dev/BitFun', - uri: 'file:///Users/dev/BitFun/src/main.rs', - diagnostics: [{ message: 'from the device we left' }], - }, - }); - - expect(diagnostics).not.toHaveBeenCalled(); - }); -}); diff --git a/src/web-ui/src/tools/lsp/services/WorkspaceLspManager.ts b/src/web-ui/src/tools/lsp/services/WorkspaceLspManager.ts deleted file mode 100644 index bbed580ad0..0000000000 --- a/src/web-ui/src/tools/lsp/services/WorkspaceLspManager.ts +++ /dev/null @@ -1,946 +0,0 @@ -/** - * Workspace-scoped LSP manager. - * - * Provides a thin API over backend workspace LSP commands and bridges backend - * events to frontend consumers (e.g. diagnostics subscriptions, notifications). - */ - -import { api } from '@/infrastructure/api/service-api/ApiClient'; -import { notificationService } from '@/shared/notification-system'; -import { createLogger } from '@/shared/utils/logger'; -import { i18nService } from '@/infrastructure/i18n'; -import { - SurfaceChangedError, - getActiveSurfaceId, - getActiveSurfaceScope, - isSurfaceChangedError, - surfaceScopedKey, - type DeviceSurfaceId, - type SurfaceScope, -} from '@/infrastructure/peer-device/deviceSurface'; - -const log = createLogger('WorkspaceLspManager'); - -interface LspEvent { - type: 'ServerStateChanged' | 'DocumentOpened' | 'DocumentClosed' | 'WorkspaceOpened' | 'WorkspaceClosed' | 'ServerError' | 'Diagnostics' | 'IndexingProgress' | 'IndexingComplete' | 'ProjectDetected'; - data: { - workspace_path?: string; - language?: string; - plugin_name?: string; - status?: string; - message?: string; - uri?: string; - error?: string; - diagnostics?: any[]; - progress?: number; - project_info?: any; - }; -} - -interface ServerState { - status: 'stopped' | 'starting' | 'running' | 'failed' | 'restarting'; - language: string; - startedAt?: number; - lastError?: string; - restartCount: number; - documentCount: number; -} - -type OpenDocumentSkippedReason = 'server-not-running'; - -export interface OpenDocumentResult { - language: string; - opened: boolean; - skippedReason?: OpenDocumentSkippedReason; - serverStatus?: ServerState['status']; -} - -interface CachedServerStatus { - status: ServerState['status']; - updatedAt: number; -} - -export class WorkspaceLspManager { - /** - * Keyed by device surface **and** workspace path. The same repository is - * routinely checked out at the same path on two machines, so a path alone - * would hand a manager negotiated with one host to the other. - */ - private static instances = new Map(); - - private workspacePath: string; - private readonly surfaceId: DeviceSurfaceId; - private detached = false; - private eventUnlisten?: () => void; - private isInitialized = false; - private serverStatusByLanguage = new Map(); - private skippedOpenNoticeLanguages = new Set(); - private readonly SERVER_STATUS_CACHE_TTL_MS = 30000; - - - private diagnosticsCallbacks = new Map void>>(); - - - private indexingProgressNotifications = new Map void; - complete: (message?: string) => void; - }>(); - - private constructor(workspacePath: string, surfaceId: DeviceSurfaceId) { - this.workspacePath = workspacePath; - this.surfaceId = surfaceId; - } - - - getWorkspacePath(): string { - return this.workspacePath; - } - - getSurfaceId(): DeviceSurfaceId { - return this.surfaceId; - } - - private static instanceKey(workspacePath: string): string { - return surfaceScopedKey(getActiveSurfaceId(), workspacePath); - } - - - static getOrCreate(workspacePath: string): WorkspaceLspManager { - const key = this.instanceKey(workspacePath); - if (!this.instances.has(key)) { - const manager = new WorkspaceLspManager(workspacePath, getActiveSurfaceId()); - this.instances.set(key, manager); - } - return this.instances.get(key)!; - } - - - static get(workspacePath: string): WorkspaceLspManager | undefined { - return this.instances.get(this.instanceKey(workspacePath)); - } - - - static async remove(workspacePath: string): Promise { - const key = this.instanceKey(workspacePath); - const manager = this.instances.get(key); - if (manager) { - await manager.dispose(); - this.instances.delete(key); - } - } - - /** - * Drop all workspace LSP managers when the rendered device surface changes. - * - * Deliberately does **not** call `lsp_close_workspace`: that request would be - * sent to the device being left, shutting down language servers an agent - * turn there may still be using. Managers are recreated against the new - * transport on the next workspace bootstrap. - */ - static detachAllForSurfaceSwitch(): void { - for (const manager of this.instances.values()) { - manager.detachForSurfaceSwitch(); - } - this.instances.clear(); - } - - - async initialize(): Promise { - if (this.isInitialized) { - return; - } - - const scope = getActiveSurfaceScope(); - this.assertRendersActiveSurface(scope, 'lsp_open_workspace'); - - try { - - await this.invokeForActiveSurface('lsp_open_workspace', { - request: { workspacePath: this.workspacePath } - }); - - - // The transport may have swapped during the round trip. Binding the event - // listener now would attach this manager to the device that replaced ours. - this.assertRendersActiveSurface(scope, 'lsp_open_workspace'); - - this.eventUnlisten = api.listen('lsp-event', (payload) => { - this.handleLspEvent(payload); - }); - - this.isInitialized = true; - log.info('LSP initialized', { workspacePath: this.workspacePath }); - } catch (error) { - if (isSurfaceChangedError(error)) { - log.debug('LSP initialization abandoned after a device surface switch', { - workspacePath: this.workspacePath, - }); - } else { - log.error('LSP initialization failed', { workspacePath: this.workspacePath, error }); - } - throw error; - } - } - - /** - * A manager belongs to the device it was created for. Once the window renders - * another one, this manager must issue nothing further: its language servers - * keep serving the device we left, and its requests would otherwise be - * answered by the device we moved to. - */ - private assertRendersActiveSurface(scope: SurfaceScope, action: string): void { - if (this.detached || this.surfaceId !== scope.surfaceId || !scope.isCurrent()) { - throw new SurfaceChangedError(this.surfaceId, scope.epoch, action); - } - } - - /** - * The manager itself owns a surface, so checking only ApiClient's request - * epoch is insufficient: an old component can call a detached manager after - * the new surface is already active. All LSP traffic passes this ownership - * boundary before and after its await. - */ - private async invokeForActiveSurface( - action: string, - args?: Record, - ): Promise { - const scope = getActiveSurfaceScope(); - this.assertRendersActiveSurface(scope, action); - const result = await api.invoke(action, args); - this.assertRendersActiveSurface(scope, action); - return result; - } - - - private handleLspEvent(event: LspEvent) { - - if (this.detached || event.data.workspace_path !== this.workspacePath) { - return; - } - - switch (event.type) { - case 'ServerStateChanged': - this.onServerStateChanged(event.data); - break; - case 'ServerError': - this.onServerError(event.data); - break; - case 'ProjectDetected': - this.onProjectDetected(event.data); - break; - case 'IndexingProgress': - this.onIndexingProgress(event.data); - break; - case 'IndexingComplete': - this.onIndexingComplete(event.data); - break; - case 'Diagnostics': - this.onDiagnosticsReceived(event.data); - break; - default: - log.warn('Unknown LSP event', { eventType: event.type }); - } - } - - - private normalizeUri(uri: string): string { - if (!uri) return uri; - - - - return uri.toLowerCase(); - } - - - private normalizeLanguage(language: string): string { - return language.trim().toLowerCase(); - } - - - private isServerStatus(status?: string): status is ServerState['status'] { - return status === 'stopped' - || status === 'starting' - || status === 'running' - || status === 'failed' - || status === 'restarting'; - } - - - private rememberServerStatus(language: string, status: ServerState['status']): void { - const key = this.normalizeLanguage(language); - this.serverStatusByLanguage.set(key, { - status, - updatedAt: Date.now() - }); - - if (status === 'running') { - this.skippedOpenNoticeLanguages.delete(key); - } - } - - - private getFreshCachedServerStatus(language: string): ServerState['status'] | undefined { - const cached = this.serverStatusByLanguage.get(this.normalizeLanguage(language)); - if (!cached) { - return undefined; - } - - if (Date.now() - cached.updatedAt > this.SERVER_STATUS_CACHE_TTL_MS) { - return undefined; - } - - return cached.status; - } - - - private async getDocumentOpenAvailability(language: string): Promise<{ - canOpen: boolean; - status?: ServerState['status']; - skippedReason?: OpenDocumentSkippedReason; - }> { - const cachedStatus = this.getFreshCachedServerStatus(language); - if (cachedStatus) { - return cachedStatus === 'running' - ? { canOpen: true, status: cachedStatus } - : { - canOpen: false, - status: cachedStatus, - skippedReason: 'server-not-running' - }; - } - - const state = await this.getServerState(language); - if (!state) { - return { canOpen: true }; - } - - this.rememberServerStatus(language, state.status); - - return state.status === 'running' - ? { canOpen: true, status: state.status } - : { - canOpen: false, - status: state.status, - skippedReason: 'server-not-running' - }; - } - - - private onDiagnosticsReceived(data: LspEvent['data']) { - const { uri, diagnostics } = data; - - if (!uri || !diagnostics) { - log.error('Invalid diagnostics data', { uri, hasDiagnostics: !!diagnostics }); - return; - } - - - const normalizedUri = this.normalizeUri(uri); - - - const callbacks = this.diagnosticsCallbacks.get(normalizedUri); - if (callbacks && callbacks.length > 0) { - callbacks.forEach((cb) => { - try { - cb(diagnostics); - } catch (error) { - log.error('Diagnostics callback error', { uri, error }); - } - }); - } - } - - - private onServerStateChanged(data: LspEvent['data']) { - const { language, status, message } = data; - - if (!language) return; - - if (this.isServerStatus(status)) { - this.rememberServerStatus(language, status); - } - - switch (status) { - case 'starting': - - notificationService.progress({ - title: i18nService.t('settings/lsp:server.starting', { language }), - message: i18nService.t('settings/lsp:server.startingMessage'), - initialProgress: 20 - }); - break; - case 'running': - - - - break; - case 'failed': - notificationService.error( - message || i18nService.t('settings/lsp:server.checkConfig'), - { - title: i18nService.t('settings/lsp:server.startFailed', { language }), - duration: 5000 - } - ); - break; - } - } - - - private onServerError(data: LspEvent['data']) { - const { language, error } = data; - - if (!language || !error) return; - - log.error('Server error', { language, error }); - - notificationService.error( - error, - { - title: i18nService.t('settings/lsp:server.error', { language }), - duration: 5000 - } - ); - } - - - private onProjectDetected(_data: LspEvent['data']) { - - - } - - - private onIndexingProgress(data: LspEvent['data']) { - const { language, plugin_name, message, progress } = data; - - if (!language || !message) return; - - - const displayName = plugin_name || i18nService.t('settings/lsp:server.languageService', { language }); - - - if (!this.indexingProgressNotifications.has(language)) { - const loadingNotif = notificationService.loading({ - title: displayName, - message: message - }); - this.indexingProgressNotifications.set(language, loadingNotif); - } else { - - const loadingNotif = this.indexingProgressNotifications.get(language); - if (loadingNotif) { - loadingNotif.updateMessage(message); - } - } - - - if (progress !== undefined && progress >= 100) { - const loadingNotif = this.indexingProgressNotifications.get(language); - if (loadingNotif) { - loadingNotif.complete(); - this.indexingProgressNotifications.delete(language); - } - } - } - - - private onIndexingComplete(data: LspEvent['data']) { - const { language } = data; - - if (!language) return; - - - const progressNotif = this.indexingProgressNotifications.get(language); - if (progressNotif) { - progressNotif.complete(); - this.indexingProgressNotifications.delete(language); - } - } - - - async openDocument(uri: string, language: string, content: string): Promise { - if (!this.isInitialized) { - try { - await this.initialize(); - } catch (initError) { - if (!isSurfaceChangedError(initError)) { - log.error('Failed to initialize manager', { workspacePath: this.workspacePath, error: initError }); - } - throw initError; - } - } - - // didOpen is only meaningful when a language server is actually running. - // Checking the state here prevents layout/remount churn (for example window - // fullscreen transitions) from generating batches of backend no-op logs. - const availability = await this.getDocumentOpenAvailability(language); - if (!availability.canOpen) { - const key = this.normalizeLanguage(language); - if (!this.skippedOpenNoticeLanguages.has(key)) { - log.debug('Skipped LSP didOpen because language server is not running', { - workspacePath: this.workspacePath, - language, - status: availability.status - }); - this.skippedOpenNoticeLanguages.add(key); - } - - return { - language, - opened: false, - skippedReason: availability.skippedReason, - serverStatus: availability.status - }; - } - - try { - - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('LSP open document timeout after 30s')), 30000); - }); - - const openPromise = this.invokeForActiveSurface('lsp_open_document', { - request: { - workspacePath: this.workspacePath, - uri, - language, - content - } - }); - - await Promise.race([openPromise, timeoutPromise]); - - - - return { language, opened: true }; - } catch (error) { - log.error('Failed to open document', { workspacePath: this.workspacePath, uri, language, error }); - throw error; - } - } - - - - async changeDocument(uri: string, content: string): Promise { - try { - await this.invokeForActiveSurface('lsp_change_document', { - request: { - workspacePath: this.workspacePath, - uri, - content - } - }); - } catch (error) { - log.error('Failed to notify document change', { uri, error }); - } - } - - - async saveDocument(uri: string): Promise { - try { - await this.invokeForActiveSurface('lsp_save_document', { - request: { - workspacePath: this.workspacePath, - uri - } - }); - } catch (error) { - log.error('Failed to notify document save', { uri, error }); - } - } - - - async closeDocument(uri: string): Promise { - try { - await this.invokeForActiveSurface('lsp_close_document', { - request: { - workspacePath: this.workspacePath, - uri - } - }); - } catch (error) { - log.error('Failed to close document', { uri, error }); - } - } - - - async getCompletions(language: string, uri: string, line: number, character: number): Promise { - try { - const result = await this.invokeForActiveSurface('lsp_get_completions_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - line, - character - } - }); - return result || []; - } catch (error) { - log.error('Failed to get completions', { workspacePath: this.workspacePath, language, uri, line, character, error }); - return []; - } - } - - - async getHover(language: string, uri: string, line: number, character: number): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_hover_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - line, - character - } - }); - } catch (error: any) { - - - const errorStr = error?.message || String(error); - if (errorStr.includes('Missing result')) { - - return null; - } - log.error('Failed to get hover', { workspacePath: this.workspacePath, language, uri, line, character, error }); - return null; - } - } - - - async gotoDefinition(language: string, uri: string, line: number, character: number): Promise { - try { - return await this.invokeForActiveSurface('lsp_goto_definition_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - line, - character - } - }); - } catch (error) { - log.error('Failed to goto definition', { workspacePath: this.workspacePath, language, uri, line, character, error }); - return null; - } - } - - - async findReferences(language: string, uri: string, line: number, character: number): Promise { - try { - return await this.invokeForActiveSurface('lsp_find_references_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - line, - character - } - }); - } catch (error) { - log.error('Failed to find references', { workspacePath: this.workspacePath, language, uri, line, character, error }); - return null; - } - } - - - async getSignatureHelp(language: string, uri: string, line: number, character: number): Promise { - try { - const result = await this.invokeForActiveSurface('lsp_get_signature_help_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - line, - character - } - }); - return result; - } catch (error) { - log.error('Failed to get signature help', { workspacePath: this.workspacePath, language, uri, line, character, error }); - return null; - } - } - - - onDiagnostics(uri: string, callback: (diagnostics: any[]) => void): void { - - const normalizedUri = this.normalizeUri(uri); - - if (!this.diagnosticsCallbacks.has(normalizedUri)) { - this.diagnosticsCallbacks.set(normalizedUri, []); - } - this.diagnosticsCallbacks.get(normalizedUri)!.push(callback); - - - this.requestDiagnostics(uri).catch(() => { - // silent - }); - } - - - private async requestDiagnostics(uri: string): Promise { - try { - const diagnostics = await this.invokeForActiveSurface('lsp_get_diagnostics', { - request: { - workspacePath: this.workspacePath, - uri - } - }); - - - const callbacks = this.diagnosticsCallbacks.get(uri); - if (callbacks && diagnostics) { - callbacks.forEach(cb => cb(diagnostics)); - } - } catch (_error) { - - } - } - - - async formatDocument(language: string, uri: string, tabSize: number = 2, insertSpaces: boolean = true): Promise { - try { - return await this.invokeForActiveSurface('lsp_format_document_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - tabSize, - insertSpaces - } - }); - } catch (error) { - log.error('Failed to format document', { workspacePath: this.workspacePath, language, uri, tabSize, insertSpaces, error }); - return null; - } - } - - - async getInlayHints( - language: string, - uri: string, - startLine: number, - startCharacter: number, - endLine: number, - endCharacter: number - ): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_inlay_hints_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - startLine, - startCharacter, - endLine, - endCharacter - } - }); - } catch (error) { - log.error('Failed to get inlay hints', { workspacePath: this.workspacePath, language, uri, error }); - return []; - } - } - - - async rename(language: string, uri: string, line: number, character: number, newName: string): Promise { - try { - return await this.invokeForActiveSurface('lsp_rename_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - line, - character, - newName - } - }); - } catch (error) { - log.error('Failed to rename', { workspacePath: this.workspacePath, language, uri, line, character, newName, error }); - return null; - } - } - - - async getCodeActions(language: string, uri: string, range: any, context: any): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_code_actions_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - range, - context - } - }); - } catch (error) { - log.error('Failed to get code actions', { workspacePath: this.workspacePath, language, uri, error }); - return null; - } - } - - - async getDocumentSymbols(language: string, uri: string): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_document_symbols_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri - } - }); - } catch (error) { - log.error('Failed to get document symbols', { workspacePath: this.workspacePath, language, uri, error }); - return null; - } - } - - - async getWorkspaceSymbols(query: string): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_workspace_symbols', { - request: { - workspacePath: this.workspacePath, - query - } - }); - } catch (error) { - log.error('Failed to search workspace symbols', { workspacePath: this.workspacePath, query, error }); - return null; - } - } - - - async getDocumentHighlight(language: string, uri: string, line: number, character: number): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_document_highlight_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - line, - character - } - }); - } catch (error) { - log.error('Failed to get document highlight', { workspacePath: this.workspacePath, language, uri, line, character, error }); - return null; - } - } - - - async getSemanticTokens(language: string, uri: string): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_semantic_tokens_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri - } - }); - } catch (error) { - log.error('Failed to get semantic tokens', { workspacePath: this.workspacePath, language, uri, error }); - return null; - } - } - - - async getSemanticTokensRange( - language: string, - uri: string, - startLine: number, - startCharacter: number, - endLine: number, - endCharacter: number - ): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_semantic_tokens_range_workspace', { - request: { - workspacePath: this.workspacePath, - language, - uri, - startLine, - startCharacter, - endLine, - endCharacter - } - }); - } catch (error) { - log.error('Failed to get semantic tokens range', { workspacePath: this.workspacePath, language, uri, error }); - return null; - } - } - - - async getServerState(language: string): Promise { - try { - return await this.invokeForActiveSurface('lsp_get_server_state', { - request: { - workspacePath: this.workspacePath, - language - } - }); - } catch (error) { - log.error('Failed to get server state', { workspacePath: this.workspacePath, language, error }); - return null; - } - } - - - async getAllServerStates(): Promise> { - try { - return await this.invokeForActiveSurface>('lsp_get_all_server_states', { - request: { - workspacePath: this.workspacePath - } - }); - } catch (error) { - log.error('Failed to get all server states', { workspacePath: this.workspacePath, error }); - return {}; - } - } - - - /** - * Close the workspace on its host. Only for a real workspace close — a device - * surface switch must use `detachForSurfaceSwitch()`, because this runs before - * the transport swap and would shut down the language servers of the device - * being left. - */ - async dispose(): Promise { - try { - - if (this.eventUnlisten) { - this.eventUnlisten(); - this.eventUnlisten = undefined; - } - - - await this.invokeForActiveSurface('lsp_close_workspace', { - request: { workspacePath: this.workspacePath } - }); - - this.isInitialized = false; - } catch (error) { - log.error('Error during dispose', { workspacePath: this.workspacePath, error }); - } - } - - /** - * Release this manager's frontend state without touching the host. Used when - * the rendered device surface changes: the language servers belong to the - * device we are leaving and must keep running for its agent. - * - * `detached` is what stops a component that still holds this instance from - * re-initializing it onto the transport of the device we switched to. - */ - detachForSurfaceSwitch(): void { - this.detached = true; - if (this.eventUnlisten) { - this.eventUnlisten(); - this.eventUnlisten = undefined; - } - this.isInitialized = false; - } -} diff --git a/src/web-ui/src/tools/lsp/types/index.ts b/src/web-ui/src/tools/lsp/types/index.ts deleted file mode 100644 index 060d9a7b05..0000000000 --- a/src/web-ui/src/tools/lsp/types/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * LSP type definitions shared by services/components. - */ - -export interface LspPlugin { - id: string; - name: string; - version: string; - author: string; - description: string; - server: ServerConfig; - languages: string[]; - file_extensions: string[]; - capabilities: CapabilitiesConfig; - settings: Record; - checksum: string; - min_bitfun_version: string; -} - -export interface ServerConfig { - command: string; - args: string[]; - env: Record; -} - -export interface CapabilitiesConfig { - completion: boolean; - hover: boolean; - definition: boolean; - references: boolean; - rename: boolean; - formatting: boolean; - diagnostics: boolean; - inlayHints?: boolean; -} - -export interface CompletionItem { - label: string; - kind?: number; - detail?: string; - documentation?: any; - sortText?: string; - filterText?: string; - insertText?: string; - insertTextFormat?: number; -} - -export interface Position { - line: number; - character: number; -} - -export interface Range { - start: Position; - end: Position; -} - -export interface Diagnostic { - range: Range; - severity?: number; - code?: any; - source?: string; - message: string; -} - -export interface HoverInfo { - contents: any; - range?: Range; -} - -export interface Location { - uri: string; - range: Range; -} - -export interface TextEdit { - range: Range; - newText: string; -} - -export enum CompletionItemKind { - Text = 1, - Method = 2, - Function = 3, - Constructor = 4, - Field = 5, - Variable = 6, - Class = 7, - Interface = 8, - Module = 9, - Property = 10, - Unit = 11, - Value = 12, - Enum = 13, - Keyword = 14, - Snippet = 15, - Color = 16, - File = 17, - Reference = 18, -} - -export enum DiagnosticSeverity { - Error = 1, - Warning = 2, - Information = 3, - Hint = 4, -} - -export enum InlayHintKind { - Type = 1, - Parameter = 2, -} - -export interface InlayHintLabelPart { - value: string; - tooltip?: any; - location?: Location; -} - -export type InlayHintLabel = string | InlayHintLabelPart[]; - -export interface InlayHint { - position: Position; - label: InlayHintLabel; - kind?: InlayHintKind; - tooltip?: any; - paddingLeft?: boolean; - paddingRight?: boolean; - textEdits?: TextEdit[]; -} - From c30c538bcb77e1debb00c9a52ea30b40e35f4e2a Mon Sep 17 00:00:00 2001 From: GCWing Date: Sat, 22 Aug 2026 23:21:14 +0800 Subject: [PATCH 2/2] fix: update retired LSP capability catalog --- docs/interactive-capabilities/README.md | 12 +- .../capabilities.json | 55 +-- .../capabilities/feature.files-editor.md | 12 +- .../technical/tauri-command-map.json | 434 +----------------- .../technical/ui-interaction-inventory.json | 31 +- scripts/generate-interactive-capabilities.mjs | 13 +- scripts/interactive-capabilities.test.mjs | 16 + .../interactive-capabilities/catalog.json | 22 +- .../generated/interactive-capabilities.json | 55 +-- 9 files changed, 96 insertions(+), 554 deletions(-) diff --git a/docs/interactive-capabilities/README.md b/docs/interactive-capabilities/README.md index 9dccf58b8c..85d5dc6474 100644 --- a/docs/interactive-capabilities/README.md +++ b/docs/interactive-capabilities/README.md @@ -1,9 +1,9 @@ # BitFun 功能与设置目录 / BitFun Features & Settings -BitFun Playbook 当前包含 **22 个功能**和 **16 个设置页**,共 **38 个**用户可理解的条目、**297 项**有源码证据的子能力。每个条目有独立 Markdown,并直接服务于说明书网站、BitFun 全局搜索和 `BitFunControl` Agent 工具。 +BitFun Playbook 当前包含 **22 个功能**和 **16 个设置页**,共 **38 个**用户可理解的条目、**295 项**有源码证据的子能力。每个条目有独立 Markdown,并直接服务于说明书网站、BitFun 全局搜索和 `BitFunControl` Agent 工具。 -BitFun Playbook currently contains **22 features**, **16 settings pages**, and **297** source-backed sub-capabilities across **38** user-facing entries. Every entry has its own Markdown page and directly powers the website, in-app global search, and the `BitFunControl` agent tool. +BitFun Playbook currently contains **22 features**, **16 settings pages**, and **295** source-backed sub-capabilities across **38** user-facing entries. Every entry has its own Markdown page and directly powers the website, in-app global search, and the `BitFunControl` agent tool. ## 唯一事实源 / Single source of truth @@ -13,20 +13,20 @@ BitFun Playbook currently contains **22 features**, **16 settings pages**, and * - 生成的底层审计表:`docs/interactive-capabilities/technical/tauri-command-map.json` - 生成的可见交互审计表:`docs/interactive-capabilities/technical/ui-interaction-inventory.json` -说明书、网站、搜索和 Agent 只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **692** 个 Tauri 命令,以及 **370** 个产品交互源码文件中的 **4441** 个交互候选,只用于实现覆盖审计。 +说明书、网站、搜索和 Agent 只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **649** 个 Tauri 命令,以及 **367** 个产品交互源码文件中的 **4424** 个交互候选,只用于实现覆盖审计。 -Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **692** Tauri commands and **4441** interaction candidates across **370** product UI source files remain implementation-audit evidence only. +Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **649** Tauri commands and **4424** interaction candidates across **367** product UI source files remain implementation-audit evidence only. ## 控制边界 / Control boundary - 每个功能或设置都有可执行的产品入口;存在多视图的设置页还必须把每个视图映射到至少一个子能力,搜索和 Agent 可携带子能力 ID 精确跳转。 - 简单、稳定且适合自动化的行为才声明为类型化 `operations` 或 `options`。复杂流程由 Agent 打开对应界面交给用户继续,不向 Agent 暴露原始 Tauri Command。 -- `BitFunControl list` 一次返回完整的精简目录(目录上限 50 项);`search` 默认只返回前 20 项。完整目录和 297 项子能力都不会写入 system prompt。 +- `BitFunControl list` 一次返回完整的精简目录(目录上限 50 项);`search` 默认只返回前 20 项。完整目录和 295 项子能力都不会写入 system prompt。 - Desktop 只有在前端控制监听器完成握手后才发布工具;CLI、Detached Dispatch 等没有交互界面的运行方式明确不提供该工具。只读 Agent 只能发现和读取目录。 - Every feature or setting has an executable product destination. Multi-view settings pages must map every registered view to at least one documented item, so search and agents can carry an item ID for exact navigation. - Only simple, stable, automation-safe behavior becomes a typed `operation` or `option`. Complex flows open their owning UI instead of exposing raw Tauri commands to agents. -- One default `BitFunControl list` call returns the complete curated catalog (hard limit: 50); `search` defaults to 20 results. Neither the full catalog nor its 297 documented items enters the system prompt. +- One default `BitFunControl list` call returns the complete curated catalog (hard limit: 50); `search` defaults to 20 results. Neither the full catalog nor its 295 documented items enters the system prompt. - Desktop advertises the tool only after the frontend control listener completes its readiness handshake. Headless surfaces such as CLI and Detached Dispatch explicitly omit it, and read-only agents may only discover and inspect entries. ## 防腐化门禁 / Anti-drift gates diff --git a/docs/interactive-capabilities/capabilities.json b/docs/interactive-capabilities/capabilities.json index c4283dbe4e..6c75662c10 100644 --- a/docs/interactive-capabilities/capabilities.json +++ b/docs/interactive-capabilities/capabilities.json @@ -4,12 +4,12 @@ "title": "BitFun Playbook", "origin": "https://playbook.openbitfun.com", "source": "src/shared/interactive-capabilities/catalog.json", - "digest": "3207e4dc899c9109f0be952b9497d2f6169f9e2bb3ba9a859dda7e6959903470", + "digest": "39a0d386d2b6251246cceb9060eb3c3672b257d9062b67f0be73ea46e117ddeb", "counts": { "features": 22, "settings": 16, "userFacing": 38, - "documentedItems": 297 + "documentedItems": 295 }, "categories": { "assistant": { @@ -691,16 +691,16 @@ "categoryId": "workspace", "titleZh": "文件与代码编辑", "titleEn": "Files & code editing", - "summaryZh": "浏览、搜索和编辑项目文件,查看差异、代码符号、诊断与 Agent 产生的改动。", - "summaryEn": "Browse, search, and edit project files while inspecting diffs, symbols, diagnostics, and agent changes.", + "summaryZh": "浏览、搜索和编辑项目文件,查看差异、按语言可用的编辑操作与 Agent 产生的改动。", + "summaryEn": "Browse, search, and edit project files while inspecting diffs, language-aware editor actions, and agent changes.", "keywordsZh": [ "文件", "代码", "编辑器", "搜索文件", "差异", - "诊断", - "LSP" + "格式化", + "符号导航" ], "keywordsEn": [ "files", @@ -708,17 +708,17 @@ "editor", "file search", "diff", - "diagnostics", - "LSP" + "formatting", + "symbol navigation" ], "highlightsZh": [ "文件树与全文搜索", - "代码导航、格式化和诊断", + "按当前语言实际支持情况提供格式化与代码导航", "查看、接受或撤销 Agent 改动" ], "highlightsEn": [ "File tree and full-text search", - "Code navigation, formatting, and diagnostics", + "Language-aware formatting and code navigation when supported", "Review, accept, or revert agent changes" ], "items": [ @@ -753,19 +753,9 @@ "titleEn": "Review file history and snapshots, then roll back a session or selected turn" }, { - "id": "lsp-navigation", - "titleZh": "使用 Hover、跳转定义、查找引用、文档符号与高亮", - "titleEn": "Use hover, go to definition, references, document symbols, and highlights" - }, - { - "id": "lsp-editing", - "titleZh": "使用补全、格式化、重命名、代码操作、内联提示与语义高亮", - "titleEn": "Use completion, formatting, rename, code actions, inlay hints, and semantic highlighting" - }, - { - "id": "lsp-plugins", - "titleZh": "检测项目并安装、启动、停止或卸载 LSP 插件", - "titleEn": "Detect projects and install, start, stop, or uninstall LSP plugins" + "id": "language-actions", + "titleZh": "仅在当前语言提供器实际支持时显示格式化、定义与类型跳转、引用、重命名、快速修复和符号导航", + "titleEn": "Show formatting, definition and type navigation, references, rename, quick fixes, and symbol navigation only when supported by the active language provider" }, { "id": "attachments", @@ -808,19 +798,20 @@ "编辑器", "搜索文件", "差异", - "诊断", - "LSP", + "格式化", + "符号导航", "files", "code", "editor", "file search", "diff", - "diagnostics", + "formatting", + "symbol navigation", "文件树与全文搜索", - "代码导航、格式化和诊断", + "按当前语言实际支持情况提供格式化与代码导航", "查看、接受或撤销 Agent 改动", "File tree and full-text search", - "Code navigation, formatting, and diagnostics", + "Language-aware formatting and code navigation when supported", "Review, accept, or revert agent changes", "浏览文件树并创建、重命名、删除或在系统中显示文件和目录", "Browse the file tree and create, rename, delete, or reveal files and directories", @@ -834,12 +825,8 @@ "Accept or reject agent changes by file, operation, or whole session", "查看文件变更历史和快照,并回滚整个会话或指定轮次", "Review file history and snapshots, then roll back a session or selected turn", - "使用 Hover、跳转定义、查找引用、文档符号与高亮", - "Use hover, go to definition, references, document symbols, and highlights", - "使用补全、格式化、重命名、代码操作、内联提示与语义高亮", - "Use completion, formatting, rename, code actions, inlay hints, and semantic highlighting", - "检测项目并安装、启动、停止或卸载 LSP 插件", - "Detect projects and install, start, stop, or uninstall LSP plugins", + "仅在当前语言提供器实际支持时显示格式化、定义与类型跳转、引用、重命名、快速修复和符号导航", + "Show formatting, definition and type navigation, references, rename, quick fixes, and symbol navigation only when supported by the active language provider", "粘贴、拖放或上传文件与图片作为会话上下文", "Paste, drop, or upload files and images as session context" ], diff --git a/docs/interactive-capabilities/capabilities/feature.files-editor.md b/docs/interactive-capabilities/capabilities/feature.files-editor.md index 8d73200fde..20a1d998e4 100644 --- a/docs/interactive-capabilities/capabilities/feature.files-editor.md +++ b/docs/interactive-capabilities/capabilities/feature.files-editor.md @@ -11,9 +11,9 @@ title_en: "Files & code editing" > 功能 / Feature -浏览、搜索和编辑项目文件,查看差异、代码符号、诊断与 Agent 产生的改动。 +浏览、搜索和编辑项目文件,查看差异、按语言可用的编辑操作与 Agent 产生的改动。 -Browse, search, and edit project files while inspecting diffs, symbols, diagnostics, and agent changes. +Browse, search, and edit project files while inspecting diffs, language-aware editor actions, and agent changes. ## 完整功能清单 / Everything included @@ -29,12 +29,8 @@ Browse, search, and edit project files while inspecting diffs, symbols, diagnost - Accept or reject agent changes by file, operation, or whole session - 查看文件变更历史和快照,并回滚整个会话或指定轮次 - Review file history and snapshots, then roll back a session or selected turn -- 使用 Hover、跳转定义、查找引用、文档符号与高亮 - - Use hover, go to definition, references, document symbols, and highlights -- 使用补全、格式化、重命名、代码操作、内联提示与语义高亮 - - Use completion, formatting, rename, code actions, inlay hints, and semantic highlighting -- 检测项目并安装、启动、停止或卸载 LSP 插件 - - Detect projects and install, start, stop, or uninstall LSP plugins +- 仅在当前语言提供器实际支持时显示格式化、定义与类型跳转、引用、重命名、快速修复和符号导航 + - Show formatting, definition and type navigation, references, rename, quick fixes, and symbol navigation only when supported by the active language provider - 粘贴、拖放或上传文件与图片作为会话上下文 - Paste, drop, or upload files and images as session context diff --git a/docs/interactive-capabilities/technical/tauri-command-map.json b/docs/interactive-capabilities/technical/tauri-command-map.json index 37d294e5a9..56b713547a 100644 --- a/docs/interactive-capabilities/technical/tauri-command-map.json +++ b/docs/interactive-capabilities/technical/tauri-command-map.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, "generatedFrom": "src/shared/interactive-capabilities/catalog.json", - "catalogDigest": "3207e4dc899c9109f0be952b9497d2f6169f9e2bb3ba9a859dda7e6959903470", - "commandCount": 692, + "catalogDigest": "39a0d386d2b6251246cceb9060eb3c3672b257d9062b67f0be73ea46e117ddeb", + "commandCount": 649, "commands": [ { "id": "accept_file", @@ -3294,436 +3294,6 @@ "signature": "fn logout_subscription_account( request: SubscriptionProviderRequest, ) -> Result", "remoteWorkspacePolicy": "LocalOnly" }, - { - "id": "lsp_change_document", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_change_document", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_change_document(request: ChangeDocumentRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_close_document", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_close_document", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_close_document(request: CloseDocumentRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_close_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_close_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_close_workspace(request: OpenWorkspaceRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_detect_project", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_detect_project", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_detect_project( request: DetectProjectRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_did_change", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_did_change", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_did_change(request: DidChangeRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_did_close", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_did_close", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_did_close(request: DidCloseRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_did_open", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_did_open", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_did_open(request: DidOpenRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_did_save", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_did_save", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_did_save(request: DidSaveRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_find_references", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_find_references", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_find_references( request: FindReferencesRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_find_references_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_find_references_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_find_references_workspace( request: FindReferencesRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_format_document", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_format_document", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_format_document( request: FormatDocumentRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_format_document_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_format_document_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_format_document_workspace( request: FormatDocumentRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_all_server_states", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_all_server_states", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_all_server_states( request: OpenWorkspaceRequest, ) -> Result, String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_code_actions_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_code_actions_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_code_actions_workspace( request: GetCodeActionsRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_completions", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_completions", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_get_completions( request: GetCompletionsRequest, ) -> Result, String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_completions_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_completions_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_completions_workspace( request: GetCompletionsRequest, ) -> Result, String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_document_highlight_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_document_highlight_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_document_highlight_workspace( request: GetDocumentHighlightRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_document_symbols_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_document_symbols_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_document_symbols_workspace( request: GetDocumentSymbolsRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_hover", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_hover", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_get_hover(request: GetHoverRequest) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_hover_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_hover_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_hover_workspace( request: GetHoverRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_inlay_hints_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_inlay_hints_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_inlay_hints_workspace( request: GetInlayHintsRequest, ) -> Result, String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_plugin", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_plugin", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_get_plugin(request: GetPluginRequest) -> Result, String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_semantic_tokens_range_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_semantic_tokens_range_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_semantic_tokens_range_workspace( request: GetSemanticTokensRangeRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_semantic_tokens_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_semantic_tokens_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_semantic_tokens_workspace( request: GetSemanticTokensRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_server_capabilities", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_server_capabilities", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_get_server_capabilities( request: GetServerCapabilitiesRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_server_state", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_server_state", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_get_server_state(request: GetServerStateRequest) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_get_supported_extensions", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_get_supported_extensions", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_get_supported_extensions() -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_goto_definition", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_goto_definition", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_goto_definition( request: GotoDefinitionRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_goto_definition_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_goto_definition_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_goto_definition_workspace( request: GotoDefinitionRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_initialize", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_initialize", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_initialize() -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_install_plugin", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_install_plugin", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_install_plugin(request: InstallPluginRequest) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_list_plugins", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_list_plugins", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_list_plugins() -> Result, String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_list_workspaces", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_list_workspaces", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_list_workspaces() -> Result, String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_open_document", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_open_document", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_open_document(request: OpenDocumentRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_open_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_open_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_open_workspace( request: OpenWorkspaceRequest, app_handle: tauri::AppHandle, ) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_prestart_server", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_prestart_server", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_prestart_server(request: PrestartServerRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_rename_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_rename_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_rename_workspace(request: RenameRequest) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_save_document", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_save_document", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_save_document(request: SaveDocumentRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_start_server_for_file", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_start_server_for_file", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_start_server_for_file( request: StartServerForFileRequest, ) -> Result", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_stop_all_servers", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_stop_all_servers", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_stop_all_servers() -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_stop_server", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_stop_server", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_stop_server(request: StopServerRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_stop_server_workspace", - "moduleId": "lsp_workspace", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_stop_server_workspace", - "sourceFile": "src/apps/desktop/src/api/lsp_workspace_api.rs", - "signature": "fn lsp_stop_server_workspace(request: GetServerStateRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, - { - "id": "lsp_uninstall_plugin", - "moduleId": "lsp", - "capabilityId": "feature.files-editor", - "visibility": "implementation", - "rustPath": "lsp_uninstall_plugin", - "sourceFile": "src/apps/desktop/src/api/lsp_api.rs", - "signature": "fn lsp_uninstall_plugin(request: UninstallPluginRequest) -> Result<(), String>", - "remoteWorkspacePolicy": "LegacyUnaudited" - }, { "id": "mark_announcement_seen", "moduleId": "announcement", diff --git a/docs/interactive-capabilities/technical/ui-interaction-inventory.json b/docs/interactive-capabilities/technical/ui-interaction-inventory.json index 7b4f7ce5ba..ee25cc037d 100644 --- a/docs/interactive-capabilities/technical/ui-interaction-inventory.json +++ b/docs/interactive-capabilities/technical/ui-interaction-inventory.json @@ -1,13 +1,13 @@ { "schemaVersion": 1, "generatedFrom": "src/shared/interactive-capabilities/catalog.json", - "catalogDigest": "3207e4dc899c9109f0be952b9497d2f6169f9e2bb3ba9a859dda7e6959903470", + "catalogDigest": "39a0d386d2b6251246cceb9060eb3c3672b257d9062b67f0be73ea46e117ddeb", "roots": [ "src/web-ui/src" ], - "digest": "644f35d8f903d81ae15754aca9ac5c0ff23d26b6da878cbc1c5cfd2c480d90d0", - "fileCount": 370, - "interactionCount": 4441, + "digest": "9dd485a7d47621da8491ecac62d2b16f7a91d47f0bda1ea73989d7e82cc212a6", + "fileCount": 367, + "interactionCount": 4424, "files": [ { "sourceFile": "src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx", @@ -1369,11 +1369,6 @@ "interactionCount": 10, "digest": "c973526c84c73d048e5759050ac5c0b0b63e9fb26f6d6909bc5d09717da52191" }, - { - "sourceFile": "src/web-ui/src/infrastructure/config/components/LspConfig.tsx", - "interactionCount": 10, - "digest": "23120608db3c46d70af9b5d693cc14de002ab72d693741cc38c0756c02734d35" - }, { "sourceFile": "src/web-ui/src/infrastructure/config/components/MCPResourceBrowser.tsx", "interactionCount": 7, @@ -1591,8 +1586,8 @@ }, { "sourceFile": "src/web-ui/src/shared/context-menu-system/providers/EditorMenuProvider.ts", - "interactionCount": 2, - "digest": "6af7643a21c2bc21b0f4dc3e0a96016ee42b9c5a7b8dd641929093060c8f41b0" + "interactionCount": 3, + "digest": "92871ae42c86c5a737f97c6e645e1485324ea031d16bbe512509c778285bf1bd" }, { "sourceFile": "src/web-ui/src/shared/context-menu-system/providers/FileExplorerMenuProvider.ts", @@ -1706,8 +1701,8 @@ }, { "sourceFile": "src/web-ui/src/tools/editor/components/CodeEditor.tsx", - "interactionCount": 4, - "digest": "f5649543b779a51fbafcc155ec4914bd7895fdd61cc51b03438f5e46c0587a8a" + "interactionCount": 5, + "digest": "e769ac327279aa6cef25bc8b6860c1cdcc1a5868f1aba3d981e10c1f984c87ce" }, { "sourceFile": "src/web-ui/src/tools/editor/components/DiffEditor.tsx", @@ -1839,16 +1834,6 @@ "interactionCount": 12, "digest": "7a83675c6693beb49f8b41f8209175830b7637ff54442a706766469cd5088bae" }, - { - "sourceFile": "src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.tsx", - "interactionCount": 6, - "digest": "16b654b54d296e41aa421b57a2d160b7834dbd499c9f8b68b5b93b46d3d636a0" - }, - { - "sourceFile": "src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.tsx", - "interactionCount": 3, - "digest": "5d6404dcad9f4764b13ae606f8ad6bbb929e844ff23ec459c70ab82d5be6e028" - }, { "sourceFile": "src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx", "interactionCount": 6, diff --git a/scripts/generate-interactive-capabilities.mjs b/scripts/generate-interactive-capabilities.mjs index 8c0c774467..3893722a9f 100644 --- a/scripts/generate-interactive-capabilities.mjs +++ b/scripts/generate-interactive-capabilities.mjs @@ -184,10 +184,15 @@ function buildInteractionInventory(roots) { const normalized = toPosix(file); return /\.(?:ts|tsx)$/u.test(file) && !/\.(?:test|spec|stories|example)\.(?:ts|tsx)$/u.test(file) - && !normalized.includes('/__tests__/'); - }).sort(); - for (const file of sourceFiles) { - const relativeFile = toPosix(path.relative(REPO_ROOT, file)); + && !normalized.includes('/__tests__/') + && !normalized.includes('/generated/'); + }) + .map((file) => ({ + file, + relativeFile: toPosix(path.relative(REPO_ROOT, file)), + })) + .sort((left, right) => left.relativeFile < right.relativeFile ? -1 : left.relativeFile > right.relativeFile ? 1 : 0); + for (const { file, relativeFile } of sourceFiles) { const matchedLines = readFileSync(file, 'utf8') .split(/\r?\n/u) .map((line) => line.trim().replace(/\s+/gu, ' ')) diff --git a/scripts/interactive-capabilities.test.mjs b/scripts/interactive-capabilities.test.mjs index 1656723bcc..0bd62aee69 100644 --- a/scripts/interactive-capabilities.test.mjs +++ b/scripts/interactive-capabilities.test.mjs @@ -80,6 +80,19 @@ test('the built-in browser manual covers its element picker in both languages', assert.ok(browser.searchTerms.includes('Start the element picker to highlight hovered elements and show tag, ID, and class details')); }); +test('the files and editor manual exposes provider-backed actions without retired LSP facts', async () => { + const source = JSON.parse(await read('src/shared/interactive-capabilities/catalog.json')); + const filesEditor = source.capabilities.find(({ id }) => id === 'feature.files-editor'); + assert.ok(filesEditor); + assert.ok(filesEditor.items.some(({ id }) => id === 'language-actions')); + assert.doesNotMatch(JSON.stringify(filesEditor), /\blsp\b|language server/iu); + assert.equal(Object.hasOwn(source.implementationOwners, 'lsp'), false); + assert.equal(Object.hasOwn(source.implementationOwners, 'lsp_workspace'), false); + + const { technicalMap } = buildCapabilityCatalog(); + assert.equal(technicalMap.commands.some(({ id }) => id.startsWith('lsp_')), false); +}); + test('built-in and external browsers share one agent action contract', async () => { const { publicCatalog } = buildCapabilityCatalog(); const browser = publicCatalog.capabilities.find(({ id }) => id === 'feature.browser'); @@ -153,6 +166,9 @@ test('docs, runtime, and technical views are generated projections of one semant assert.equal(interactionMap.digest, source.reviewedInteractionContract.digest); assert.ok(interactionMap.fileCount >= 300); assert.ok(interactionMap.interactionCount >= 4_000); + const interactionSourceFiles = interactionMap.files.map(({ sourceFile }) => sourceFile); + assert.deepEqual(interactionSourceFiles, [...interactionSourceFiles].sort()); + assert.ok(interactionSourceFiles.every((sourceFile) => !sourceFile.includes('/generated/'))); assert.ok(interactionMap.files.some(({ sourceFile }) => sourceFile.endsWith('/BrowserPanel.tsx'))); assert.ok(interactionMap.files.some(({ sourceFile }) => diff --git a/src/shared/interactive-capabilities/catalog.json b/src/shared/interactive-capabilities/catalog.json index 7a8faf82c7..41299bb236 100644 --- a/src/shared/interactive-capabilities/catalog.json +++ b/src/shared/interactive-capabilities/catalog.json @@ -43,9 +43,9 @@ }, "reviewedInteractionContract": { "roots": ["src/web-ui/src"], - "digest": "644f35d8f903d81ae15754aca9ac5c0ff23d26b6da878cbc1c5cfd2c480d90d0" + "digest": "9dd485a7d47621da8491ecac62d2b16f7a91d47f0bda1ea73989d7e82cc212a6" }, - "reviewedDocumentedItemCount": 297, + "reviewedDocumentedItemCount": 295, "capabilities": [ { "id": "feature.ai-assistant", @@ -203,12 +203,12 @@ "categoryId": "workspace", "titleZh": "文件与代码编辑", "titleEn": "Files & code editing", - "summaryZh": "浏览、搜索和编辑项目文件,查看差异、代码符号、诊断与 Agent 产生的改动。", - "summaryEn": "Browse, search, and edit project files while inspecting diffs, symbols, diagnostics, and agent changes.", - "keywordsZh": ["文件", "代码", "编辑器", "搜索文件", "差异", "诊断", "LSP"], - "keywordsEn": ["files", "code", "editor", "file search", "diff", "diagnostics", "LSP"], - "highlightsZh": ["文件树与全文搜索", "代码导航、格式化和诊断", "查看、接受或撤销 Agent 改动"], - "highlightsEn": ["File tree and full-text search", "Code navigation, formatting, and diagnostics", "Review, accept, or revert agent changes"], + "summaryZh": "浏览、搜索和编辑项目文件,查看差异、按语言可用的编辑操作与 Agent 产生的改动。", + "summaryEn": "Browse, search, and edit project files while inspecting diffs, language-aware editor actions, and agent changes.", + "keywordsZh": ["文件", "代码", "编辑器", "搜索文件", "差异", "格式化", "符号导航"], + "keywordsEn": ["files", "code", "editor", "file search", "diff", "formatting", "symbol navigation"], + "highlightsZh": ["文件树与全文搜索", "按当前语言实际支持情况提供格式化与代码导航", "查看、接受或撤销 Agent 改动"], + "highlightsEn": ["File tree and full-text search", "Language-aware formatting and code navigation when supported", "Review, accept, or revert agent changes"], "items": [ { "id": "file-tree", "titleZh": "浏览文件树并创建、重命名、删除或在系统中显示文件和目录", "titleEn": "Browse the file tree and create, rename, delete, or reveal files and directories", "evidence": ["command:explorer_get_file_tree", "command:create_file", "command:create_directory", "command:rename_file", "command:delete_file", "command:delete_directory", "command:reveal_in_explorer"] }, { "id": "indexed-search", "titleZh": "建立或重建索引,按文件名和全文内容流式搜索", "titleEn": "Build or rebuild an index and stream filename and full-text search results", "evidence": ["command:search_build_index", "command:search_rebuild_index", "command:start_search_filenames_stream", "command:start_search_file_contents_stream"] }, @@ -216,9 +216,7 @@ { "id": "diffs", "titleZh": "查看修改文件、会话、轮次和单次操作的差异与统计", "titleEn": "Inspect diffs and statistics by file, session, turn, or operation", "evidence": ["command:get_all_modified_files", "command:get_file_diff", "command:get_session_file_diff_stats", "command:get_turn_files", "command:get_operation_diff"] }, { "id": "accept-reject", "titleZh": "按文件、操作或整个会话接受或拒绝 Agent 改动", "titleEn": "Accept or reject agent changes by file, operation, or whole session", "evidence": ["command:accept_file", "command:reject_file", "command:accept_operation", "command:reject_operation", "command:accept_session"] }, { "id": "snapshots-rollback", "titleZh": "查看文件变更历史和快照,并回滚整个会话或指定轮次", "titleEn": "Review file history and snapshots, then roll back a session or selected turn", "evidence": ["command:get_file_change_history", "command:get_snapshot_sessions", "command:rollback_session", "command:rollback_session_to_turn"] }, - { "id": "lsp-navigation", "titleZh": "使用 Hover、跳转定义、查找引用、文档符号与高亮", "titleEn": "Use hover, go to definition, references, document symbols, and highlights", "evidence": ["command:lsp_get_hover_workspace", "command:lsp_goto_definition_workspace", "command:lsp_find_references_workspace", "command:lsp_get_document_symbols_workspace", "command:lsp_get_document_highlight_workspace"] }, - { "id": "lsp-editing", "titleZh": "使用补全、格式化、重命名、代码操作、内联提示与语义高亮", "titleEn": "Use completion, formatting, rename, code actions, inlay hints, and semantic highlighting", "evidence": ["command:lsp_get_completions_workspace", "command:lsp_format_document_workspace", "command:lsp_rename_workspace", "command:lsp_get_code_actions_workspace", "command:lsp_get_inlay_hints_workspace", "command:lsp_get_semantic_tokens_workspace"] }, - { "id": "lsp-plugins", "titleZh": "检测项目并安装、启动、停止或卸载 LSP 插件", "titleEn": "Detect projects and install, start, stop, or uninstall LSP plugins", "evidence": ["command:lsp_detect_project", "command:lsp_install_plugin", "command:lsp_prestart_server", "command:lsp_stop_server_workspace", "command:lsp_uninstall_plugin"] }, + { "id": "language-actions", "titleZh": "仅在当前语言提供器实际支持时显示格式化、定义与类型跳转、引用、重命名、快速修复和符号导航", "titleEn": "Show formatting, definition and type navigation, references, rename, quick fixes, and symbol navigation only when supported by the active language provider", "evidence": ["source:src/web-ui/src/shared/context-menu-system/providers/EditorMenuProvider.ts#supportsEditorAction"] }, { "id": "attachments", "titleZh": "粘贴、拖放或上传文件与图片作为会话上下文", "titleEn": "Paste, drop, or upload files and images as session context", "evidence": ["command:get_clipboard_files", "command:paste_files", "command:upload_image_contexts"] } ], "stepsZh": ["先打开一个项目", "打开“文件”", "选择文件进行编辑,或搜索内容与符号"], @@ -1541,11 +1539,9 @@ "git": { "capabilityId": "feature.git", "reviewedCommandCount": 31 }, "external_sources": { "capabilityId": "feature.ecosystem-compatibility", "reviewedCommandCount": 25 }, "snapshot_service": { "capabilityId": "feature.files-editor", "reviewedCommandCount": 24 }, - "lsp_workspace": { "capabilityId": "feature.files-editor", "reviewedCommandCount": 24 }, "mcp": { "capabilityId": "setting.tools.mcp", "reviewedCommandCount": 23 }, "config": { "capabilityId": "setting.application.general", "reviewedCommandCount": 19 }, "session": { "capabilityId": "feature.ai-assistant", "reviewedCommandCount": 19 }, - "lsp": { "capabilityId": "feature.files-editor", "reviewedCommandCount": 19 }, "dispatch": { "capabilityId": "feature.detached-dispatch", "reviewedCommandCount": 18 }, "system": { "capabilityId": "setting.application.general", "reviewedCommandCount": 17 }, "miniapp_market": { "capabilityId": "feature.miniapps", "reviewedCommandCount": 17 }, diff --git a/src/web-ui/src/app/global-search/generated/interactive-capabilities.json b/src/web-ui/src/app/global-search/generated/interactive-capabilities.json index 4e06de54c1..767548960b 100644 --- a/src/web-ui/src/app/global-search/generated/interactive-capabilities.json +++ b/src/web-ui/src/app/global-search/generated/interactive-capabilities.json @@ -4,12 +4,12 @@ "title": "BitFun Playbook", "origin": "https://playbook.openbitfun.com", "source": "src/shared/interactive-capabilities/catalog.json", - "digest": "3207e4dc899c9109f0be952b9497d2f6169f9e2bb3ba9a859dda7e6959903470", + "digest": "39a0d386d2b6251246cceb9060eb3c3672b257d9062b67f0be73ea46e117ddeb", "counts": { "features": 22, "settings": 16, "userFacing": 38, - "documentedItems": 297 + "documentedItems": 295 }, "categories": { "assistant": { @@ -703,16 +703,16 @@ "categoryId": "workspace", "titleZh": "文件与代码编辑", "titleEn": "Files & code editing", - "summaryZh": "浏览、搜索和编辑项目文件,查看差异、代码符号、诊断与 Agent 产生的改动。", - "summaryEn": "Browse, search, and edit project files while inspecting diffs, symbols, diagnostics, and agent changes.", + "summaryZh": "浏览、搜索和编辑项目文件,查看差异、按语言可用的编辑操作与 Agent 产生的改动。", + "summaryEn": "Browse, search, and edit project files while inspecting diffs, language-aware editor actions, and agent changes.", "keywordsZh": [ "文件", "代码", "编辑器", "搜索文件", "差异", - "诊断", - "LSP" + "格式化", + "符号导航" ], "keywordsEn": [ "files", @@ -720,17 +720,17 @@ "editor", "file search", "diff", - "diagnostics", - "LSP" + "formatting", + "symbol navigation" ], "highlightsZh": [ "文件树与全文搜索", - "代码导航、格式化和诊断", + "按当前语言实际支持情况提供格式化与代码导航", "查看、接受或撤销 Agent 改动" ], "highlightsEn": [ "File tree and full-text search", - "Code navigation, formatting, and diagnostics", + "Language-aware formatting and code navigation when supported", "Review, accept, or revert agent changes" ], "items": [ @@ -765,19 +765,9 @@ "titleEn": "Review file history and snapshots, then roll back a session or selected turn" }, { - "id": "lsp-navigation", - "titleZh": "使用 Hover、跳转定义、查找引用、文档符号与高亮", - "titleEn": "Use hover, go to definition, references, document symbols, and highlights" - }, - { - "id": "lsp-editing", - "titleZh": "使用补全、格式化、重命名、代码操作、内联提示与语义高亮", - "titleEn": "Use completion, formatting, rename, code actions, inlay hints, and semantic highlighting" - }, - { - "id": "lsp-plugins", - "titleZh": "检测项目并安装、启动、停止或卸载 LSP 插件", - "titleEn": "Detect projects and install, start, stop, or uninstall LSP plugins" + "id": "language-actions", + "titleZh": "仅在当前语言提供器实际支持时显示格式化、定义与类型跳转、引用、重命名、快速修复和符号导航", + "titleEn": "Show formatting, definition and type navigation, references, rename, quick fixes, and symbol navigation only when supported by the active language provider" }, { "id": "attachments", @@ -820,19 +810,20 @@ "编辑器", "搜索文件", "差异", - "诊断", - "LSP", + "格式化", + "符号导航", "files", "code", "editor", "file search", "diff", - "diagnostics", + "formatting", + "symbol navigation", "文件树与全文搜索", - "代码导航、格式化和诊断", + "按当前语言实际支持情况提供格式化与代码导航", "查看、接受或撤销 Agent 改动", "File tree and full-text search", - "Code navigation, formatting, and diagnostics", + "Language-aware formatting and code navigation when supported", "Review, accept, or revert agent changes", "浏览文件树并创建、重命名、删除或在系统中显示文件和目录", "Browse the file tree and create, rename, delete, or reveal files and directories", @@ -846,12 +837,8 @@ "Accept or reject agent changes by file, operation, or whole session", "查看文件变更历史和快照,并回滚整个会话或指定轮次", "Review file history and snapshots, then roll back a session or selected turn", - "使用 Hover、跳转定义、查找引用、文档符号与高亮", - "Use hover, go to definition, references, document symbols, and highlights", - "使用补全、格式化、重命名、代码操作、内联提示与语义高亮", - "Use completion, formatting, rename, code actions, inlay hints, and semantic highlighting", - "检测项目并安装、启动、停止或卸载 LSP 插件", - "Detect projects and install, start, stop, or uninstall LSP plugins", + "仅在当前语言提供器实际支持时显示格式化、定义与类型跳转、引用、重命名、快速修复和符号导航", + "Show formatting, definition and type navigation, references, rename, quick fixes, and symbol navigation only when supported by the active language provider", "粘贴、拖放或上传文件与图片作为会话上下文", "Paste, drop, or upload files and images as session context" ],