UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

403 lines (402 loc) 15.7 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.signatureIndex = void 0; exports.buildSignature = buildSignature; exports.getCurrentNodeType = getCurrentNodeType; exports.lineSignatureBuilder = lineSignatureBuilder; exports.getPipes = getPipes; exports.getAliasedCompletionItemSignature = getAliasedCompletionItemSignature; exports.regexStringSignature = regexStringSignature; exports.isMatchingOption = isMatchingOption; exports.getActiveParameterIndex = getActiveParameterIndex; exports.isRegexStringSignature = isRegexStringSignature; exports.findActiveParameterStringRegex = findActiveParameterStringRegex; exports.getDefaultSignatures = getDefaultSignatures; exports.getFunctionSignatureHelp = getFunctionSignatureHelp; const vscode_languageserver_1 = require("vscode-languageserver"); const snippets_1 = require("./utils/snippets"); const NodeTypes = __importStar(require("./utils/node-types")); const TreeSitter = __importStar(require("./utils/tree-sitter")); const options_1 = require("./parsing/options"); const markdown_builder_1 = require("./utils/markdown-builder"); const translation_1 = require("./utils/translation"); function buildSignature(label, value) { return { label: label, documentation: { kind: 'markdown', value: value, }, }; } function getCurrentNodeType(input) { const prebuiltTypes = snippets_1.PrebuiltDocumentationMap.getByName(input); if (!prebuiltTypes || prebuiltTypes.length === 0) { return null; } let longestDocs = prebuiltTypes[0]; for (const prebuilt of prebuiltTypes) { if (prebuilt.description.length > longestDocs.description.length) { longestDocs = prebuilt; } } return longestDocs; } function lineSignatureBuilder(lineRootNode, lineCurrentNode, _completeMmap) { const currentCmd = NodeTypes.findParentCommand(lineCurrentNode) || lineRootNode; const pipes = getPipes(lineRootNode); const varNode = getVariableNode(lineRootNode); const allCmds = getAllCommands(lineRootNode); const regexOption = getRegexOption(lineRootNode); if (pipes.length === 1) return getPipesSignature(pipes); switch (true) { case isStringWithRegex(currentCmd.text, regexOption): return getDefaultSignatures(); case varNode && isSetOrReadWithVarNode(currentCmd?.text || lineRootNode.text, varNode, lineRootNode, allCmds): return getSignatureForVariable(varNode); case currentCmd?.text.startsWith('return') || lineRootNode.text.startsWith('return'): return getReturnStatusSignature(); case allCmds.length === 1: return getCommandSignature(currentCmd); default: return null; } } function getPipes(rootNode) { const pipeNames = snippets_1.PrebuiltDocumentationMap.getByType('pipe'); return TreeSitter.getChildNodes(rootNode).reduce((acc, node) => { const pipe = pipeNames.find(p => p.name === node.text); if (pipe) acc.push(pipe); return acc; }, []); } function getVariableNode(rootNode) { return TreeSitter.getChildNodes(rootNode).find(c => NodeTypes.isVariableDefinition(c)); } function getAllCommands(rootNode) { return TreeSitter.getChildNodes(rootNode).filter(c => NodeTypes.isCommand(c)); } function getRegexOption(rootNode) { return TreeSitter.getChildNodes(rootNode).find(n => NodeTypes.isMatchingOption(n, options_1.Option.create('-r', '--regex'))); } function isStringWithRegex(line, regexOption) { return line.startsWith('string') && !!regexOption; } function isSetOrReadWithVarNode(line, varNode, rootNode, allCmds) { return !!varNode && (line.startsWith('set') || line.startsWith('read')) && allCmds.pop()?.text === rootNode.text.trim(); } function getSignatureForVariable(varNode) { const output = getCurrentNodeType(varNode.text); if (!output) return null; return { signatures: [buildSignature(output.name, output.description)], activeSignature: 0, activeParameter: 0, }; } function getReturnStatusSignature() { const output = snippets_1.PrebuiltDocumentationMap.getByType('status').map((o) => `___${o.name}___ - _${o.description}_`).join('\n'); return { signatures: [buildSignature('$status', output)], activeSignature: 0, activeParameter: 0, }; } function getPipesSignature(pipes) { return { signatures: pipes.map((o) => buildSignature(o.name, `${o.name} - _${o.description}_`)), activeSignature: 0, activeParameter: 0, }; } function getCommandSignature(firstCmd) { const output = snippets_1.PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text); return { signatures: [buildSignature(firstCmd.text, output.map((o) => `${o.name} - _${o.description}_`).join('\n'))], activeSignature: 0, activeParameter: 0, }; } function getAliasedCompletionItemSignature(item) { return { signatures: [buildSignature(item.label, [ '```fish', `${item.fishKind} ${item.label} ${item.detail}`, '```', ].join('\n'))], activeSignature: 0, activeParameter: 0, }; } function regexStringSignature() { const signatureDoc = { kind: 'markdown', value: [ markdownStringRepetitions, markdownStringCharClasses, markdownStringGroups, ].join('\n---\n'), }; return { label: 'Regex Patterns', documentation: signatureDoc, }; } function regexStringCharacterSets() { const inputText = [ markdownStringRepetitions, markdownStringCharClasses, markdownStringGroups, ].join('\n---\n'); const parameters = [ vscode_languageserver_1.ParameterInformation.create('argv[1]', inputText), vscode_languageserver_1.ParameterInformation.create('argv[2]', inputText), ]; return { label: 'Regex Groups', documentation: { kind: 'markdown', value: markdownStringCharacterSets, }, parameters: parameters, activeParameter: 0, }; } function isMatchingOption(text, options) { if (!text.startsWith('-')) return false; if (text.startsWith('--') && options.longOption) { const cleanText = text.includes('=') ? text.slice(0, text.indexOf('=')) : text; return cleanText === `--${options.longOption}`; } if (text.startsWith('-') && options.shortOption) { return text.slice(1).includes(options.shortOption); } return false; } function getActiveParameterIndex(line, commandName, needsSubcommand, cursorPosition) { const tokens = line.trim().split(/\s+/); let currentPosition = 0; let paramIndex = 0; const commands = commandName.split(' '); let previousWasCommand = false; for (const token of tokens) { if (commands.includes(token) || ['if', 'else if', 'switch', 'case'].includes(token)) { cursorPosition += token.length + 1; previousWasCommand = true; continue; } if (needsSubcommand && previousWasCommand) { cursorPosition += token.length + 1; previousWasCommand = false; continue; } break; } for (let i = 1; i < tokens.length; i++) { const token = tokens[i]; if (currentPosition + token.length >= cursorPosition) { break; } if (token.startsWith('-')) { if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) { i++; currentPosition += tokens[i].length + 1; } } else { paramIndex++; } currentPosition += token.length + 1; } return paramIndex; } function isRegexStringSignature(line) { const tokens = line.split(' '); const hasStringCommand = tokens.some(token => token === 'string') && !tokens.some(token => token === '--'); if (hasStringCommand) { return tokens.some(token => isMatchingOption(token, { shortOption: 'r', longOption: 'regex', })); } return false; } function findActiveParameterStringRegex(line, cursorPosition) { const tokens = line.split(' '); const hasStringCommand = tokens.some(token => token === 'string'); const isRegex = hasStringCommand && tokens.some(token => isMatchingOption(token, { shortOption: 'r', longOption: 'regex', })); const activeParameter = isRegex ? getActiveParameterIndex(line, 'string ', true, cursorPosition) : 0; return { isRegex, activeParameter }; } exports.signatureIndex = { stringRegexPatterns: 0, stringRegexCharacterSets: 1, }; function getDefaultSignatures() { return { activeParameter: 0, activeSignature: 0, signatures: [ regexStringSignature(), regexStringCharacterSets(), ], }; } function getFunctionSignatureHelp(analyzer, lineLastNode, line, position) { const functionName = lineLastNode.parent?.firstNamedChild?.text.trim(); if (!functionName) return null; const funcSymbol = analyzer.findSymbol((symbol, _) => symbol.name === functionName); if (!funcSymbol || funcSymbol.kind !== vscode_languageserver_1.SymbolKind.Function) return null; const paramNames = funcSymbol.children .filter(s => s.fishKind === 'FUNCTION_VARIABLE' && s.name !== 'argv'); const argvParam = funcSymbol.children .find(s => s.fishKind === 'FUNCTION_VARIABLE' && s.name === 'argv'); if (argvParam) { paramNames.push(argvParam); } const paramDocs = paramNames.map((p, idx) => { const markdownString = p.toMarkupContent().value.split(markdown_builder_1.md.separator()); const label = p.name === 'argv' ? `$${p.name}[${idx + 1}..-1]` : p.name; const newContentString = p.name === 'argv' ? [ '', `${markdown_builder_1.md.bold(`(${(0, translation_1.symbolKindToString)(p.kind)})`)} ${label}`, markdown_builder_1.md.separator(), `This parameter corresponds to ${markdown_builder_1.md.inlineCode(`$argv[${idx + 1}..-1]`)} in the function.`, '', ].join(markdown_builder_1.md.newline()) : [ '', `${markdown_builder_1.md.bold(`(${(0, translation_1.symbolKindToString)(p.kind)})`)} ${markdown_builder_1.md.inlineCode(p.name)}`, markdown_builder_1.md.separator(), `This parameter corresponds to ${markdown_builder_1.md.inlineCode(`$argv[${idx + 1}]`)} in the function.`, '', ].join(markdown_builder_1.md.newline()); const newValue = p.name === 'argv' ? [ newContentString, ].join(markdown_builder_1.md.separator()) : [ newContentString, markdownString.slice(3, 4), ].join(markdown_builder_1.md.separator()); const newContent = { kind: vscode_languageserver_1.MarkupKind.Markdown, value: newValue, }; return { label: label, documentation: newContent, }; }); const label = `${funcSymbol.name} ${paramDocs.map(p => p.label).join(' ')}`.trim(); const signature = vscode_languageserver_1.SignatureInformation.create(label, funcSymbol.detail, ...paramDocs); signature.documentation = { kind: vscode_languageserver_1.MarkupKind.Markdown, value: funcSymbol.detail || 'No documentation available', }; const activeParameter = calculateActiveParameter(line, position) - 1; return { signatures: [signature], activeSignature: 0, activeParameter: Math.min(activeParameter, paramNames.length - 1), }; } function calculateActiveParameter(line, position) { const textBeforeCursor = line.substring(0, position.character); const tokens = textBeforeCursor.trim().split(/\s+/); let paramCount = 0; for (let i = 1; i < tokens.length; i++) { const token = tokens[i]; if (token?.startsWith('-')) { if (i + 1 < tokens.length && !tokens[i + 1]?.startsWith('-')) { i++; } continue; } paramCount++; } return paramCount; } const markdownStringRepetitions = [ 'Repetitions', '-----------', '- __*__ refers to 0 or more repetitions of the previous expression', '- __+__ 1 or more', '- __?__ 0 or 1.', '- __{n}__ to exactly n (where n is a number)', '- __{n,m}__ at least n, no more than m.', '- __{n,}__ n or more', ].join('\n'); const markdownStringCharClasses = [ 'Character Classes', '-----------------', '- __.__ any character except newline', '- __\\d__ a decimal digit and __\\D__, not a decimal digit', '- __\\s__ whitespace and __\\S__, not whitespace', '- __\\w__ a “word” character and __\\W__, a “non-word” character', '- __\\b__ a “word” boundary, and __\\B__, not a word boundary', '- __[...]__ (where “…” is some characters) is a character set', '- __[^...]__ is the inverse of the given character set', '- __[x-y]__ is the range of characters from x-y', '- __[[:xxx:]]__ is a named character set', '- __[[:^xxx:]]__ is the inverse of a named character set', ].join('\n'); const markdownStringCharacterSets = [ '__[[:alnum:]]__ : “alphanumeric”', '__[[:alpha:]]__ : “alphabetic”', '__[[:ascii:]]__ : “0-127”', '__[[:blank:]]__ : “space or tab”', '__[[:cntrl:]]__ : “control character”', '__[[:digit:]]__ : “decimal digit”', '__[[:graph:]]__ : “printing, excluding space”', '__[[:lower:]]__ : “lower case letter”', '__[[:print:]]__ : “printing, including space”', '__[[:punct:]]__ : “printing, excluding alphanumeric”', '__[[:space:]]__ : “white space”', '__[[:upper:]]__ : “upper case letter”', '__[[:word:]]__ : “same as w”', '__[[:xdigit:]]__ : “hexadecimal digit”', ].join('\n'); const markdownStringGroups = [ 'Groups', '------', '- __(...)__ is a capturing group', '- __(?:...)__ is a non-capturing group', '- __\\n__ is a backreference (where n is the number of the group, starting with 1)', '- __$n__ is a reference from the replacement expression to a group in the match expression.', ].join('\n');