UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

365 lines (332 loc) 10.8 kB
import { Hover, MarkupContent, MarkupKind } from 'vscode-languageserver-protocol/node'; import { SyntaxNode } from 'web-tree-sitter'; // import { hasPossibleSubCommand } from './utils/builtins'; import { execCommandDocs, execCommandType, CompletionArguments, execCompleteSpace, execCompleteCmdArgs, documentCommandDescription } from './utils/exec'; import { getChildNodes, getNodeText } from './utils/tree-sitter'; //////// @TODO: clean up this file export type markdownFiletypes = 'fish' | 'man'; export function enrichToMarkdown(doc: string): MarkupContent { return { kind: MarkupKind.Markdown, value: [ doc, ].join(), }; } export function enrichToCodeBlockMarkdown(doc: string, filetype:markdownFiletypes = 'fish'): MarkupContent { return { kind: MarkupKind.Markdown, value: [ '```' + filetype, doc.trim(), '```', ].join('\n'), }; } export function enrichWildcard(label: string, documentation: string, examples: [string, string][]): MarkupContent { const exampleStr: string[] = ['---']; for (const [cmd, desc] of examples) { exampleStr.push(`__${cmd}__ - ${desc}`); } return { kind: MarkupKind.Markdown, value: [ `_${label}_ ${documentation}`, '---', exampleStr.join('\n'), ].join('\n'), }; } export function enrichCommandArg(doc: string): MarkupContent { const [_first, ...after] = doc.split('\t'); const first = _first?.trim() || ''; const second = after?.join('\t').trim() || ''; const arg = '__' + first + '__'; const desc = '_' + second + '_'; const enrichedDoc = [ arg, desc, ].join(' '); return enrichToMarkdown(enrichedDoc); } export function enrichCommandWithFlags(command: string, flags: string[]): MarkupContent { const retString = [ `___${command}___`, '___', flags.map(line => line.split('\t')) .map(line => `__${line[0]}__ _${line.slice(1).join(' ')}_`) .join('\n'), ].join('\n'); return enrichToMarkdown(retString); } export function enrichToPlainText(doc: string): MarkupContent { return { kind: MarkupKind.PlainText, value: doc.trim(), }; } export async function documentationHoverProvider(cmd: string) : Promise<Hover | null> { const cmdDocs = await execCommandDocs(cmd); const cmdType = await execCommandType(cmd); if (!cmdDocs) { return null; } else { return { contents: cmdType === 'command' ? enrichToCodeBlockMarkdown(cmdDocs, 'man') : enrichToCodeBlockMarkdown(cmdDocs, 'fish'), }; } } export async function documentationHoverProviderForBuiltIns(cmd: string): Promise<Hover | null> { const cmdDocs: string = await execCommandDocs(cmd); if (!cmdDocs) { return null; } const splitDocs = cmdDocs.split('\n'); const startIndex = splitDocs.findIndex((line: string) => line.trim() === 'NAME'); return { contents: { kind: MarkupKind.Markdown, value: [ `__${cmd.toUpperCase()}__ - _https://fishshell.com/docs/current/cmds/${cmd.trim()}.html_`, '___', '```man', splitDocs.slice(startIndex).join('\n'), '```', ].join('\n'), }, }; } function commandStringHelper(cmd: string) { const cmdArray = cmd.split(' ', 1); return cmdArray.length > 1 ? '___' + cmdArray[0] + '___' + ' ' + cmdArray[1] : '___' + cmdArray[0] + '___'; } export function documentationHoverCommandArg(root: SyntaxNode, cmp: CompletionArguments) : Hover { let text = ''; const argsArray = [...cmp.args.keys()]; for (const node of getChildNodes(root)) { const nodeText = getNodeText(node); if (nodeText.startsWith('-') && argsArray.includes(nodeText)) { text += '\n' + '_' + nodeText + '_ ' + cmp.args.get(nodeText); } } const cmd = commandStringHelper(cmp.command.trim()); return { contents: enrichToMarkdown( [ cmd, '---', text.trim(), ].join('\n'), ), }; } export function forwardSubCommandCollect(rootNode: SyntaxNode): string[] { const stringToComplete : string[] = []; for (const curr of rootNode.children) { if (curr.text.startsWith('-') && curr.text.startsWith('$')) { break; } else { stringToComplete.push(curr.text); } } return stringToComplete; } export function forwardArgCommandCollect(rootNode: SyntaxNode) : string[] { const stringToComplete : string[] = []; const _currentNode = rootNode.children; for (const curr of rootNode.children) { if (curr.text.startsWith('-') && curr.text.startsWith('$')) { stringToComplete.push(curr.text); } else { continue; } } return stringToComplete; } // export function collectCompletionOptions(rootNode: SyntaxNode) { // let cmdText = [rootNode.children[0]!.text]; // if (hasPossibleSubCommand(cmdText[0]!)) { // cmdText = forwardSubCommandCollect(rootNode); // } // // DIFF FLAG FORMATS // // consider the difference between, find -name .git // // and ls --long -l // // // do complete and check for each flagsToFind // // // //exec // // const flagsToFind = forwardArgCommandCollect(rootNode); // } /*export async function hoverForCommandArgument(node: SyntaxNode): Promise<Hover | null> {*/ /*const text = getNodeText(node) */ /*if (text.startsWith('-')) {*/ /*const parent = findParentCommand(node);*/ /*const hoverCompletion = new HoverFromCompletion(parent)*/ /*return await hoverCompletion.generate()*/ /*}*/ /*return null*/ /*}*/ function getFlagString(arr: string[]): string { return '__' + arr[0] + '__' + ' ' + arr[1] + '\n'; } export class HoverFromCompletion { private currentNode: SyntaxNode; private commandNode: SyntaxNode; private commandString: string = ''; private entireCommandString: string = ''; private completions: string[][] = []; private oldOptions: boolean = false; private flagsGiven: string[] = []; constructor(commandNode: SyntaxNode, currentNode: SyntaxNode) { this.currentNode = currentNode; this.commandNode = commandNode; this.commandString = commandNode.child(0)?.text || ''; this.entireCommandString = commandNode.text || ''; this.flagsGiven = this.entireCommandString .split(' ').slice(1) .filter(flag => flag.startsWith('-')) .map(flag => flag.split('=')[0]) as string[] || []; } /** * set this.commandString for possible subcommands * handles a command such as: * $ string match -ra '.*' -- "hello all people" */ private async checkForSubCommands() { const spaceCmps = await execCompleteSpace(this.commandString); if (spaceCmps.length === 0) { return this.commandString; } const cmdArr = this.commandNode.text.split(' ').slice(1); let i = 0; while (i < cmdArr.length) { const argStr = cmdArr[i]!.trim(); if (!argStr.startsWith('-') && spaceCmps.includes(argStr)) { this.commandString += ' ' + argStr.toString(); } else if (argStr.includes('-')) { break; } i++; } return this.commandString; } private isSubCommand() { const currentNodeText = this.currentNode.text; if (currentNodeText.startsWith('-') || currentNodeText.startsWith("'") || currentNodeText.startsWith('"')) { return false; } const cmdArr = this.commandString.split(' '); if (cmdArr.length > 1) { return cmdArr.includes(currentNodeText); } return false; } /** * @see man complete: styles --> long options * enables the ability to differentiate between * short flags chained together, or a command * that * a command option like: * '-Wall' or --> returns true * find -name '.git' --> returns true * * ls -la --> returns false * @param {string[]} cmpFlags - [TODO:description] * @returns {boolean} true if old styles are valid * false if short flags can be chained */ private hasOldStyleFlags() { for (const cmpArr of this.completions) { if (cmpArr[0]?.startsWith('--')) { continue; } else if (cmpArr[0]?.startsWith('-') && cmpArr[0]?.length > 2) { return true; } } return false; } /** * handles splitting short options if the command has no * old style flags. * @see this.hasOldStyleFlags() */ private reparseFlags() { const shortFlagsHandled = []; for (const flag of this.flagsGiven) { if (flag.startsWith('--')) { shortFlagsHandled.push(flag); } else if (flag.startsWith('-') && flag.length > 2) { const splitShortFlags = flag.split('').slice(1).map(str => '-' + str); shortFlagsHandled.push(...splitShortFlags); } } return shortFlagsHandled; } public async buildCompletions() { this.commandString = await this.checkForSubCommands(); const preBuiltCompletions = await execCompleteCmdArgs(this.commandString); for (const cmp of preBuiltCompletions) { this.completions.push(cmp.split('\t')); } return this.completions; } public findCompletion(flag: string) { for (const flagArr of this.completions) { if (flagArr[0] === flag) { return flagArr; } } return null; } private async checkForHoverDoc() { const cmd = await documentCommandDescription(this.commandString); const cmdArr = cmd.trim().split(' '); const cmdStrLen = this.commandString.split(' ').length; const boldText = '__' + cmdArr.slice(0, cmdStrLen).join(' ') + '__'; const otherText = ' ' + cmdArr.slice(cmdStrLen).join(' '); return boldText + otherText; } public async generateForFlags(): Promise<Hover> { let text = ''; this.completions = await this.buildCompletions(); this.oldOptions = this.hasOldStyleFlags(); const cmd = await this.checkForHoverDoc(); if (!this.oldOptions) { this.flagsGiven = this.reparseFlags(); } for (const flag of this.flagsGiven) { const found = this.findCompletion(flag); if (found) { text += getFlagString(found); } } return { contents: enrichToMarkdown([ cmd, '---', text.trim(), ].join('\n')), }; } public async generateForSubcommand() { return await documentationHoverProvider(this.commandString); } public async generate(): Promise<Hover | void> { this.commandString = await this.checkForSubCommands(); if (this.isSubCommand()) { const output = await documentationHoverProvider(this.commandString); //console.log(output) if (output) { return output; } } else { return await this.generateForFlags(); } return; } }