fish-lsp
Version:
LSP implementation for fish/fish-shell
298 lines (297 loc) • 10.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractCommands = extractCommands;
exports.extractCommandLocations = extractCommandLocations;
exports.extractMatchingCommandLocations = extractMatchingCommandLocations;
const vscode_languageserver_1 = require("vscode-languageserver");
const tree_sitter_1 = require("../utils/tree-sitter");
const DEFAULT_CONFIG = {
parseCommandSubstitutions: true,
parseParenthesized: true,
cleanKeywords: true,
};
const FISH_KEYWORDS = new Set([
'and', 'or', 'not', 'begin', 'end', 'if', 'else', 'switch', 'case',
'for', 'in', 'while', 'function', 'return', 'break', 'continue',
'set', 'test', 'true', 'false',
]);
const FISH_OPERATORS = new Set([
'&&', '||', '|', ';', '&', '>', '<', '>>', '<<', '>&', '<&',
'2>', '2>>', '2>&1', '1>&2', '/dev/null',
]);
function extractCommands(node, config = DEFAULT_CONFIG) {
if (!node.text?.trim())
return [];
const nodeText = node.text;
const optionCommand = parseOptionArgument(nodeText);
if (optionCommand) {
return [optionCommand];
}
const cleanedText = cleanQuotes(nodeText);
const commands = new Set();
const directCommands = parseDirectCommands(cleanedText, config);
directCommands.forEach(cmd => commands.add(cmd));
if (config.parseCommandSubstitutions) {
const substitutionCommands = parseCommandSubstitutions(cleanedText);
substitutionCommands.forEach(cmd => commands.add(cmd));
}
if (config.parseParenthesized) {
const parenthesizedCommands = parseParenthesizedExpressions(cleanedText);
parenthesizedCommands.forEach(cmd => commands.add(cmd));
}
return Array.from(commands).filter(cmd => cmd.length > 0);
}
function extractCommandLocations(node, documentUri, config = DEFAULT_CONFIG) {
if (!node.text?.trim())
return [];
const nodeRange = (0, tree_sitter_1.getRange)(node);
const nodeText = node.text;
const optionCommand = parseOptionArgument(nodeText);
if (optionCommand) {
const offset = nodeText.indexOf(optionCommand);
return [{
command: optionCommand,
location: vscode_languageserver_1.Location.create(documentUri, createPreciseRange(optionCommand, offset, nodeRange)),
}];
}
const cleanedText = cleanQuotes(nodeText);
const quoteOffset = getQuoteOffset(nodeText);
return findCommandsWithOffsets(cleanedText, config)
.map(({ command, offset }) => ({
command,
location: vscode_languageserver_1.Location.create(documentUri, createPreciseRange(command, offset + quoteOffset, nodeRange)),
}));
}
function extractMatchingCommandLocations(symbol, node, documentUri, config = DEFAULT_CONFIG) {
return extractCommandLocations(node, documentUri, config)
.filter(ref => ref.command === symbol.name)
.map(ref => ref.location);
}
function cleanQuotes(input) {
const trimmed = input.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"') ||
trimmed.startsWith("'") && trimmed.endsWith("'")) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function getQuoteOffset(input) {
const trimmed = input.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"') ||
trimmed.startsWith("'") && trimmed.endsWith("'")) {
return 1;
}
return 0;
}
function findCommandsWithOffsets(text, config) {
const results = [];
results.push(...findDirectCommandOffsets(text, config));
if (config.parseCommandSubstitutions) {
results.push(...findCommandSubstitutionOffsets(text));
}
if (config.parseParenthesized) {
results.push(...findParenthesizedCommandOffsets(text));
}
return results;
}
function findCommandSubstitutionOffsets(text) {
const results = [];
const regex = /\$\(([^)]+)\)/g;
let match;
while ((match = regex.exec(text)) !== null) {
const commandText = match[1];
const innerOffset = match.index + 2;
if (commandText?.trim()) {
const firstCommand = getFirstCommand(commandText);
if (firstCommand) {
results.push({
command: firstCommand,
offset: innerOffset + commandText.indexOf(firstCommand),
});
}
}
}
return results;
}
function findParenthesizedCommandOffsets(text) {
const results = [];
const stack = [];
let start = -1;
for (let i = 0; i < text.length; i++) {
if (text[i] === '(') {
if (stack.length === 0)
start = i;
stack.push(i);
}
else if (text[i] === ')' && stack.length > 0) {
stack.pop();
if (stack.length === 0 && start !== -1) {
const innerText = text.slice(start + 1, i);
const innerOffset = start + 1;
if (innerText.trim()) {
const commands = extractCommandsFromText(innerText);
for (const command of commands) {
const commandOffset = innerText.indexOf(command);
if (commandOffset !== -1) {
results.push({
command,
offset: innerOffset + commandOffset,
});
}
}
}
start = -1;
}
}
}
return results;
}
function findDirectCommandOffsets(text, config) {
const results = [];
const statements = text.split(/[;&|]+/);
let currentOffset = 0;
for (const statement of statements) {
const trimmedStatement = statement.trim();
const statementStart = text.indexOf(trimmedStatement, currentOffset);
if (trimmedStatement) {
const tokens = tokenizeStatement(trimmedStatement);
const relevantTokens = config.cleanKeywords
? tokens.filter(token => !FISH_KEYWORDS.has(token) && !FISH_OPERATORS.has(token))
: tokens;
for (const token of relevantTokens) {
if (token && !isNumeric(token) && token.length > 1) {
const tokenOffset = trimmedStatement.indexOf(token);
if (tokenOffset !== -1) {
results.push({
command: token,
offset: statementStart + tokenOffset,
});
}
}
}
}
currentOffset = statementStart + statement.length;
}
return results;
}
function getFirstCommand(text) {
const tokens = tokenizeStatement(text);
return tokens.length > 0 && tokens[0] && tokens[0].length > 1 ? tokens[0] : null;
}
function extractCommandsFromText(input, cleanKeywords = true) {
const statements = input.split(/[;&|]+/)
.map(stmt => stmt.trim())
.filter(stmt => stmt.length > 0);
const commands = [];
for (const statement of statements) {
const tokens = tokenizeStatement(statement);
const filteredTokens = cleanKeywords
? tokens.filter(token => !FISH_KEYWORDS.has(token) && !FISH_OPERATORS.has(token))
: tokens;
for (const token of filteredTokens) {
if (token && !isNumeric(token) && token.length > 1) {
commands.push(token);
}
}
}
return commands;
}
function parseCommandSubstitutions(input) {
const commands = [];
const regex = /\$\(([^)]+)\)/g;
let match;
while ((match = regex.exec(input)) !== null) {
const commandText = match[1];
if (commandText?.trim()) {
commands.push(...extractCommandsFromText(commandText, true));
}
}
return commands;
}
function parseParenthesizedExpressions(input) {
const commands = [];
const stack = [];
let start = -1;
for (let i = 0; i < input.length; i++) {
if (input[i] === '(') {
if (stack.length === 0)
start = i;
stack.push(i);
}
else if (input[i] === ')' && stack.length > 0) {
stack.pop();
if (stack.length === 0 && start !== -1) {
const innerText = input.slice(start + 1, i);
if (innerText.trim()) {
commands.push(...extractCommandsFromText(innerText, true));
}
start = -1;
}
}
}
return commands;
}
function parseOptionArgument(text) {
const optionArgRegex = /^(?:-[a-zA-Z]|--[a-zA-Z][a-zA-Z0-9-]*)\s*=\s*([a-zA-Z_][a-zA-Z0-9_-]*)/;
const match = text.match(optionArgRegex);
if (match && match[1]) {
const command = match[1].trim();
if (command.length > 1 && !isNumeric(command)) {
return command;
}
}
return null;
}
function parseDirectCommands(input, config) {
return extractCommandsFromText(input, config.cleanKeywords);
}
function tokenizeStatement(statement) {
const tokens = [];
let current = '';
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < statement.length; i++) {
const char = statement[i];
if (!char)
continue;
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
current += char;
}
else if (inQuotes && char === quoteChar) {
inQuotes = false;
current += char;
quoteChar = '';
}
else if (!inQuotes && /\s/.test(char)) {
if (current.trim()) {
tokens.push(current.trim());
current = '';
}
}
else {
current += char;
}
}
if (current.trim()) {
tokens.push(current.trim());
}
return tokens;
}
function createPreciseRange(command, offset, nodeRange) {
const startChar = nodeRange.start.character + offset;
return {
start: {
line: nodeRange.start.line,
character: startChar,
},
end: {
line: nodeRange.start.line,
character: startChar + command.length,
},
};
}
function isNumeric(str) {
return /^[0-9]+$/.test(str);
}