UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

608 lines (607 loc) 24.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getFilteredLocalSymbols = exports.NestedSyntaxNodeWithReferences = void 0; exports.getReferences = getReferences; exports.allUnusedLocalReferences = allUnusedLocalReferences; exports.getImplementation = getImplementation; const vscode_languageserver_1 = require("vscode-languageserver"); const analyze_1 = require("./analyze"); const node_types_1 = require("./utils/node-types"); const tree_sitter_1 = require("./utils/tree-sitter"); const symbol_1 = require("./parsing/symbol"); const options_1 = require("./parsing/options"); const logger_1 = require("./logger"); const argparse_1 = require("./parsing/argparse"); const Locations = __importStar(require("./utils/locations")); const workspace_manager_1 = require("./utils/workspace-manager"); const translation_1 = require("./utils/translation"); const alias_1 = require("./parsing/alias"); const nested_strings_1 = require("./parsing/nested-strings"); const emit_1 = require("./parsing/emit"); function getReferences(document, position, opts = { excludeDefinition: false, localOnly: false, firstMatch: false, allWorkspaces: false, onlyInFiles: [], logPerformance: true, loggingEnabled: false, reporter: undefined, }) { const results = []; const logCallback = logWrapper(document, position, opts); const definitionSymbol = analyze_1.analyzer.getDefinition(document, position); if (!definitionSymbol) { logCallback(`No definition symbol found for position ${JSON.stringify(position)} in document ${document.uri}`, 'warning'); return []; } if (!opts.excludeDefinition) results.push(definitionSymbol.toLocation()); if (isSymbolLocalToDocument(definitionSymbol)) opts.localOnly = true; const documentsToSearch = getDocumentsToSearch(document, logCallback, opts); if (definitionSymbol.isArgparse() || definitionSymbol.isFunction()) { results.push(...(0, argparse_1.getGlobalArgparseLocations)(definitionSymbol.document, definitionSymbol)); } if (definitionSymbol.isFunction() && definitionSymbol.hasEventHook() && definitionSymbol.document.isAutoloaded()) { results.push(...analyze_1.analyzer.findSymbols((d, _) => { if (d.isEmittedEvent() && d.name === definitionSymbol.name) { return true; } return false; }).map(d => d.toLocation())); } const searchableDocumentsUris = new Set(documentsToSearch.map(doc => doc.uri)); const searchableDocuments = new Set(documentsToSearch.filter(doc => searchableDocumentsUris.has(doc.uri))); const matchingNodes = {}; let shouldExitEarly = false; let reporting = false; const reporter = opts.reporter; if (opts.reporter && searchableDocuments.size > 500) { reporter?.begin('[fish-lsp] finding references', 0, 'Finding references...', true); reporting = true; } let index = 0; for (const doc of searchableDocuments) { const prog = Math.ceil((index + 1) / searchableDocuments.size * 100); if (reporting) { reporter?.report(prog); } index += 1; if (!workspace_manager_1.workspaceManager.current?.contains(doc.uri)) { continue; } const filteredSymbols = (0, exports.getFilteredLocalSymbols)(definitionSymbol, doc); const root = analyze_1.analyzer.getRootNode(doc.uri); if (!root) { logCallback(`No root node found for document ${doc.uri}`, 'warning'); continue; } const matchableNodes = getChildNodesOptimized(definitionSymbol, doc); for (const node of matchableNodes) { if (filteredSymbols && filteredSymbols.some(s => s.containsNode(node) || s.scopeNode.equals(node) || s.scopeContainsNode(node))) { continue; } if (definitionSymbol.isReference(doc, node, true)) { const currentDocumentsNodes = matchingNodes[doc.uri] ?? []; currentDocumentsNodes.push(node); matchingNodes[doc.uri] = currentDocumentsNodes; if (opts.firstMatch) { shouldExitEarly = true; break; } } } if (shouldExitEarly) break; } for (const [uri, nodes] of Object.entries(matchingNodes)) { for (const node of nodes) { const locations = getLocationWrapper(definitionSymbol, node, uri) .filter(loc => !results.some(location => Locations.Location.equals(loc, location))); results.push(...locations); } } const docShorthand = `${workspace_manager_1.workspaceManager.current?.name}`; const count = results.length; const name = definitionSymbol.name; logCallback(`Found ${count} references for symbol '${name}' in document '${docShorthand}'`, 'info'); if (reporting) reporter?.done(); const sorter = locationSorter(definitionSymbol); return results.sort(sorter); } function allUnusedLocalReferences(document) { const symbols = (0, symbol_1.filterFirstPerScopeSymbol)(document).filter(s => s.isLocal() && s.name !== 'argv' && !s.isEventHook()); if (!symbols) return []; const usedSymbols = []; const unusedSymbols = []; for (const symbol of symbols) { const localSymbols = (0, exports.getFilteredLocalSymbols)(symbol, document); let found = false; const root = analyze_1.analyzer.getRootNode(document.uri); if (!root) { logger_1.logger.warning(`No root node found for document ${document.uri}`); continue; } for (const node of (0, tree_sitter_1.getChildNodes)(root)) { if (localSymbols?.some(c => c.scopeContainsNode(node))) { continue; } if (symbol.isReference(document, node, true)) { found = true; usedSymbols.push(symbol); break; } } if (!found) unusedSymbols.push(symbol); } const finalUnusedSymbols = unusedSymbols.filter(symbol => { if (symbol.isArgparse() && usedSymbols.some(s => s.equalArgparse(symbol))) { return false; } if (symbol.hasEventHook()) { if (symbol.isGlobal()) return false; if (symbol.isLocal() && symbol.children.some(c => c.fishKind === 'FUNCTION_EVENT' && usedSymbols.some(s => s.isEmittedEvent() && c.name === s.name))) { return false; } if (symbol.document.isAutoloaded() && symbol.isFunction() && symbol.hasEventHook()) { const eventsEmitted = symbol.children.filter(c => c.isEventHook()); for (const event of eventsEmitted) { if (analyze_1.analyzer.findNode(n => (0, emit_1.isEmittedEventDefinitionName)(n) && n.text === event.name)) { return false; } } } } return true; }); logger_1.logger.debug({ usage: 'finalUnusedLocalReferences', finalUnusedSymbols: finalUnusedSymbols.map(s => s.name), }); return finalUnusedSymbols; } function getImplementation(document, position) { const locations = []; const node = analyze_1.analyzer.nodeAtPoint(document.uri, position.line, position.character); if (!node) return []; const symbol = analyze_1.analyzer.getDefinition(document, position); if (!symbol) return []; if (symbol.isEmittedEvent()) { const result = analyze_1.analyzer.findSymbol((s, _) => s.isEventHook() && s.name === symbol.name)?.toLocation(); if (result) { locations.push(result); return locations; } } if (symbol.isEventHook()) { const result = analyze_1.analyzer.findSymbol((s, _) => s.isEmittedEvent() && s.name === symbol.name)?.toLocation(); if (result) { locations.push(result); return locations; } } const newLocations = getReferences(document, position) .filter(location => location.uri !== document.uri); if (newLocations.some(s => s.uri === symbol.uri)) { locations.push(symbol.toLocation()); return locations; } if (newLocations.some(s => s.uri.includes('completions/'))) { locations.push(newLocations.find(s => s.uri.includes('completions/'))); return locations; } locations.push(symbol.toLocation()); return locations; } function getLocationWrapper(symbol, node, uri) { let range = (0, tree_sitter_1.getRange)(node); if (symbol.fishKind === 'ARGPARSE' && (0, node_types_1.isOption)(node)) { range = { start: { line: range.start.line, character: range.start.character + getLeadingDashCount(node), }, end: { line: range.end.line, character: range.end.character + 1, }, }; return [vscode_languageserver_1.Location.create(uri, range)]; } if ((0, alias_1.isAliasDefinitionValue)(node)) { const parent = (0, node_types_1.findParentCommand)(node); if (!parent) return []; const info = alias_1.FishAlias.getInfo(parent); if (!info) return []; const aliasRange = extractCommandRangeFromAliasValue(node, symbol.name); if (aliasRange) { range = aliasRange; } return [vscode_languageserver_1.Location.create(uri, range)]; } if (NestedSyntaxNodeWithReferences.isBindCall(symbol, node)) { return (0, nested_strings_1.extractMatchingCommandLocations)(symbol, node, uri); } if (NestedSyntaxNodeWithReferences.isCompleteConditionCall(symbol, node)) { return (0, nested_strings_1.extractMatchingCommandLocations)(symbol, node, uri); } if (symbol.isFunction() && ((0, node_types_1.isString)(node) || (0, node_types_1.isOption)(node))) { return (0, nested_strings_1.extractCommandLocations)(node, uri) .filter(loc => loc.command === symbol.name) .map(loc => loc.location); } return [vscode_languageserver_1.Location.create(uri, range)]; } function getLeadingDashCount(node) { if (!node || !node.text) return 0; const text = node.text; let count = 0; for (let i = 0; i < text.length; i++) { if (text[i] === '-') { count++; } else { break; } } return count; } var NestedSyntaxNodeWithReferences; (function (NestedSyntaxNodeWithReferences) { function isAliasValueNode(definitionSymbol, node) { if (!(0, alias_1.isAliasDefinitionValue)(node)) return false; const parent = (0, node_types_1.findParentCommand)(node); if (!parent) return false; const info = alias_1.FishAlias.getInfo(parent); if (!info) return false; const infoCmds = info.value.split(';').map(cmd => cmd.trim().split(' ').at(0)); return infoCmds.includes(definitionSymbol.name); } NestedSyntaxNodeWithReferences.isAliasValueNode = isAliasValueNode; function isBindCall(definitionSymbol, node) { if (!node?.parent || (0, node_types_1.isOption)(node)) return false; const parent = (0, node_types_1.findParentCommand)(node); if (!parent || !(0, node_types_1.isCommandWithName)(parent, 'bind')) return false; const subcommands = parent.children.slice(2).filter(c => !(0, node_types_1.isOption)(c)); if (!subcommands.some(c => c.equals(node))) return false; const cmds = (0, nested_strings_1.extractCommands)(node); return cmds.some(cmd => cmd === definitionSymbol.name); } NestedSyntaxNodeWithReferences.isBindCall = isBindCall; function isCompleteConditionCall(definitionSymbol, node) { if ((0, node_types_1.isOption)(node) || !node.isNamed || (0, node_types_1.isProgram)(node)) return false; if (!node.parent || !(0, node_types_1.isCommandWithName)(node.parent, 'complete')) return false; if (!node?.previousSibling || !(0, node_types_1.isMatchingOption)(node?.previousSibling, options_1.Option.fromRaw('-n', '--condition'))) return false; const cmds = (0, nested_strings_1.extractCommands)(node); logger_1.logger.debug(`Extracted commands from complete condition node: ${cmds}`); return !!cmds.some(cmd => cmd.trim() === definitionSymbol.name); } NestedSyntaxNodeWithReferences.isCompleteConditionCall = isCompleteConditionCall; function isWrappedCall(definitionSymbol, node) { if (!node?.parent || !(0, node_types_1.findParentFunction)(node)) return false; if (node.previousNamedSibling && (0, node_types_1.isMatchingOption)(node.previousNamedSibling, options_1.Option.fromRaw('-w', '--wraps'))) { const cmds = (0, nested_strings_1.extractCommands)(node); logger_1.logger.debug(`Extracted commands from wrapped call node: ${cmds}`); return cmds.some(cmd => cmd.trim() === definitionSymbol.name); } if ((0, options_1.isMatchingOptionOrOptionValue)(node, options_1.Option.fromRaw('-w', '--wraps'))) { logger_1.logger.warning(`Node ${node.text} is a wrapped call for symbol ${definitionSymbol.name}`); const cmds = (0, nested_strings_1.extractCommands)(node); logger_1.logger.debug(`Extracted commands from wrapped call node: ${cmds}`); return cmds.some(cmd => cmd.trim() === definitionSymbol.name); } return false; } NestedSyntaxNodeWithReferences.isWrappedCall = isWrappedCall; function isAnyNestedCommand(definitionSymbol, node) { return isAliasValueNode(definitionSymbol, node) || isBindCall(definitionSymbol, node) || isCompleteConditionCall(definitionSymbol, node); } NestedSyntaxNodeWithReferences.isAnyNestedCommand = isAnyNestedCommand; })(NestedSyntaxNodeWithReferences || (exports.NestedSyntaxNodeWithReferences = NestedSyntaxNodeWithReferences = {})); function isSymbolLocalToDocument(symbol) { if (symbol.isGlobal()) return false; if (symbol.isLocal() && symbol.isArgparse()) { const parent = symbol.parent; if (parent && parent.isGlobal()) return false; } if (symbol.document.isAutoloaded()) { if (symbol.isFunction() || symbol.hasEventHook()) { return false; } if (symbol.isEvent()) { return false; } } return true; } function extractCommandRangeFromAliasValue(node, commandName) { const text = node.text; let searchText = text; let baseOffset = 0; if (text.includes('=')) { const equalsIndex = text.indexOf('='); searchText = text.substring(equalsIndex + 1); baseOffset = equalsIndex + 1; } if (searchText.startsWith('"') && searchText.endsWith('"') || searchText.startsWith("'") && searchText.endsWith("'")) { searchText = searchText.slice(1, -1); baseOffset += 1; } const commandMatches = findCommandPositions(searchText, commandName); if (commandMatches.length === 0) return null; const firstMatch = commandMatches[0]; if (!firstMatch) return null; const startOffset = baseOffset + firstMatch.start; const endOffset = startOffset + commandName.length; return vscode_languageserver_1.Range.create(node.startPosition.row, node.startPosition.column + startOffset, node.startPosition.row, node.startPosition.column + endOffset); } function findCommandPositions(shellCode, commandName) { const matches = []; const commandSeparators = /([;&|]+|\s*&&\s*|\s*\|\|\s*)/; const parts = shellCode.split(commandSeparators); let currentOffset = 0; for (const part of parts) { if (!part || commandSeparators.test(part)) { currentOffset += part.length; continue; } const trimmedPart = part.trim(); const partStartOffset = currentOffset + part.indexOf(trimmedPart); if (trimmedPart) { const firstWordMatch = trimmedPart.match(/^([^\s]+)/); if (firstWordMatch) { const firstWord = firstWordMatch[1]; if (firstWord === commandName) { matches.push({ start: partStartOffset, end: partStartOffset + commandName.length, }); } } } currentOffset += part.length; } return matches; } function* getChildNodesOptimized(symbol, doc) { const root = analyze_1.analyzer.analyze(doc).root; if (!root) return; const localSymbols = analyze_1.analyzer.getFlatDocumentSymbols(doc.uri) .filter(s => { if (s.uri === doc.uri) return false; if (s.isFunction() && s.isLocal() && s.name === symbol.name && symbol.isFunction()) { return !s.equals(symbol); } return s.name === symbol.name && s.kind === symbol.kind && s.isLocal() && !symbol.equalDefinition(s); }); const skipNodes = localSymbols.map(s => s.parent?.node).filter(n => n !== undefined); const isPotentialMatch = (current) => { if (symbol.isArgparse() && ((0, node_types_1.isOption)(current) || current.text === symbol.name || current.text === symbol.argparseFlagName)) { return true; } else if (symbol.name === current.text) { return true; } else if ((0, node_types_1.isString)(current)) { return true; } if (symbol.isFunction()) { return symbol.name === current.text || (0, node_types_1.isCommandName)(current) || current.type === 'word' || current.isNamed; } return false; }; const queue = [root]; while (queue.length > 0) { const current = queue.shift(); if (!current) continue; if (skipNodes.some(s => (0, tree_sitter_1.containsNode)(s, current) || s.equals(current) && !(0, node_types_1.isProgram)(current))) { continue; } if (isPotentialMatch(current)) { yield current; } if (current.children.length > 0) { queue.unshift(...current.children); } } } function getDocumentsToSearch(document, logCallback, opts) { let documentsToSearch = []; if (opts.localOnly) { documentsToSearch.push(document); } else if (opts.allWorkspaces) { workspace_manager_1.workspaceManager.all.forEach((ws) => { documentsToSearch.push(...ws.allDocuments()); }); } else { let currentWorkspace = workspace_manager_1.workspaceManager.current; if (!currentWorkspace) { currentWorkspace = workspace_manager_1.workspaceManager.findContainingWorkspace(document.uri) || undefined; if (!currentWorkspace) { logCallback(`No current workspace found for document ${document.uri}`, 'warning'); return [document]; } } currentWorkspace?.allDocuments().forEach((doc) => { documentsToSearch.push(doc); }); } if (opts.onlyInFiles && opts.onlyInFiles.length > 0) { documentsToSearch = documentsToSearch.filter(doc => { const fileType = doc.getAutoloadType(); if (!fileType) return false; return opts.onlyInFiles.includes(fileType); }); } return documentsToSearch; } function logWrapper(document, position, opts) { const posStr = `{line: ${position.line}, character: ${position.character}}`; const requestMsg = `getReferencesNew(params) -> ${new Date().toISOString()}`; const params = { uri: (0, translation_1.uriToReadablePath)(document.uri), position: posStr, opts: opts, }; const startTime = performance.now(); return function (message, level = 'info') { if (!opts.loggingEnabled) return; const endTime = performance.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); const logObj = { request: requestMsg, params, message, }; switch (level) { case 'info': logger_1.logger.info(logObj, duration); break; case 'debug': logger_1.logger.debug(logObj, duration); break; case 'warning': logger_1.logger.warning(logObj, duration); break; case 'error': logger_1.logger.error({ ...logObj, message: `Error: ${message}`, duration, }); break; default: logger_1.logger.warning({ ...logObj, message: `Unknown log level: ${level}. Original message: ${message}`, duration, }); break; } logger_1.logger.debug(`DURATION: ${duration}`, { uri: (0, translation_1.uriToReadablePath)(document.uri), position: posStr }); }; } const locationSorter = (defSymbol) => { const getUriPriority = (defSymbol) => { return (uri) => { let basePriority = 10; if (defSymbol.isArgparse()) { if (uri === defSymbol.uri) basePriority = 100; else if (uri.includes('completions/')) basePriority = 50; } else if (defSymbol.isFunction()) { if (uri === defSymbol.uri) basePriority = 100; else if (uri.includes('completions/')) basePriority = 50; } else if (defSymbol.isVariable()) { if (uri === defSymbol.uri) basePriority = 100; } const uriHash = uri.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); return basePriority + uriHash % 1000 / 10000; }; }; const uriPriority = getUriPriority(defSymbol); return function (a, b) { const aUriPriority = uriPriority(a.uri); const bUriPriority = uriPriority(b.uri); if (aUriPriority !== bUriPriority) { return bUriPriority - aUriPriority; } if (a.range.start.line !== b.range.start.line) { return a.range.start.line - b.range.start.line; } return a.range.start.character - b.range.start.character; }; }; const getFilteredLocalSymbols = (definitionSymbol, doc) => { if (definitionSymbol.isVariable() && !definitionSymbol.isArgparse()) { return analyze_1.analyzer.getFlatDocumentSymbols(doc.uri) .filter(s => s.isLocal() && !s.equals(definitionSymbol) && !definitionSymbol.equalScopes(s) && s.name === definitionSymbol.name && s.kind === definitionSymbol.kind); } if (doc.uri === definitionSymbol.uri) return []; return analyze_1.analyzer.getFlatDocumentSymbols(doc.uri) .filter(s => s.isLocal() && s.name === definitionSymbol.name && s.kind === definitionSymbol.kind && !s.equals(definitionSymbol)); }; exports.getFilteredLocalSymbols = getFilteredLocalSymbols;