feat(lsp): add oh-my-opencode.json config support with priority

Config file priority (high to low):
1. ./.opencode/oh-my-opencode.json (project)
2. ~/.config/opencode/oh-my-opencode.json (user)
3. ~/.config/opencode/opencode.json (opencode)
4. builtin servers

LSP servers sorted by source then priority field (higher = preferred)
This commit is contained in:
YeonGyu-Kim
2025-12-04 21:57:18 +09:00
parent 7090fca0fd
commit 81869ccaac

View File

@@ -7,97 +7,131 @@ export interface ResolvedServer {
id: string id: string
command: string[] command: string[]
extensions: string[] extensions: string[]
priority: number
env?: Record<string, string> env?: Record<string, string>
initialization?: Record<string, unknown> initialization?: Record<string, unknown>
} }
interface OpencodeJsonLspEntry { interface LspEntry {
disabled?: boolean disabled?: boolean
command?: string[] command?: string[]
extensions?: string[] extensions?: string[]
priority?: number
env?: Record<string, string> env?: Record<string, string>
initialization?: Record<string, unknown> initialization?: Record<string, unknown>
} }
interface OpencodeJson { interface ConfigJson {
lsp?: Record<string, OpencodeJsonLspEntry> lsp?: Record<string, LspEntry>
} }
let cachedOpencodeConfig: OpencodeJson | null = null type ConfigSource = "project" | "user" | "opencode"
function loadOpencodeJson(): OpencodeJson { interface ServerWithSource extends ResolvedServer {
if (cachedOpencodeConfig) return cachedOpencodeConfig source: ConfigSource
}
const configPath = join(homedir(), ".config", "opencode", "opencode.json") function loadJsonFile<T>(path: string): T | null {
if (existsSync(configPath)) { if (!existsSync(path)) return null
try { try {
const content = readFileSync(configPath, "utf-8") return JSON.parse(readFileSync(path, "utf-8")) as T
cachedOpencodeConfig = JSON.parse(content) as OpencodeJson } catch {
return cachedOpencodeConfig return null
} catch {
}
} }
cachedOpencodeConfig = {}
return cachedOpencodeConfig
} }
function getDisabledServers(): Set<string> { function getConfigPaths(): { project: string; user: string; opencode: string } {
const config = loadOpencodeJson() const cwd = process.cwd()
const disabled = new Set<string>() 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
if (config.lsp) {
for (const [id, entry] of Object.entries(config.lsp)) { for (const [id, entry] of Object.entries(config.lsp)) {
if (entry.disabled) { if (entry.disabled) {
disabled.add(id) disabled.add(id)
continue
} }
}
}
return disabled if (seen.has(id)) continue
}
function getUserLspServers(): Map<string, ResolvedServer> {
const config = loadOpencodeJson()
const servers = new Map<string, ResolvedServer>()
if (config.lsp) {
for (const [id, entry] of Object.entries(config.lsp)) {
if (entry.disabled) continue
if (!entry.command || !entry.extensions) continue if (!entry.command || !entry.extensions) continue
servers.set(id, { servers.push({
id, id,
command: entry.command, command: entry.command,
extensions: entry.extensions, extensions: entry.extensions,
priority: entry.priority ?? 0,
env: entry.env, env: entry.env,
initialization: entry.initialization, initialization: entry.initialization,
source,
}) })
} seen.add(id)
}
return servers
}
export function findServerForExtension(ext: string): ResolvedServer | null {
const userServers = getUserLspServers()
const disabledServers = getDisabledServers()
for (const server of userServers.values()) {
if (server.extensions.includes(ext) && isServerInstalled(server.command)) {
return server
} }
} }
for (const [id, config] of Object.entries(BUILTIN_SERVERS)) { for (const [id, config] of Object.entries(BUILTIN_SERVERS)) {
if (disabledServers.has(id)) continue if (disabled.has(id) || seen.has(id)) continue
if (userServers.has(id)) continue
if (config.extensions.includes(ext) && isServerInstalled(config.command)) { 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 { return {
id, id: server.id,
command: config.command, command: server.command,
extensions: config.extensions, extensions: server.extensions,
priority: server.priority,
env: server.env,
initialization: server.initialization,
} }
} }
} }
@@ -125,23 +159,50 @@ export function isServerInstalled(command: string[]): boolean {
return false return false
} }
export function getAllServers(): Array<{ id: string; installed: boolean; extensions: string[]; disabled: boolean }> { export function getAllServers(): Array<{
const result: Array<{ id: string; installed: boolean; extensions: string[]; disabled: boolean }> = [] id: string
const userServers = getUserLspServers() installed: boolean
const disabledServers = getDisabledServers() 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>() const seen = new Set<string>()
for (const server of userServers.values()) { for (const server of servers) {
if (seen.has(server.id)) continue
result.push({ result.push({
id: server.id, id: server.id,
installed: isServerInstalled(server.command), installed: isServerInstalled(server.command),
extensions: server.extensions, extensions: server.extensions,
disabled: false, disabled: false,
source: server.source,
priority: server.priority,
}) })
seen.add(server.id) seen.add(server.id)
} }
for (const id of disabledServers) { for (const id of disabled) {
if (seen.has(id)) continue if (seen.has(id)) continue
const builtin = BUILTIN_SERVERS[id] const builtin = BUILTIN_SERVERS[id]
result.push({ result.push({
@@ -149,20 +210,14 @@ export function getAllServers(): Array<{ id: string; installed: boolean; extensi
installed: builtin ? isServerInstalled(builtin.command) : false, installed: builtin ? isServerInstalled(builtin.command) : false,
extensions: builtin?.extensions || [], extensions: builtin?.extensions || [],
disabled: true, disabled: true,
}) source: "disabled",
seen.add(id) priority: 0,
}
for (const [id, config] of Object.entries(BUILTIN_SERVERS)) {
if (seen.has(id)) continue
result.push({
id,
installed: isServerInstalled(config.command),
extensions: config.extensions,
disabled: false,
}) })
} }
return result return result
} }
export function getConfigPaths_(): { project: string; user: string; opencode: string } {
return getConfigPaths()
}