UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

242 lines (212 loc) 8.25 kB
import { MarkupContent, SignatureHelp, SignatureInformation, } from 'vscode-languageserver'; import { SyntaxNode } from 'web-tree-sitter'; import { ExtendedBaseJson, PrebuiltDocumentationMap } from './utils/snippets'; import { FishAliasCompletionItem } from './utils/completion/types'; import * as NodeTypes from './utils/node-types'; import * as TreeSitter from './utils/tree-sitter'; import { CompletionItemMap } from './utils/completion/startup-cache'; export function buildSignature(label: string, value: string) : SignatureInformation { return { label: label, documentation: { kind: 'markdown', value: value, }, }; } export function getCurrentNodeType(input: string) { const prebuiltTypes = 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; } export function lineSignatureBuilder(lineRootNode: SyntaxNode, lineCurrentNode: SyntaxNode, _completeMmap: CompletionItemMap): SignatureHelp | null { 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; } } export function getPipes(rootNode: SyntaxNode): ExtendedBaseJson[] { const pipeNames = PrebuiltDocumentationMap.getByType('pipe'); return TreeSitter.getChildNodes(rootNode).reduce((acc: ExtendedBaseJson[], node) => { const pipe = pipeNames.find(p => p.name === node.text); if (pipe) acc.push(pipe); return acc; }, []); } function getVariableNode(rootNode: SyntaxNode): SyntaxNode | undefined { return TreeSitter.getChildNodes(rootNode).find(c => NodeTypes.isVariableDefinition(c)); } function getAllCommands(rootNode: SyntaxNode): SyntaxNode[] { return TreeSitter.getChildNodes(rootNode).filter(c => NodeTypes.isCommand(c)); } function getRegexOption(rootNode: SyntaxNode): SyntaxNode | undefined { return TreeSitter.getChildNodes(rootNode).find(n => NodeTypes.isMatchingOption(n, { shortOption: '-r', longOption: '--regex' })); } function isStringWithRegex(line: string, regexOption: SyntaxNode | undefined): boolean { return line.startsWith('string') && !!regexOption; } function isSetOrReadWithVarNode(line: string, varNode: SyntaxNode | undefined, rootNode: SyntaxNode, allCmds: SyntaxNode[]): boolean { return !!varNode && (line.startsWith('set') || line.startsWith('read')) && allCmds.pop()?.text === rootNode.text.trim(); } function getSignatureForVariable(varNode: SyntaxNode): SignatureHelp | null { const output = getCurrentNodeType(varNode.text); if (!output) return null; return { signatures: [buildSignature(output.name, output.description)], activeSignature: 0, activeParameter: 0, }; } function getReturnStatusSignature(): SignatureHelp { const output = PrebuiltDocumentationMap.getByType('status').map((o: ExtendedBaseJson) => `___${o.name}___ - _${o.description}_`).join('\n'); return { signatures: [buildSignature('$status', output)], activeSignature: 0, activeParameter: 0, }; } function getPipesSignature(pipes: ExtendedBaseJson[]): SignatureHelp { return { signatures: pipes.map((o: ExtendedBaseJson) => buildSignature(o.name, `${o.name} - _${o.description}_`)), activeSignature: 0, activeParameter: 0, }; } function getCommandSignature(firstCmd: SyntaxNode): SignatureHelp { const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text); return { signatures: [buildSignature(firstCmd.text, output.map((o: ExtendedBaseJson) => `${o.name} - _${o.description}_`).join('\n'))], activeSignature: 0, activeParameter: 0, }; } export function getAliasedCompletionItemSignature(item: FishAliasCompletionItem): SignatureHelp { // const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text); return { signatures: [buildSignature(item.label, [ '```fish', `${item.fishKind} ${item.label} ${item.detail}`, '```', ].join('\n'))], activeSignature: 0, activeParameter: 0, }; } export function regexStringSignature() : SignatureInformation { //const regexItems = stringRegexExpressions; //let signatureDoc = ["__String Regex Patterns__", "---"]; //for (const item of regexItems) { // signatureDoc.push(`${item.label} {item.description}`) //} const signatureDoc: MarkupContent = { kind: 'markdown', value: [ markdownStringRepetitions, markdownStringCharClasses, markdownStringGroups, ].join('\n---\n'), }; return { label: 'Regex Patterns', documentation: signatureDoc, }; } function regexStringCharacterSets(): SignatureInformation { return { label: 'Regex Groups', documentation: { kind: 'markdown', value: markdownStringCharacterSets, } as MarkupContent, }; } type signatureType = 'stringRegexPatterns' | 'stringRegexCharacterSets'; export const signatureIndex: {[str in signatureType]: number} = { stringRegexPatterns: 0, stringRegexCharacterSets: 1, }; export function getDefaultSignatures() : SignatureHelp { return { activeParameter: 0, activeSignature: 0, signatures: [ regexStringSignature(), regexStringCharacterSets(), ], }; } // REGEX STRING LINES 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');