LSP ソースコード解析
モジュール概要
LSP(Language Server Protocol)モジュールは、OpenCodeにIDEレベルのコード理解機能を提供します。標準化されたLSPプロトコルを通じて、OpenCodeは正確な診断情報(コンパイルエラー、型エラー、lint警告)、定義へのジャンプ、参照の検索、ホバー情報、シンボル階層を取得できます。これにより、AIアシスタントは正確なコードセマンティクスに基づいて意思決定できます。
このモジュールはpackages/opencode/src/lsp/にあり、合計5ファイルで約2900行のコードで構成されています。コアエントリーポイントのLSP名前空間はEffect Serviceアーキテクチャを採用しており、Instance.state()を通じてグローバルシングルトン状態を管理しています。このモジュールには30以上のLanguage Server(TypeScript、Gopls、Rust Analyzer、Pyright、Clangd、Lua LSなど)の組み込み定義が含まれており、それぞれ独自のプロジェクトルートディレクトリ検出ロジックとspawn実装を持っています。
コア設計の選択:
- Effect Serviceパターン:
LSP名前空間はServiceMap.Service経由でグローバルサービスとして登録され、Instance.state()がライフサイクルを管理します - インテリジェントなクライアント再利用:
getClients(file)は(root, serverID)で重複排除し、プロジェクトごとに1つのサーバーインスタンスのみを起動します - デバウンスされた診断収集:
waitForDiagnostics()は150msのデバウンス+3秒のタイムアウトを使用して、エージェントのブロックを避けます - 自動ダウンロードと分離:Language Serverバイナリは
Global.Path.binに自動ダウンロードされ、Flag.OPENCODE_DISABLE_LSP_DOWNLOADで制御されます
主要ファイル
| ファイルパス | 行数 | 責任範囲 |
|---|---|---|
src/lsp/index.ts | 約559 | モジュールメインエントリ:Effect Service定義、InstanceState状態管理、getClientsインテリジェントマッチング、すべての公開API |
src/lsp/client.ts | 約253 | LSPクライアント実装:vscode-jsonrpc接続管理、診断収集、ファイルバージョン管理、接続シャットダウン |
src/lsp/server.ts | 約1968 | 30以上のLanguage Server定義:LSPServer.Infoインターフェース、NearestRoot高階関数、spawn実装と自動ダウンロード |
src/lsp/launch.ts | 約22 | Spawnヘルパー関数:Process.spawnをラップし、サーバー起動コード簡略化 |
src/lsp/language.ts | 約120 | LANGUAGE_EXTENSIONSマッピングテーブル:ファイル拡張子(例:.tsx)→ LSP languageId(例:typescriptreact) |
型システム
RangeとDiagnostic — 位置と診断
// Text range (line number + character offset)
export const Range = z.object({
start: z.object({ line: z.number(), character: z.number() }),
end: z.object({ line: z.number(), character: z.number() }),
})
// Symbol information (for documentSymbol / workspaceSymbol)
export const Symbol = z.object({
name: z.string(),
kind: z.number(), // LSP SymbolKind enum value
location: z.object({
uri: z.string(),
range: Range,
}),
})
// Server connection status
export const Status = z.object({
id: z.string(), // Unique server identifier
name: z.string(), // Human-readable name
root: z.string(), // Project root directory
status: z.union([
z.literal("connected"),
z.literal("error"),
]),
})
InstanceState — グローバル状態
// LSP module global state structure
{
clients: LSPClient.Info[], // All active client connections
servers: Record<string, LSPServer.Info>, // Registered server definitions (key = serverID)
broken: Set<string>, // Known failed server IDs, to avoid retrying
spawning: Map<string, Promise<LSPClient.Info | undefined>>, // Currently launching clients (deduplication)
}
主要フィールドの説明:
clients:すべてのアクティブなLSPクライアント接続。各クライアントは(root, serverID)の組み合わせにバインドされていますservers:組み込み定義とユーザー設定からマージされたサーバーレジストリ。keyはサーバーIDです(例:"typescript"、"gopls")broken:起動に失敗したサーバーIDのセット。このセットに追加されると、以後のgetClients()呼び出しは再試行せずにスキップしますspawning:インフライトのクライアント起動Promise。同じ(root, serverID)への同時リクエストは同じPromiseを共有し、重複起動を防ぎます
LSPServer.Info — サーバー定義
// Definition interface for each Language Server in server.ts
interface Info {
id: string // Unique identifier, e.g. "typescript", "gopls", "rust-analyzer"
extensions: string[] // Associated file extensions, e.g. [".ts", ".tsx"]
root: RootFunction // Function to determine project root directory
spawn(root: string): Promise<Handle | undefined> // Launch server process
}
LSPClient.Info — クライアントインスタンス
// Core client structure in client.ts
{
serverID: string // Corresponding server ID
root: string // Project root directory
connection: MessageConnection // vscode-jsonrpc message connection
diagnostics: Map<string, Diagnostic[]> // File path → diagnostic list
files: Record<string, number> // File path → version number (incrementing counter)
process: ChildProcess // Server child process
}
コアフロー
getClients — インテリジェントファイル-クライアントマッチング
getClients(file)はLSPモジュールで最も重要な関数であり、指定されたファイルに対する適切なLSPクライアントを見つけ出すか作成する責任を持ちます。完全なロジックは8ステップで構成されています:
// index.ts — simplified core logic
async function getClients(file: string): Promise<LSPClient.Info[]> {
const state = await instanceState()
const results: LSPClient.Info[] = []
// Step 1: Check if the file is within the Instance directory
// Files outside the project directory are not processed
if (!file.startsWith(instance.directory)) return results
// Step 2: Extract file extension
const ext = path.extname(file) // e.g. ".ts", ".go"
// Step 3: Iterate all registered servers, check extension match
for (const [serverID, server] of Object.entries(state.servers)) {
if (!server.extensions.includes(ext)) continue
// Step 4: Call server.root(file) to determine project root directory
const root = server.root(file)
if (!root) continue // File does not belong to any project
// Step 5: Check broken set, skip known failed servers
if (state.broken.has(serverID)) continue
// Step 6: Look for existing client (root + serverID match)
const existing = state.clients.find(
c => c.root === root && c.serverID === serverID
)
if (existing) {
results.push(existing)
continue
}
// Step 7: Check spawning Map for concurrent requests
const spawnKey = `${root}:${serverID}`
const spawning = state.spawning.get(spawnKey)
if (spawning) {
const client = await spawning
if (client) results.push(client)
continue
}
// Step 8: Create new client and store in cache
const promise = LSPClient.create({ serverID, server: server.handle, root })
.catch(() => {
state.broken.add(serverID) // Launch failed → add to broken set
return undefined
})
state.spawning.set(spawnKey, promise)
const client = await promise
state.spawning.delete(spawnKey)
if (client) {
state.clients.push(client)
results.push(client)
}
}
return results
}
このロジックは完全なクライアント再利用と重複排除を実装しています:
- 同じプロジェクトの同じタイプのLanguage Serverが2つ起動されることはありません
- 同時の
getClients呼び出しは同じspawn Promiseを共有します - 失敗したサーバーは
brokenセットに記録され、再試行されません
LSPClient.create — JSON-RPC接続確立
LSPClient.create()はクライアント作成のコア関数であり、Language Serverとの通信チャネルを確立します:
// client.ts — core flow (simplified)
async function create(input: {
serverID: string
server: LSPServer.Handle
root: string
}): Promise<Info> {
// 1. Launch server child process
const handle = await input.server.spawn(input.root)
if (!handle) throw new Error("spawn failed")
// 2. Establish JSON-RPC message connection
const connection = createMessageConnection(
new StreamMessageReader(handle.process.stdout),
new StreamMessageWriter(handle.process.stdin),
)
// 3. Start message listener
connection.listen()
// 4. Send initialize request (45-second timeout)
const initResult = await withTimeout(
connection.sendRequest("initialize", {
rootUri: pathToUri(input.root),
capabilities: {
textDocument: {
synchronization: { didOpen: true, didChange: true, didClose: true },
publishDiagnostics: { relatedInformation: true },
},
workspace: { configuration: true },
},
}),
45_000,
)
// 5. Send initialized notification
connection.sendNotification("initialized", {})
// 6. Create client instance
return {
serverID: input.serverID,
root: input.root,
connection,
process: handle.process,
diagnostics: new Map(),
files: {},
}
}
接続が確立された後、クライアントはtextDocument/publishDiagnostics通知をリッスンし、診断マップを自動的に更新します。
診断収集フロー
診断収集はLSPモジュールの最も重要な出力であり、エージェントがコードを修正する能力に直接影響します。完全なフロー:
1. Agent edits file
└─ LSP.touchFile(filePath, content, true)
2. getClients(file) matches appropriate clients
└─ by extension → server → root → reuse/create client
3. Client sends file change notification
├─ First open? → textDocument/didOpen
└─ Already open? → textDocument/didChange (with version number)
4. Language Server analyzes code and pushes diagnostics
└─ textDocument/publishDiagnostics notification
5. Client updates diagnostics Map
└─ TypeScript server special: first diagnostic does not publish event (!exists check)
6. Broadcast to upper layers via Bus
└─ Bus.publish(Event.Diagnostics, { ... })
waitForDiagnosticsデバウンスメカニズム:
// client.ts — diagnostic waiting (simplified)
async function waitForDiagnostics(
client: LSPClient.Info,
uri: string,
): Promise<Diagnostic[]> {
const DIAGNOSTICS_DEBOUNCE_MS = 150 // Debounce interval
const DIAGNOSTICS_TIMEOUT_MS = 3000 // Total timeout
return new Promise((resolve) => {
let timer: ReturnType<typeof setTimeout>
// Subscribe to diagnostic events
const unsub = Bus.subscribe(Event.Diagnostics, (event) => {
// Only care about the current file
if (event.uri !== uri) return
// Reset timer after receiving diagnostics (debounce)
clearTimeout(timer)
timer = setTimeout(() => {
unsub()
resolve(client.diagnostics.get(uri) ?? [])
}, DIAGNOSTICS_DEBOUNCE_MS)
})
// Total timeout protection: force return after 3 seconds
setTimeout(() => {
unsub()
clearTimeout(timer)
resolve(client.diagnostics.get(uri) ?? [])
}, DIAGNOSTICS_TIMEOUT_MS)
})
}
150msのデバウンスの理由:Language Serverは通常、構文診断を最初に送信し、その後意味診断を送信します。デバウンスにより、エージェントは中間状態ではなく完全な診断結果を受け取れます。3秒の合計タイムアウトにより、エージェントが無限に待機するのを防ぎます。
サーバー定義とNearestRoot
server.tsはこのモジュールで最も大きなファイル(約1968行)であり、30以上のLanguage Serverを定義しています。各サーバー定義の中核は2つの関数で構成されています:root()とspawn()。
NearestRoot高階関数:
// server.ts — project root directory detection (simplified)
function NearestRoot(
files: string[], // Target file name list, e.g. ["package.json", "tsconfig.json"]
exclude?: string[], // Exclusion patterns, e.g. ["deno.json"]
): (file: string) => string | undefined {
return (file: string) => {
// Search upward from the file's directory until Instance.directory
let dir = path.dirname(file)
while (dir.startsWith(instance.directory)) {
// Check if target files exist
for (const name of files) {
const target = path.join(dir, name)
if (fs.existsSync(target)) {
// Check exclusion patterns
if (exclude?.some(ex => fs.existsSync(path.join(dir, ex)))) {
continue // Matched exclusion rule, skip this directory
}
return dir
}
}
dir = path.dirname(dir)
}
return undefined
}
}
この高階関数はモノレポシナリオをサポートしています:異なるサブディレクトリ内のファイルは異なるプロジェクトルートにマッチでき、したがって独立したLanguage Serverインスタンスを起動します。除外パターンは(Denoプロジェクトで誤ったTSサーバーが起動するのを避けるために、TypeScriptサーバーがdeno.jsonを除外するなどの)特殊ケースを処理します。
組み込みサーバー概要(30以上のタイプ):
| カテゴリ | サーバー | ルート検出戦略 |
|---|---|---|
| フロントエンド | TypeScript、Deno、Vue、Astro、Svelte | NearestRoot(["package.json", "tsconfig.json"], ["deno.json"]) |
| システム言語 | Gopls、Rust Analyzer、Clangd、SourceKit (Swift) | NearestRoot(["go.mod"]) / Cargo.toml / compile_commands.json / Package.swift |
| スクリプト言語 | Pyright、JDTLS (Java)、KotlinLS、Rubocop (Ruby)、JuliaLS | NearestRoot(["pyproject.toml", "setup.py"]) / pom.xml / build.gradleなど |
| 関数型言語 | LuaLS、Zls (Zig)、Gleam、Ocaml、HLS (Haskell) | NearestRoot([".luarc.json"]) / build.zig / gleam.toml / dune / hie.yaml |
| インフラストラクチャ | TerraformLS、DockerfileLS、YamlLS、Nixd | NearestRoot(["*.tf"]) / Dockerfile / *.yml / flake.nix |
| データ/クエリ | Prisma、TexLab (LaTeX)、SQLS | NearestRoot(["*.prisma"]) / *.tex / *.sql |
| その他 | Biome、ESLint、Dart、Clojure、PHPIntelephense、CSharp、FSharp | それぞれの設定ファイル検出 |
| タイプセット | Tinymist (Typst) | NearestRoot(["*.typ"]) |
自動ダウンロードロジック
一部のLanguage Server(例:Biome、Deno、Ty)は自動ダウンロードをサポートしています。フローは以下の通りです:
// server.ts — auto-download (simplified)
async function spawnWithDownload(input: {
command: string
args: string[]
url: string // Download URL template
}): Promise<Handle | undefined> {
// Check flag
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) {
// Only try local lookup, no download
return Process.spawn(input.command, input.args)
}
// Try local launch
const local = await Process.spawn(input.command, input.args)
if (local) return local
// Not found locally → auto-download to Global.Path.bin
const binPath = path.join(Global.Path.bin, input.command)
if (!fs.existsSync(binPath)) {
await download(input.url, binPath)
}
return Process.spawn(binPath, input.args)
}
コードナビゲーションAPI
LSPモジュールは完全なコードナビゲーション機能を提供します。すべてのAPIはrun()ヘルパー関数を介して一致するクライアントにルーティングします:
// index.ts — run helper function (simplified)
async function run<T>(
file: string,
fn: (client: LSPClient.Info, uri: string) => Promise<T>,
): Promise<T[]> {
const clients = await getClients(file)
const uri = pathToUri(file)
return Promise.all(clients.map(c => fn(c, uri)))
}
| API | LSPメソッド | 説明 |
|---|---|---|
definition(file, position) | textDocument/definition | 参照から定義へジャンプ、Location[]を返します |
references(file, position) | textDocument/references | シンボルのすべての参照位置を検索 |
hover(file, position) | textDocument/hover | シンボルの型情報とドキュメントを取得 |
workspaceSymbol(query) | workspace/symbol | グローバルシンボル検索、kind 5/6/11/12などをフィルター、最大10件の結果 |
documentSymbol(uri) | textDocument/documentSymbol | ドキュメントシンボルリスト、DocumentSymbolまたはSymbolを返します |
incomingCalls(file, position) | 2ステップリクエスト | prepareCallHierarchy → callHierarchy/incomingCalls |
outgoingCalls(file, position) | 2ステップリクエスト | prepareCallHierarchy → callHierarchy/outgoingCalls |
workspaceSymbolフィルタリングロジック:
// index.ts — workspaceSymbol (simplified)
async function workspaceSymbol(query: string): Promise<Symbol[]> {
const allClients = await allClients()
const results: Symbol[] = []
for (const client of allClients) {
const symbols = await client.connection.sendRequest(
"workspace/symbol",
{ query },
)
for (const sym of symbols) {
// Filter to key types: Class(5), Function(6/12), Method(6), Enum(10)
if ([5, 6, 11, 12].includes(sym.kind)) {
results.push(sym)
}
}
}
// Return at most 10 results to avoid context bloat
return results.slice(0, 10)
}
呼び出しチェーンの例
チェーン1:エージェントがファイルを編集 → 診断収集 → エージェントが修正
1. Agent executes EditTool to modify file
└─ SessionProcessor processes tool-result event
2. Agent calls LSP.touchFile(filePath, newContent, true)
├─ getClients(file)
│ ├─ Extract extension: ".ts"
│ ├─ Match server: Typescript (extensions includes ".ts")
│ ├─ root(file) → NearestRoot(["package.json"]) → "/project/root"
│ ├─ Find existing client: root="/project/root", serverID="typescript" → found
│ └─ Return [existingClient]
└─ client.notify.didChange({ uri, version: 3, content: newContent })
3. Language Server analyzes the modified file
└─ Pushes textDocument/publishDiagnostics notification
4. Client updates diagnostics Map
└─ Bus.publish(Event.Diagnostics, { uri, diagnostics: [...] })
5. Agent calls LSP.waitForDiagnostics(file)
├─ Subscribe to Diagnostics event
├─ Received notification → start 150ms debounce timer
├─ No new diagnostics within 150ms → resolve
└─ Return [{ message: "Type 'string' is not assignable to 'number'", range: {...} }]
6. Agent decides whether to continue fixing based on diagnostics
└─ In the next iteration of SessionPrompt.loop(), the Agent sees the diagnostic results and decides on a fix strategy
チェーン2:最初のファイルオープン → サーバー起動 → 診断準備完了
1. Agent reads "main.go" for the first time
└─ LSP.touchFile("main.go", content, true)
2. getClients("main.go")
├─ Extension: ".go"
├─ Match server: Gopls (extensions includes ".go")
├─ root("main.go") → NearestRoot(["go.mod"]) → "/project/root"
├─ Find existing client: no match (first open)
├─ Check spawning Map: none (first time)
└─ Create new client:
├─ LSPClient.create({ serverID: "gopls", root: "/project/root" })
│ ├─ server.spawn(root) → Process.spawn("gopls", ["serve"])
│ ├─ createMessageConnection(stdout, stdin)
│ ├─ connection.sendRequest("initialize", { rootUri, capabilities })
│ ├─ connection.sendNotification("initialized", {})
│ └─ Return new client
├─ state.clients.push(newClient)
└─ Return [newClient]
3. New client sends textDocument/didOpen
└─ connection.sendNotification("textDocument/didOpen", { textDocument: { uri, languageId: "go", version: 1, text: content } })
4. Gopls analyzes and pushes diagnostics
└─ publishDiagnostics → diagnostics Map updated → Bus broadcast
5. waitForDiagnostics("main.go")
├─ 150ms debounce wait
└─ Return diagnostic results
チェーン3:モノレポ内の複数プロジェクトが共存
Project structure:
/project
├── frontend/
│ ├── package.json
│ └── app.tsx ← TypeScript file
├── backend/
│ ├── go.mod
│ └── main.go ← Go file
└── shared/
└── utils.ts ← TypeScript file
1. Agent opens "frontend/app.tsx"
├─ getClients → Typescript server
├─ root("frontend/app.tsx") → NearestRoot → "/project/frontend"
└─ Launch TS Client A (root="/project/frontend")
2. Agent opens "shared/utils.ts"
├─ getClients → Typescript server
├─ root("shared/utils.ts") → NearestRoot → "/project" (searches upward to project root's package.json)
└─ Launch TS Client B (root="/project") — different root, different instance
3. Agent opens "backend/main.go"
├─ getClients → Gopls server
├─ root("backend/main.go") → NearestRoot → "/project/backend"
└─ Launch Go Client C (root="/project/backend")
Result: 3 independent LSP clients coexisting without interference
チェーン4:サーバー起動失敗 → Brokenマーク → 以後のスキップ
1. Agent opens "broken.rs"
└─ getClients → Rust Analyzer server
2. LSPClient.create() → server.spawn() fails
├─ catch block captures error
├─ state.broken.add("rust-analyzer")
└─ Return undefined
3. Agent opens "other.rs" again
└─ getClients("other.rs")
├─ Match server: Rust Analyzer
├─ Check broken set: "rust-analyzer" ∈ broken
└─ Skip, no retry → return empty list
4. Agent sees no diagnostic results, continues working normally
設計上のトレードオフ
| 決定事項 | 根拠 |
|---|---|
| プレーンモジュールではなくEffect Service | LSPの状態はInstanceライフサイクルに従う必要があります(プロジェクトが開かれたときに初期化され、閉じられたときにクリーンアップ)。Instance.state() + Effect.addFinalizerにより、プロジェクトが閉じられたときにクライアント接続と子プロセスが適切にクリーンアップされます |
| 再試行ではなくBrokenセット | Language Serverの起動失敗は通常、バイナリ欠落やポート競合などの環境問題が原因です。繰り返し再試行しても時間とリソースを浪費するだけで、直接マークしてスキップする方が効率的です |
| spawning Map重複排除 | 同時のgetClients呼び出し(例:エージェントが複数の.tsファイルを同時に編集)は同じspawn Promiseを共有する必要があります。そうでなければ複数の同一Language Serverプロセスが起動されます |
| 150msデバウンス診断 | Language Serverは通常、構文診断(ASTベース)を最初にプッシュし、その後意味診断(型チェックベース)をプッシュします。デバウンスにより、エージェントは完全な診断セットを確実に見ます |
| 3秒の合計タイムアウト | エージェントはLSP応答の遅延により無限に待機することはできません。3秒は「診断は通常十分に完了している」と「エージェントは長く待機できない」のバランスポイントです |
| NearestRoot高階関数 | 異なるサーバーは異なる設定ファイルを検出する必要があります;高階関数はこのパターンを抽象化します。除外パターン(例:TypeScriptがdeno.jsonを除外)はLanguage Server間の競合を処理します |
| 30以上の組み込みサーバー | メインストリームの開発言語の箱出しサポートはOpenCodeのコアアピールポイントです。ユーザーはConfig.lsp経由でカスタムサーバーを追加したり、組み込みサーバーを無効にしたりもできます |
| エラーではなく自動ダウンロード | 欠落しているLanguage Serverバイナリ(例:Biome、Deno)を自動的にダウンロードする方が、エラーで終了するより優れています。Flag.OPENCODE_DISABLE_LSP_DOWNLOADにより、制限された環境でこの動作を無効にできます |
| workspaceSymbolを10件の結果にフィルター | エージェントのコンテキストウィンドウは限られています;多すぎるシンボルは有用な情報を薄めます。Class/Function/Method/Enumなどの重要なタイプにフィルターし、10件の結果に制限することで、情報の密度と完全性のバランスを取ります |
状態管理とライフサイクル
InstanceState初期化
LSP状態はInstance.state()を介して遅延初期化されるシングルトンとして作成されます:
// index.ts — state initialization (simplified)
const instanceState = Instance.state(async () => {
const cfg = await Config.get()
// 1. Collect built-in server definitions
const servers: Record<string, LSPServer.Info> = {}
for (const server of builtInServers) {
servers[server.id] = server
}
// 2. Merge user-defined server configuration
if (cfg.lsp) {
for (const [id, custom] of Object.entries(cfg.lsp)) {
if (custom.disabled) {
delete servers[id] // Disable built-in server
continue
}
// Override built-in server's command, extensions, env, initialization, etc.
if (servers[id]) {
servers[id] = mergeServerConfig(servers[id], custom)
} else {
servers[id] = createCustomServer(id, custom)
}
}
}
// 3. Global disable check
if (cfg.lsp === false) {
return { clients: [], servers: {}, broken: new Set(), spawning: new Map() }
}
return { clients: [], servers, broken: new Set(), spawning: new Map() }
})
Effect.addFinalizerクリーンアップ
// index.ts — lifecycle cleanup (simplified)
Effect.addFinalizer(async () => {
const state = await instanceState()
for (const client of state.clients) {
await LSPClient.shutdown(client)
}
state.clients = []
state.broken.clear()
state.spawning.clear()
})
Instance(プロジェクト)が閉じられると、すべてのLSPクライアント接続が切断され、子プロセスが終了されます。
LSPClient.shutdown — 接続シャットダウン
// client.ts — shutdown flow (simplified)
async function shutdown(client: LSPClient.Info): Promise<void> {
try {
// 1. Send shutdown request (graceful shutdown)
await withTimeout(
client.connection.sendRequest("shutdown"),
5000,
)
// 2. Send exit notification
client.connection.sendNotification("exit")
} catch {
// Timeout or error → force kill process
} finally {
// 3. Close connection
client.connection.dispose()
// 4. Kill child process
client.process.kill()
}
}
他のモジュールとの関係
- Agent:Agentはファイル編集後に
LSP.touchFile()を呼び出し、LSP.diagnostics()を使用して最新の診断を取得して、コード修正が必要かどうかを判断します。hover()とdefinition()はAgentがシンボルセマンティクスを理解するのに役立ちます - Session / SessionPrompt:
SessionPromptはユーザーメッセージを構築する際にLSP.documentSymbol()とLSP.workspaceSymbol()を呼び出して、LLMにコード構造の概要を提供します - Bus:LSPは
Bus.publish(Event.Diagnostics)とBusEvent.define("lsp.client.diagnostics")経由で診断状態の変化を通知します。上位層モジュール(Agent、UI)はこれらのイベントを購読します - Config:
Config.lspは有効化されたサーバー、カスタムコマンド、環境変数を定義します;cfg.lsp === falseでLSPモジュール全体をグローバルに無効にできます - Instance:LSP状態は
Instance.state()を介して管理され、プロジェクトインスタンスライフサイクルに従います。Instance.directoryはNearestRootの検索境界です - Global/Flag:
Global.Path.binは自動ダウンロードされたLanguage Serverバイナリを保存します;Flag.OPENCODE_DISABLE_LSP_DOWNLOADは自動ダウンロードを許可するかを制御します - Process:
launch.tsのspawn()関数はProcess.spawn()をラップし、子プロセス起動のためのエラー処理と環境変数注入を統一します