Files
oh-my-opencode-free-fork/src/tools/lsp/config.ts
YeonGyu-Kim 9e490d311f feat(lsp): sync with OpenCode LSP implementation
- Add 50+ extension mappings to EXT_TO_LANG (Clojure, Erlang, F#, Haskell, Scala, OCaml, etc.)
- Add missing BUILTIN_SERVERS: Biome, Oxlint, ty (Python), FSharp, Terraform-ls
- Improve isServerInstalled() to check node_modules/.bin, ~/.config/opencode/bin paths
- Add Windows .exe extension support for command detection
- Fix GitHub issue #118 - LSP servers not being detected

🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode)
2025-12-20 13:58:07 +09:00

244 lines
6.0 KiB
TypeScript

import { existsSync, readFileSync } from "fs"
import { join } from "path"
import { homedir } from "os"
import { BUILTIN_SERVERS, EXT_TO_LANG } from "./constants"
export interface ResolvedServer {
id: string
command: string[]
extensions: string[]
priority: number
env?: Record<string, string>
initialization?: Record<string, unknown>
}
interface LspEntry {
disabled?: boolean
command?: string[]
extensions?: string[]
priority?: number
env?: Record<string, string>
initialization?: Record<string, unknown>
}
interface ConfigJson {
lsp?: Record<string, LspEntry>
}
type ConfigSource = "project" | "user" | "opencode"
interface ServerWithSource extends ResolvedServer {
source: ConfigSource
}
function loadJsonFile<T>(path: string): T | null {
if (!existsSync(path)) return null
try {
return JSON.parse(readFileSync(path, "utf-8")) as T
} catch {
return null
}
}
function getConfigPaths(): { project: string; user: string; opencode: string } {
const cwd = process.cwd()
return {
project: join(cwd, ".opencode", "oh-my-opencode.json"),
user: join(homedir(), ".config", "opencode", "oh-my-opencode.json"),
opencode: join(homedir(), ".config", "opencode", "opencode.json"),
}
}
function loadAllConfigs(): Map<ConfigSource, ConfigJson> {
const paths = getConfigPaths()
const configs = new Map<ConfigSource, ConfigJson>()
const project = loadJsonFile<ConfigJson>(paths.project)
if (project) configs.set("project", project)
const user = loadJsonFile<ConfigJson>(paths.user)
if (user) configs.set("user", user)
const opencode = loadJsonFile<ConfigJson>(paths.opencode)
if (opencode) configs.set("opencode", opencode)
return configs
}
function getMergedServers(): ServerWithSource[] {
const configs = loadAllConfigs()
const servers: ServerWithSource[] = []
const disabled = new Set<string>()
const seen = new Set<string>()
const sources: ConfigSource[] = ["project", "user", "opencode"]
for (const source of sources) {
const config = configs.get(source)
if (!config?.lsp) continue
for (const [id, entry] of Object.entries(config.lsp)) {
if (entry.disabled) {
disabled.add(id)
continue
}
if (seen.has(id)) continue
if (!entry.command || !entry.extensions) continue
servers.push({
id,
command: entry.command,
extensions: entry.extensions,
priority: entry.priority ?? 0,
env: entry.env,
initialization: entry.initialization,
source,
})
seen.add(id)
}
}
for (const [id, config] of Object.entries(BUILTIN_SERVERS)) {
if (disabled.has(id) || seen.has(id)) continue
servers.push({
id,
command: config.command,
extensions: config.extensions,
priority: -100,
source: "opencode",
})
}
return servers.sort((a, b) => {
if (a.source !== b.source) {
const order: Record<ConfigSource, number> = { project: 0, user: 1, opencode: 2 }
return order[a.source] - order[b.source]
}
return b.priority - a.priority
})
}
export function findServerForExtension(ext: string): ResolvedServer | null {
const servers = getMergedServers()
for (const server of servers) {
if (server.extensions.includes(ext) && isServerInstalled(server.command)) {
return {
id: server.id,
command: server.command,
extensions: server.extensions,
priority: server.priority,
env: server.env,
initialization: server.initialization,
}
}
}
return null
}
export function getLanguageId(ext: string): string {
return EXT_TO_LANG[ext] || "plaintext"
}
export function isServerInstalled(command: string[]): boolean {
if (command.length === 0) return false
const cmd = command[0]
const isWindows = process.platform === "win32"
const ext = isWindows ? ".exe" : ""
const pathEnv = process.env.PATH || ""
const pathSeparator = isWindows ? ";" : ":"
const paths = pathEnv.split(pathSeparator)
for (const p of paths) {
if (existsSync(join(p, cmd)) || existsSync(join(p, cmd + ext))) {
return true
}
}
const cwd = process.cwd()
const additionalPaths = [
join(cwd, "node_modules", ".bin", cmd),
join(cwd, "node_modules", ".bin", cmd + ext),
join(homedir(), ".config", "opencode", "bin", cmd),
join(homedir(), ".config", "opencode", "bin", cmd + ext),
join(homedir(), ".config", "opencode", "node_modules", ".bin", cmd),
join(homedir(), ".config", "opencode", "node_modules", ".bin", cmd + ext),
]
for (const p of additionalPaths) {
if (existsSync(p)) {
return true
}
}
return false
}
export function getAllServers(): Array<{
id: string
installed: boolean
extensions: string[]
disabled: boolean
source: string
priority: number
}> {
const configs = loadAllConfigs()
const servers = getMergedServers()
const disabled = new Set<string>()
for (const config of configs.values()) {
if (!config.lsp) continue
for (const [id, entry] of Object.entries(config.lsp)) {
if (entry.disabled) disabled.add(id)
}
}
const result: Array<{
id: string
installed: boolean
extensions: string[]
disabled: boolean
source: string
priority: number
}> = []
const seen = new Set<string>()
for (const server of servers) {
if (seen.has(server.id)) continue
result.push({
id: server.id,
installed: isServerInstalled(server.command),
extensions: server.extensions,
disabled: false,
source: server.source,
priority: server.priority,
})
seen.add(server.id)
}
for (const id of disabled) {
if (seen.has(id)) continue
const builtin = BUILTIN_SERVERS[id]
result.push({
id,
installed: builtin ? isServerInstalled(builtin.command) : false,
extensions: builtin?.extensions || [],
disabled: true,
source: "disabled",
priority: 0,
})
}
return result
}
export function getConfigPaths_(): { project: string; user: string; opencode: string } {
return getConfigPaths()
}