Download src/utils/plugins/lspRecommendation.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/plugins/lspRecommendation.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/plugins/lspRecommendation.ts
-
curl -L -o lspRecommendation.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/plugins/lspRecommendation.ts
10.7 kB
| /** | |
| * 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 showing | |
| const 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') | |
| } | |