UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

412 lines (377 loc) 14.9 kB
import { DocumentSymbol, SymbolKind, Range, WorkspaceSymbol, Position, Location, FoldingRange } from 'vscode-languageserver'; import { SyntaxNode } from 'web-tree-sitter'; import { isFunctionDefinitionName, isVariableDefinitionName, refinedFindParentVariableDefinitionKeyword } from './utils/node-types'; //import { findVariableDefinitionOptions } from './utils/options'; import { DocumentSymbolDetail } from './utils/symbol-documentation-builder'; import { getNodeAtRange, getRange, isPositionAfter, pointToPosition } from './utils/tree-sitter'; import { ScopeTag, DefinitionScope, getScope } from './utils/definition-scope'; import { GenericTree } from './utils/generic-tree'; import { LspDocument } from './document'; // add some form of tags to the symbol so that we can extend the symbol with more information // current implementation is WIP inside file : ./utils/options.ts export interface FishDocumentSymbol extends DocumentSymbol { name: string; uri: string; text: string; detail: string; kind: SymbolKind; range: Range; selectionRange: Range; scope: DefinitionScope; children: FishDocumentSymbol[]; } export namespace FishDocumentSymbol { /** * Creates a new symbol information literal. * * @param name The name of the symbol. * @param uri The documentUri of the symbol. * @param text The text in the symbol scope. * @param detail The detail of the symbol. (Markdown included inside 'range') * @param kind The kind of the symbol. * @param range The enclosing range of the symbol. * @param selectionRange The selectionRange of the symbol. * @param children Children of the symbol. */ export function create(name: string, uri: string, text: string, detail: string, kind: SymbolKind, range: Range, selectionRange: Range, scope: DefinitionScope, children: FishDocumentSymbol[]): FishDocumentSymbol { return { name, uri, text, detail, kind, range, selectionRange, scope, children, } as FishDocumentSymbol; } export function copy(symbol: FishDocumentSymbol, newChildren: FishDocumentSymbol[] = []): FishDocumentSymbol { return create( symbol.name, symbol.uri, symbol.text, symbol.detail, symbol.kind, symbol.range, symbol.selectionRange, symbol.scope, newChildren, ); } export function equal(a: FishDocumentSymbol, b: FishDocumentSymbol): boolean { return ( a.name === b.name && a.uri === b.uri && a.range.start.character === b.range.start.character && a.range.start.line === b.range.start.line && a.range.end.character === b.range.end.character && a.range.end.line === b.range.end.line && a.selectionRange.start.character === b.selectionRange.start.character && a.selectionRange.start.line === b.selectionRange.start.line && a.selectionRange.end.line === b.selectionRange.end.line && a.selectionRange.end.character === b.selectionRange.end.character ); } export function toWorkspaceSymbol(symbol: FishDocumentSymbol): WorkspaceSymbol { return WorkspaceSymbol.create( symbol.name, symbol.kind, symbol.uri, symbol.range, ); } export function toLocation(symbol: FishDocumentSymbol): Location { return Location.create( symbol.uri, symbol.selectionRange, ); } export function logString(symbol: FishDocumentSymbol): string { const symbolIcon = symbol.kind === SymbolKind.Function ? '  ' : '  '; return `${symbolIcon}${symbol.name} :::: ${symbol.scope.scopeTag}`; } export function flattenArray(symbols: FishDocumentSymbol[]) : FishDocumentSymbol[] { function* flattenGenerator(symbols: FishDocumentSymbol[]): Generator<FishDocumentSymbol> { for (const symbol of symbols) { yield symbol; yield* flattenGenerator(symbol.children); } } return [...flattenGenerator(symbols)]; } export function equalScopes(a: FishDocumentSymbol, b: FishDocumentSymbol): boolean { if (a.scope.scopeNode && b.scope.scopeNode) { if ([a.scope.scopeTag, b.scope.scopeTag].includes('inherit')) { return a.scope.scopeNode.equals(b.scope.scopeNode); } else if ( ['global', 'universal'].includes(a.scope.scopeTag) && ['global', 'universal'].includes(b.scope.scopeTag) ) { return true; } return a.scope.scopeTag === b.scope.scopeTag && a.scope.scopeNode.equals(b.scope.scopeNode); } return false; } /* * the first symbol is before the second symbol */ export function isBefore(first: FishDocumentSymbol, second: FishDocumentSymbol): boolean { return first.range.start.line < second.range.start.line; } /* * the first symbol is after the second symbol */ export function isAfter(first: FishDocumentSymbol, second: FishDocumentSymbol): boolean { return first.range.start.line > second.range.start.line; } export function getSyntaxNode(root: SyntaxNode, symbol: FishDocumentSymbol): SyntaxNode | null { return getNodeAtRange(root, symbol.range); } export function toTree(symbols: FishDocumentSymbol[]) { return new GenericTree<FishDocumentSymbol>(symbols); } export function debug(symbol: FishDocumentSymbol) { const positionString = (pos: Position) => `(line: ${pos.line}, char: ${pos.character})`; const rangeString = (n: SyntaxNode) => { const range = getRange(n); return `${positionString(range.start)} --- ${positionString(range.end)}`; }; const scopeNodeLines = symbol.scope.scopeNode.text.split('\n'); return { name: symbol.name, range: positionString(symbol.range.start) + ' --- ' + positionString(symbol.range.end), selectionRange: positionString(symbol.selectionRange.start) + ' --- ' + positionString(symbol.selectionRange.end), text: symbol.text.split('\n').length > 1 ? symbol.text + '...' : symbol.text, scope: { scopeTag: symbol.scope.scopeTag, scopeNode: { text: scopeNodeLines[0] + '...', type: symbol.scope.scopeNode.type, range: rangeString(symbol.scope.scopeNode), }, }, type: symbol.kind === SymbolKind.Function ? 'function' : 'variable', uri: symbol.uri, }; } export function toFoldingRange(symbol: FishDocumentSymbol): FoldingRange { return { startLine: symbol.range.start.line, endLine: symbol.range.end.line, collapsedText: symbol.name, }; //return FoldingRange.create( // symbol.range.start.line, // symbol.range.end.line, // symbol.range.start.character, // symbol.range.end.character, // FoldingRangeKind.Region, // symbol.name //) } //export function toGlobalCompletion(symbol: FishDocumentSymbol, data: FishCompletionData): FishCompletionItem { // const kind = symbol.kind === SymbolKind.Function ? FishCompletionItemKind.GLOBAL_FUNCTION : FishCompletionItemKind.GLOBAL_VARIABLE; // const detail: MarkupContent = {kind: 'markdown', value: symbol.detail} // return createCompletionItem(symbol.name, kind, detail, data) //} // //export function toLocalCompletion(symbol: FishDocumentSymbol, data: FishCompletionData): FishCompletionItem { // const kind = symbol.kind === SymbolKind.Function // ? isGlobalSymbol(symbol) ? FishCompletionItemKind.USER_FUNCTION : FishCompletionItemKind.LOCAL_FUNCTION // : isGlobalSymbol(symbol) ? FishCompletionItemKind.GLOBAL_VARIABLE : FishCompletionItemKind.LOCAL_VARIABLE; // const detail: MarkupContent = {kind: 'markdown', value: symbol.detail} // return createCompletionItem(symbol.name, kind, detail, data) //} export type MockSymbol = { name: string; scope: ScopeTag; range: Range; }; export function toMock(symbol: FishDocumentSymbol): MockSymbol { const { name, scope, range } = symbol; return { name, scope: scope.scopeTag, range, }; } export function createMock(name: string, scope: ScopeTag, range: Range): MockSymbol { return { name, scope, range, }; } } /** * Checks if a FishDocumentSymbol's state, should NOT be changeable. * Renaming a FishDocumentSymbol across the entire workspace, shouldn't * be possible for internal symbols (seen in '/usr/share/fish/**.fish'). */ export function symbolIsImmutable(symbol: FishDocumentSymbol): boolean { const { uri, scope } = symbol; return uri.startsWith('/usr/share/fish/') || scope.scopeTag === 'universal'; } export function isGlobalSymbol(symbol: FishDocumentSymbol): boolean { return symbol.scope.scopeTag === 'global'; } export function isUniversalSymbol(symbol: FishDocumentSymbol): boolean { return symbol.scope.scopeTag === 'universal'; } export function filterGlobalSymbols(symbols: FishDocumentSymbol[]): FishDocumentSymbol[] { return FishDocumentSymbol .toTree(symbols) .toFlatArray() .filter((symbol) => symbol.scope.scopeTag === 'global'); } export function filterLocalSymbols(symbols: FishDocumentSymbol[]): FishDocumentSymbol[] { return FishDocumentSymbol .toTree(symbols) .toFlatArray() .filter((symbol) => symbol.scope.scopeTag !== 'global' && symbol.scope.scopeTag !== 'universal'); } export function filterLastPerScopeSymbol(symbolArray: FishDocumentSymbol[]) { const symbolTree: GenericTree<FishDocumentSymbol> = new GenericTree(symbolArray); const flatArray: FishDocumentSymbol[] = symbolTree.toFlatArray(); return symbolTree .filterToTree((symbol: FishDocumentSymbol) => !flatArray.some((s) => { return ( s.name === symbol.name && !FishDocumentSymbol.equal(symbol, s) && FishDocumentSymbol.equalScopes(symbol, s) && FishDocumentSymbol.isBefore(symbol, s) ); })) .toArray(); } const compareSymbolToPosition = (symbol: FishDocumentSymbol, position: Position) => { const compareHelper = (symbol: FishDocumentSymbol, position: Position) => { const { scope } = symbol; if (['global', 'universal'].includes(scope.scopeTag)) { return true; } return scope.containsPosition(position); }; return symbol.kind === SymbolKind.Function ? compareHelper(symbol, position) : symbol.scope.containsPosition(position) && isPositionAfter(symbol.selectionRange.end, position); }; export function findSymbolsForCompletion(symbols: FishDocumentSymbol[], position: Position): FishDocumentSymbol[] { const symbolTree = new GenericTree<FishDocumentSymbol>(symbols); const possibleDuplicates = symbolTree .filterToTree((symbol: FishDocumentSymbol) => compareSymbolToPosition(symbol, position)) .toFlatArray() .reverse(); const uniqueSymbolsArray: FishDocumentSymbol[] = []; for (const symbol of possibleDuplicates) { if (uniqueSymbolsArray.some((s) => s.name === symbol.name)) { continue; } uniqueSymbolsArray.push(symbol); } return uniqueSymbolsArray; } /** * finds all symbols (variables and function that have been defined) */ export function findSymbolReferences(symbols: FishDocumentSymbol[], matchSymbol: FishDocumentSymbol): FishDocumentSymbol[] { return new GenericTree<FishDocumentSymbol>(symbols) .filterToTree((symbol: FishDocumentSymbol) => { //if (symbol.scope.scopeTag === 'global' ) return true; return matchSymbol.name === symbol.name && FishDocumentSymbol.equalScopes(matchSymbol, symbol); }) .toFlatArray(); } export function findLastDefinition(symbols: FishDocumentSymbol[], matchNode: SyntaxNode) { const symbolTree = new GenericTree<FishDocumentSymbol>(symbols); const symbolFunctionCompare = (symbol: FishDocumentSymbol, matchNode: SyntaxNode) => { const matchPosition = pointToPosition(matchNode.startPosition); const { name, kind: _kind, scope: _scope } = symbol; return name === matchNode.text && compareSymbolToPosition(symbol, matchPosition); }; return symbolTree .filterToTree((symbol: FishDocumentSymbol) => symbolFunctionCompare(symbol, matchNode)) .toFlatArray() .pop(); } /** * TreeSitter definition nodes in fish shell rely on commands, and thus create trees that * need specific traversals per command. Creates a standard object of properties to be * deconstructed into a FishDocumentSymbol. Where parent is the root most node of the * entire command to create a symbol. Child is the identifier of the symbol. * * See fish below: * --------------------------------------------------------------------------------------- * set -gx FOO BAR; # FOO is a variable we globally define and export * --------------------------------------------------------------------------------------- * Child is just the identifier `$FOO` * Parent is the entire string `set -gx FOO BAR;` for the command */ export function definitionSymbolHandler(node: SyntaxNode): { shouldCreate: boolean; kind: SymbolKind; child: SyntaxNode; parent: SyntaxNode; } { let shouldCreate = false; let [child, parent] = [node, node.parent || node]; let kind: SymbolKind = SymbolKind.Null; if (isVariableDefinitionName(node)) { parent = refinedFindParentVariableDefinitionKeyword(node)!.parent!; kind = SymbolKind.Variable; shouldCreate = true; if (node.text.startsWith('$')) { shouldCreate = false; } } else if (node.firstNamedChild && isFunctionDefinitionName(node.firstNamedChild)) { parent = node; child = node.firstNamedChild!; kind = SymbolKind.Function; shouldCreate = true; } return { shouldCreate, kind, child, parent, }; } /** * Creates all FishDocumentSymbols in a file * @param {string} uri - path to the file * @param {SyntaxNode[]} currentNodes - root node(s) to traverse for definitions * @returns {FishDocumentSymbol[]} - all defined FishDocumentSymbol's in file */ export function getFishDocumentSymbols(document: LspDocument, ...currentNodes: SyntaxNode[]): FishDocumentSymbol[] { const symbols: FishDocumentSymbol[] = []; for (const node of currentNodes) { const childrenSymbols = getFishDocumentSymbols(document, ...node.children); const { shouldCreate, kind, child, parent } = definitionSymbolHandler(node); if (shouldCreate) { symbols.push( FishDocumentSymbol.create( child.text, document.uri, parent.text, DocumentSymbolDetail.create(child.text, document.uri, kind, child), kind, getRange(parent), getRange(child), getScope(document, child), childrenSymbols, ), ); continue; } symbols.push(...childrenSymbols); } return symbols; }