UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

298 lines (297 loc) 10.4 kB
"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); }