Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
11 kB · 374 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375/** * LSP Plugin Recommendation Utility * * Scans installed marketplaces for LSP plugins and recommends plugins * based on file extensions, but ONLY when the LSP binary is already * installed on the system. * * Limitation: Can only detect LSP plugins that declare their servers * inline in the marketplace entry. Plugins with separate .lsp.json files * are not detectable until after installation. */
import { extname } from 'path'import { isBinaryInstalled } from '../binaryCheck.js'import { getGlobalConfig, saveGlobalConfig } from '../config.js'import { logForDebugging } from '../debug.js'import { isPluginInstalled } from './installedPluginsManager.js'import { getMarketplace, loadKnownMarketplacesConfig,} from './marketplaceManager.js'import { ALLOWED_OFFICIAL_MARKETPLACE_NAMES, type PluginMarketplaceEntry,} from './schemas.js'
/** * LSP plugin recommendation returned to the caller */export type LspPluginRecommendation = { pluginId: string // "plugin-name@marketplace-name" pluginName: string // Human-readable plugin name marketplaceName: string // Marketplace name description?: string // Plugin description isOfficial: boolean // From official marketplace? extensions: string[] // File extensions this plugin supports command: string // LSP server command (e.g., "typescript-language-server")}
// Maximum number of times user can ignore recommendations before we stop showingconst MAX_IGNORED_COUNT = 5
/** * Check if a marketplace is official (from Anthropic) */function isOfficialMarketplace(name: string): boolean { return ALLOWED_OFFICIAL_MARKETPLACE_NAMES.has(name.toLowerCase())}
/** * Internal type for LSP info extracted from plugin manifest */type LspInfo = { extensions: Set<string> command: string}
/** * Extract LSP info (extensions and command) from inline lspServers config. * * NOTE: Can only read inline configs, not external .lsp.json files. * String paths are skipped as they reference files only available after installation. * * @param lspServers - The lspServers field from PluginMarketplaceEntry * @returns LSP info with extensions and command, or null if not extractable */function extractLspInfoFromManifest( lspServers: PluginMarketplaceEntry['lspServers'],): LspInfo | null { if (!lspServers) { return null }
// If it's a string path (e.g., "./.lsp.json"), we can't read it from marketplace if (typeof lspServers === 'string') { logForDebugging( '[lspRecommendation] Skipping string path lspServers (not readable from marketplace)', ) return null }
// If it's an array, process each element if (Array.isArray(lspServers)) { for (const item of lspServers) { // Skip string paths in arrays if (typeof item === 'string') { continue } // Try to extract from inline config object const info = extractFromServerConfigRecord(item) if (info) { return info } } return null }
// It's an inline config object: Record<string, LspServerConfig> return extractFromServerConfigRecord(lspServers)}
/** * Extract LSP info from a server config record (inline object format) *//** * Type guard to check if a value is a record object */function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null}
function extractFromServerConfigRecord( serverConfigs: Record<string, unknown>,): LspInfo | null { const extensions = new Set<string>() let command: string | null = null
for (const [_serverName, config] of Object.entries(serverConfigs)) { if (!isRecord(config)) { continue }
// Get command from first valid server config if (!command && typeof config.command === 'string') { command = config.command }
// Collect all extensions from extensionToLanguage mapping const extMapping = config.extensionToLanguage if (isRecord(extMapping)) { for (const ext of Object.keys(extMapping)) { extensions.add(ext.toLowerCase()) } } }
if (!command || extensions.size === 0) { return null }
return { extensions, command }}
/** * Internal type for plugin with LSP info */type LspPluginInfo = { entry: PluginMarketplaceEntry marketplaceName: string extensions: Set<string> command: string isOfficial: boolean}
/** * Get all LSP plugins from all installed marketplaces * * @returns Map of pluginId to plugin info with LSP metadata */async function getLspPluginsFromMarketplaces(): Promise< Map<string, LspPluginInfo>> { const result = new Map<string, LspPluginInfo>()
try { const config = await loadKnownMarketplacesConfig()
for (const marketplaceName of Object.keys(config)) { try { const marketplace = await getMarketplace(marketplaceName) const isOfficial = isOfficialMarketplace(marketplaceName)
for (const entry of marketplace.plugins) { // Skip plugins without lspServers if (!entry.lspServers) { continue }
const lspInfo = extractLspInfoFromManifest(entry.lspServers) if (!lspInfo) { continue }
const pluginId = `${entry.name}@${marketplaceName}` result.set(pluginId, { entry, marketplaceName, extensions: lspInfo.extensions, command: lspInfo.command, isOfficial, }) } } catch (error) { logForDebugging( `[lspRecommendation] Failed to load marketplace ${marketplaceName}: ${error}`, ) } } } catch (error) { logForDebugging( `[lspRecommendation] Failed to load marketplaces config: ${error}`, ) }
return result}
/** * Find matching LSP plugins for a file path. * * Returns recommendations for plugins that: * 1. Support the file's extension * 2. Have their LSP binary installed on the system * 3. Are not already installed * 4. Are not in the user's "never suggest" list * * Results are sorted with official marketplace plugins first. * * @param filePath - Path to the file to find LSP plugins for * @returns Array of matching plugin recommendations (empty if none or disabled) */export async function getMatchingLspPlugins( filePath: string,): Promise<LspPluginRecommendation[]> { // Check if globally disabled if (isLspRecommendationsDisabled()) { logForDebugging('[lspRecommendation] Recommendations are disabled') return [] }
// Extract file extension const ext = extname(filePath).toLowerCase() if (!ext) { logForDebugging('[lspRecommendation] No file extension found') return [] }
logForDebugging(`[lspRecommendation] Looking for LSP plugins for ${ext}`)
// Get all LSP plugins from marketplaces const allLspPlugins = await getLspPluginsFromMarketplaces()
// Get config for filtering const config = getGlobalConfig() const neverPlugins = config.lspRecommendationNeverPlugins ?? []
// Filter to matching plugins const matchingPlugins: Array<{ info: LspPluginInfo; pluginId: string }> = []
for (const [pluginId, info] of allLspPlugins) { // Check extension match if (!info.extensions.has(ext)) { continue }
// Filter: not in "never" list if (neverPlugins.includes(pluginId)) { logForDebugging( `[lspRecommendation] Skipping ${pluginId} (in never suggest list)`, ) continue }
// Filter: not already installed if (isPluginInstalled(pluginId)) { logForDebugging( `[lspRecommendation] Skipping ${pluginId} (already installed)`, ) continue }
matchingPlugins.push({ info, pluginId }) }
// Filter: binary must be installed (async check) const pluginsWithBinary: Array<{ info: LspPluginInfo; pluginId: string }> = []
for (const { info, pluginId } of matchingPlugins) { const binaryExists = await isBinaryInstalled(info.command) if (binaryExists) { pluginsWithBinary.push({ info, pluginId }) logForDebugging( `[lspRecommendation] Binary '${info.command}' found for ${pluginId}`, ) } else { logForDebugging( `[lspRecommendation] Skipping ${pluginId} (binary '${info.command}' not found)`, ) } }
// Sort: official marketplaces first pluginsWithBinary.sort((a, b) => { if (a.info.isOfficial && !b.info.isOfficial) return -1 if (!a.info.isOfficial && b.info.isOfficial) return 1 return 0 })
// Convert to recommendations return pluginsWithBinary.map(({ info, pluginId }) => ({ pluginId, pluginName: info.entry.name, marketplaceName: info.marketplaceName, description: info.entry.description, isOfficial: info.isOfficial, extensions: Array.from(info.extensions), command: info.command, }))}
/** * Add a plugin to the "never suggest" list * * @param pluginId - Plugin ID to never suggest again */export function addToNeverSuggest(pluginId: string): void { saveGlobalConfig(currentConfig => { const current = currentConfig.lspRecommendationNeverPlugins ?? [] if (current.includes(pluginId)) { return currentConfig } return { ...currentConfig, lspRecommendationNeverPlugins: [...current, pluginId], } }) logForDebugging(`[lspRecommendation] Added ${pluginId} to never suggest`)}
/** * Increment the ignored recommendation count. * After MAX_IGNORED_COUNT ignores, recommendations are disabled. */export function incrementIgnoredCount(): void { saveGlobalConfig(currentConfig => { const newCount = (currentConfig.lspRecommendationIgnoredCount ?? 0) + 1 return { ...currentConfig, lspRecommendationIgnoredCount: newCount, } }) logForDebugging('[lspRecommendation] Incremented ignored count')}
/** * Check if LSP recommendations are disabled. * Disabled when: * - User explicitly disabled via config * - User has ignored MAX_IGNORED_COUNT recommendations */export function isLspRecommendationsDisabled(): boolean { const config = getGlobalConfig() return ( config.lspRecommendationDisabled === true || (config.lspRecommendationIgnoredCount ?? 0) >= MAX_IGNORED_COUNT )}
/** * Reset the ignored count (useful if user re-enables recommendations) */export function resetIgnoredCount(): void { saveGlobalConfig(currentConfig => { const currentCount = currentConfig.lspRecommendationIgnoredCount ?? 0 if (currentCount === 0) { return currentConfig } return { ...currentConfig, lspRecommendationIgnoredCount: 0, } }) logForDebugging('[lspRecommendation] Reset ignored count')}