fish-lsp
Version:
LSP implementation for fish/fish-shell
608 lines (607 loc) • 24.9 kB
JavaScript
;
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;