From 47319f8a3ebaaab87ade51d6ccaa4c41c831feca Mon Sep 17 00:00:00 2001 From: otreblan Date: Tue, 8 Jun 2021 19:28:35 -0500 Subject: [PATCH 01/13] Complete command line arguments --- server/src/analyser.ts | 38 ++++++++++++++++++++++++++++--- server/src/get-options.sh | 19 ++++++++++++++++ server/src/server.ts | 48 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100755 server/src/get-options.sh diff --git a/server/src/analyser.ts b/server/src/analyser.ts index 12986a677..5896f4c8a 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -367,9 +367,9 @@ export default class Analyzer { } /** - * Find the full word at the given point. + * Find the node at the given point. */ - public wordAtPoint(uri: string, line: number, column: number): string | null { + private nodeAtPoint(uri: string, line: number, column: number): Parser.SyntaxNode | null { const document = this.uriToTreeSitterTrees[uri] if (!document.rootNode) { @@ -377,7 +377,14 @@ export default class Analyzer { return null } - const node = document.rootNode.descendantForPosition({ row: line, column }) + return document.rootNode.descendantForPosition({ row: line, column }) + } + + /** + * Find the full word at the given point. + */ + public wordAtPoint(uri: string, line: number, column: number): string | null { + const node = this.nodeAtPoint(uri, line, column) if (!node || node.childCount > 0 || node.text.trim() === '') { return null @@ -386,6 +393,31 @@ export default class Analyzer { return node.text.trim() } + /** + * Find the name of the command at the given point. + */ + public commandNameAtPoint(uri: string, line: number, column: number): string | null { + const node = this.nodeAtPoint(uri, line, column) + + if (!node || node.childCount > 0 || node.text.trim() === '') { + return null + } + + const parent = node.parent + + if (!parent || parent.type !== 'command') { + return null + } + + const firstChild = parent.firstNamedChild + + if (!firstChild || firstChild.type !== 'command_name') { + return null + } + + return firstChild.text.trim() + } + /** * Find a block of comments above a line position */ diff --git a/server/src/get-options.sh b/server/src/get-options.sh new file mode 100755 index 000000000..abcb8721d --- /dev/null +++ b/server/src/get-options.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +source /usr/share/bash-completion/bash_completion + +COMP_LINE="$*" +COMP_WORDS=("$@") +COMP_CWORD="${#COMP_WORDS[@]}" +((COMP_CWORD--)) +COMP_POINT="${#COMP_LINE}" +COMP_WORDBREAKS='"'"'><=;|&(:" + +_command_offset 0 2> /dev/null + +if (( ${#COMPREPLY[@]} == 0 )) +then + _longopt "${COMP_WORDS[0]}" +fi + +printf "%s\t" "${COMPREPLY[@]}" diff --git a/server/src/server.ts b/server/src/server.ts index b597a4153..5a8f2c634 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,6 +1,8 @@ import * as path from 'path' import * as TurndownService from 'turndown' import * as LSP from 'vscode-languageserver' +import * as Process from 'child_process' +import * as Path from 'path' import { TextDocument } from 'vscode-languageserver-textdocument' import Analyzer from './analyser' @@ -123,6 +125,16 @@ export default class BashServer { ) } + private getCommandNameAtPoint( + params: LSP.ReferenceParams | LSP.TextDocumentPositionParams, + ): string | null { + return this.analyzer.commandNameAtPoint( + params.textDocument.uri, + params.position.line, + params.position.character, + ) + } + private logRequest({ request, params, @@ -323,6 +335,22 @@ export default class BashServer { return [] } + let options: string[] = [] + if (word && word.startsWith('-')) { + const commandName = this.getCommandNameAtPoint({ + ...params, + position: { + line: params.position.line, + // Go one character back to get completion on the current word + character: Math.max(params.position.character - 1, 0), + }, + }) + + if(commandName) { + options = getCommandOptions(commandName, word) + } + } + const currentUri = params.textDocument.uri // TODO: an improvement here would be to detect if the current word is @@ -385,11 +413,21 @@ export default class BashServer { }, })) + const optionsCompletions = options.map(option => ({ + label: option, + kind: LSP.SymbolKind.Interface, + data: { + name: option, + type: CompletionItemDataType.Symbol + }, + })) + const allCompletions = [ ...reservedWordsCompletions, ...symbolCompletions, ...programCompletions, ...builtinsCompletions, + ...optionsCompletions, ] if (word) { @@ -531,3 +569,13 @@ const getMarkdownContent = (documentation: string): LSP.MarkupContent => ({ // Passed as markdown for syntax highlighting kind: 'markdown' as const, }) + +function getCommandOptions(name: string, word: string): string[] { + // TODO: The options could be cached. + const options = Process.spawnSync( + Path.join(__dirname, '../src/get-options.sh'), + [name, word] + ) + + return options.stdout.toString().split('\t') +} From 43c3c93e5254270bd2235dbd86589ead537c40ef Mon Sep 17 00:00:00 2001 From: otreblan Date: Tue, 8 Jun 2021 19:49:52 -0500 Subject: [PATCH 02/13] Prettier --- server/src/analyser.ts | 46 ++++++++++++++------------ server/src/executables.ts | 4 +-- server/src/server.ts | 69 ++++++++++++++++++++------------------- 3 files changed, 63 insertions(+), 56 deletions(-) diff --git a/server/src/analyser.ts b/server/src/analyser.ts index 5896f4c8a..9ab2ed9f3 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -123,11 +123,11 @@ export default class Analyzer { */ public findDefinition(name: string): LSP.Location[] { const symbols: LSP.SymbolInformation[] = [] - Object.keys(this.uriToDeclarations).forEach(uri => { + Object.keys(this.uriToDeclarations).forEach((uri) => { const declarationNames = this.uriToDeclarations[uri][name] || [] - declarationNames.forEach(d => symbols.push(d)) + declarationNames.forEach((d) => symbols.push(d)) }) - return symbols.map(s => s.location) + return symbols.map((s) => s.location) } /** @@ -175,10 +175,7 @@ export default class Analyzer { // FIXME: type the response and unit test it const explainshellResponse = await request({ - uri: URI(endpoint) - .path('/api/explain') - .addQuery('cmd', cmd) - .toString(), + uri: URI(endpoint).path('/api/explain').addQuery('cmd', cmd).toString(), json: true, }) @@ -216,7 +213,7 @@ export default class Analyzer { */ public findReferences(name: string): LSP.Location[] { const uris = Object.keys(this.uriToTreeSitterTrees) - return flattenArray(uris.map(uri => this.findOccurrences(uri, name))) + return flattenArray(uris.map((uri) => this.findOccurrences(uri, name))) } /** @@ -229,7 +226,7 @@ export default class Analyzer { const locations: LSP.Location[] = [] - TreeSitterUtil.forEach(tree.rootNode, n => { + TreeSitterUtil.forEach(tree.rootNode, (n) => { let name: null | string = null let range: null | LSP.Range = null @@ -273,12 +270,12 @@ export default class Analyzer { }): LSP.SymbolInformation[] { const symbols: LSP.SymbolInformation[] = [] - Object.keys(this.uriToDeclarations).forEach(uri => { + Object.keys(this.uriToDeclarations).forEach((uri) => { const declarationsInFile = this.uriToDeclarations[uri] || {} - Object.keys(declarationsInFile).map(name => { + Object.keys(declarationsInFile).map((name) => { const match = exactMatch ? name === word : name.startsWith(word) if (match) { - declarationsInFile[name].forEach(symbol => symbols.push(symbol)) + declarationsInFile[name].forEach((symbol) => symbols.push(symbol)) } }) }) @@ -325,7 +322,10 @@ export default class Analyzer { const name = contents.slice(named.startIndex, named.endIndex) const namedDeclarations = this.uriToDeclarations[uri][name] || [] - const parent = TreeSitterUtil.findParent(n, p => p.type === 'function_definition') + const parent = TreeSitterUtil.findParent( + n, + (p) => p.type === 'function_definition', + ) const parentName = parent && parent.firstNamedChild ? contents.slice( @@ -369,7 +369,11 @@ export default class Analyzer { /** * Find the node at the given point. */ - private nodeAtPoint(uri: string, line: number, column: number): Parser.SyntaxNode | null { + private nodeAtPoint( + uri: string, + line: number, + column: number, + ): Parser.SyntaxNode | null { const document = this.uriToTreeSitterTrees[uri] if (!document.rootNode) { @@ -406,13 +410,13 @@ export default class Analyzer { const parent = node.parent if (!parent || parent.type !== 'command') { - return null + return null } const firstChild = parent.firstNamedChild if (!firstChild || firstChild.type !== 'command_name') { - return null + return null } return firstChild.text.trim() @@ -467,17 +471,19 @@ export default class Analyzer { } public getAllVariableSymbols(): LSP.SymbolInformation[] { - return this.getAllSymbols().filter(symbol => symbol.kind === LSP.SymbolKind.Variable) + return this.getAllSymbols().filter( + (symbol) => symbol.kind === LSP.SymbolKind.Variable, + ) } private getAllSymbols(): LSP.SymbolInformation[] { // NOTE: this could be cached, it takes < 1 ms to generate for a project with 250 bash files... const symbols: LSP.SymbolInformation[] = [] - Object.keys(this.uriToDeclarations).forEach(uri => { - Object.keys(this.uriToDeclarations[uri]).forEach(name => { + Object.keys(this.uriToDeclarations).forEach((uri) => { + Object.keys(this.uriToDeclarations[uri]).forEach((name) => { const declarationNames = this.uriToDeclarations[uri][name] || [] - declarationNames.forEach(d => symbols.push(d)) + declarationNames.forEach((d) => symbols.push(d)) }) }) diff --git a/server/src/executables.ts b/server/src/executables.ts index d41d5587c..ef6fea9f1 100644 --- a/server/src/executables.ts +++ b/server/src/executables.ts @@ -17,11 +17,11 @@ export default class Executables { */ public static fromPath(path: string): Promise { const paths = path.split(':') - const promises = paths.map(x => findExecutablesInPath(x)) + const promises = paths.map((x) => findExecutablesInPath(x)) return Promise.all(promises) .then(ArrayUtil.flatten) .then(ArrayUtil.uniq) - .then(executables => new Executables(executables)) + .then((executables) => new Executables(executables)) } private executables: Set diff --git a/server/src/server.ts b/server/src/server.ts index 5a8f2c634..47b2d8f8e 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -41,7 +41,7 @@ export default class BashServer { return Promise.all([ Executables.fromPath(PATH), Analyzer.fromRoot({ connection, rootPath, parser }), - ]).then(xs => { + ]).then((xs) => { const executables = xs[0] const analyzer = xs[1] return new BashServer(connection, executables, analyzer) @@ -72,7 +72,7 @@ export default class BashServer { // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. this.documents.listen(this.connection) - this.documents.onDidChangeContent(change => { + this.documents.onDidChangeContent((change) => { const { uri } = change.document const diagnostics = this.analyzer.analyze(uri, change.document) if (config.getHighlightParsingError()) { @@ -168,8 +168,9 @@ export default class BashServer { currentUri, symbolUri, )}${symbolDocumentation}` - : `${symbolKindToDescription(symbol.kind)} defined on line ${symbolStarLine + - 1}${symbolDocumentation}` + : `${symbolKindToDescription(symbol.kind)} defined on line ${ + symbolStarLine + 1 + }${symbolDocumentation}` } private getCompletionItemsForSymbols({ @@ -257,7 +258,7 @@ export default class BashServer { currentUri, }) // do not return hover referencing for the current line - .filter(symbol => symbol.location.range.start.line !== params.position.line) + .filter((symbol) => symbol.location.range.start.line !== params.position.line) .map((symbol: LSP.SymbolInformation) => this.getDocumentationForSymbol({ currentUri, symbol }), ) @@ -301,7 +302,7 @@ export default class BashServer { return this.analyzer .findOccurrences(params.textDocument.uri, word) - .map(n => ({ range: n.range })) + .map((n) => ({ range: n.range })) } private onReferences(params: LSP.ReferenceParams): LSP.Location[] | null { @@ -337,18 +338,18 @@ export default class BashServer { let options: string[] = [] if (word && word.startsWith('-')) { - const commandName = this.getCommandNameAtPoint({ - ...params, - position: { - line: params.position.line, - // Go one character back to get completion on the current word - character: Math.max(params.position.character - 1, 0), - }, - }) + const commandName = this.getCommandNameAtPoint({ + ...params, + position: { + line: params.position.line, + // Go one character back to get completion on the current word + character: Math.max(params.position.character - 1, 0), + }, + }) - if(commandName) { - options = getCommandOptions(commandName, word) - } + if (commandName) { + options = getCommandOptions(commandName, word) + } } const currentUri = params.textDocument.uri @@ -381,7 +382,7 @@ export default class BashServer { return symbolCompletions } - const reservedWordsCompletions = ReservedWords.LIST.map(reservedWord => ({ + const reservedWordsCompletions = ReservedWords.LIST.map((reservedWord) => ({ label: reservedWord, kind: LSP.SymbolKind.Interface, // ?? data: { @@ -392,8 +393,8 @@ export default class BashServer { const programCompletions = this.executables .list() - .filter(executable => !Builtins.isBuiltin(executable)) - .map(executable => { + .filter((executable) => !Builtins.isBuiltin(executable)) + .map((executable) => { return { label: executable, kind: LSP.SymbolKind.Function, @@ -404,7 +405,7 @@ export default class BashServer { } }) - const builtinsCompletions = Builtins.LIST.map(builtin => ({ + const builtinsCompletions = Builtins.LIST.map((builtin) => ({ label: builtin, kind: LSP.SymbolKind.Interface, // ?? data: { @@ -413,12 +414,12 @@ export default class BashServer { }, })) - const optionsCompletions = options.map(option => ({ + const optionsCompletions = options.map((option) => ({ label: option, kind: LSP.SymbolKind.Interface, data: { name: option, - type: CompletionItemDataType.Symbol + type: CompletionItemDataType.Symbol, }, })) @@ -432,7 +433,7 @@ export default class BashServer { if (word) { // Filter to only return suffixes of the current word - return allCompletions.filter(item => item.label.startsWith(word)) + return allCompletions.filter((item) => item.label.startsWith(word)) } return allCompletions @@ -485,15 +486,15 @@ function deduplicateSymbols({ const getSymbolId = ({ name, kind }: LSP.SymbolInformation) => `${name}${kind}` - const symbolsCurrentFile = symbols.filter(s => isCurrentFile(s)) + const symbolsCurrentFile = symbols.filter((s) => isCurrentFile(s)) const symbolsOtherFiles = symbols - .filter(s => !isCurrentFile(s)) + .filter((s) => !isCurrentFile(s)) // Remove identical symbols matching current file .filter( - symbolOtherFiles => + (symbolOtherFiles) => !symbolsCurrentFile.some( - symbolCurrentFile => + (symbolCurrentFile) => getSymbolId(symbolCurrentFile) === getSymbolId(symbolOtherFiles), ), ) @@ -571,11 +572,11 @@ const getMarkdownContent = (documentation: string): LSP.MarkupContent => ({ }) function getCommandOptions(name: string, word: string): string[] { - // TODO: The options could be cached. - const options = Process.spawnSync( - Path.join(__dirname, '../src/get-options.sh'), - [name, word] - ) + // TODO: The options could be cached. + const options = Process.spawnSync(Path.join(__dirname, '../src/get-options.sh'), [ + name, + word, + ]) - return options.stdout.toString().split('\t') + return options.stdout.toString().split('\t') } From 38351068336b30007801314ddb12393e5563fa02 Mon Sep 17 00:00:00 2001 From: otreblan Date: Tue, 8 Jun 2021 19:58:54 -0500 Subject: [PATCH 03/13] Eslint autofix --- server/src/analyser.ts | 38 ++++++++++++++++++-------------------- server/src/executables.ts | 4 ++-- server/src/server.ts | 37 ++++++++++++++++++------------------- 3 files changed, 38 insertions(+), 41 deletions(-) diff --git a/server/src/analyser.ts b/server/src/analyser.ts index 9ab2ed9f3..b8b930d78 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -123,11 +123,11 @@ export default class Analyzer { */ public findDefinition(name: string): LSP.Location[] { const symbols: LSP.SymbolInformation[] = [] - Object.keys(this.uriToDeclarations).forEach((uri) => { + Object.keys(this.uriToDeclarations).forEach(uri => { const declarationNames = this.uriToDeclarations[uri][name] || [] - declarationNames.forEach((d) => symbols.push(d)) + declarationNames.forEach(d => symbols.push(d)) }) - return symbols.map((s) => s.location) + return symbols.map(s => s.location) } /** @@ -175,7 +175,10 @@ export default class Analyzer { // FIXME: type the response and unit test it const explainshellResponse = await request({ - uri: URI(endpoint).path('/api/explain').addQuery('cmd', cmd).toString(), + uri: URI(endpoint) + .path('/api/explain') + .addQuery('cmd', cmd) + .toString(), json: true, }) @@ -213,7 +216,7 @@ export default class Analyzer { */ public findReferences(name: string): LSP.Location[] { const uris = Object.keys(this.uriToTreeSitterTrees) - return flattenArray(uris.map((uri) => this.findOccurrences(uri, name))) + return flattenArray(uris.map(uri => this.findOccurrences(uri, name))) } /** @@ -226,7 +229,7 @@ export default class Analyzer { const locations: LSP.Location[] = [] - TreeSitterUtil.forEach(tree.rootNode, (n) => { + TreeSitterUtil.forEach(tree.rootNode, n => { let name: null | string = null let range: null | LSP.Range = null @@ -270,12 +273,12 @@ export default class Analyzer { }): LSP.SymbolInformation[] { const symbols: LSP.SymbolInformation[] = [] - Object.keys(this.uriToDeclarations).forEach((uri) => { + Object.keys(this.uriToDeclarations).forEach(uri => { const declarationsInFile = this.uriToDeclarations[uri] || {} - Object.keys(declarationsInFile).map((name) => { + Object.keys(declarationsInFile).map(name => { const match = exactMatch ? name === word : name.startsWith(word) if (match) { - declarationsInFile[name].forEach((symbol) => symbols.push(symbol)) + declarationsInFile[name].forEach(symbol => symbols.push(symbol)) } }) }) @@ -322,10 +325,7 @@ export default class Analyzer { const name = contents.slice(named.startIndex, named.endIndex) const namedDeclarations = this.uriToDeclarations[uri][name] || [] - const parent = TreeSitterUtil.findParent( - n, - (p) => p.type === 'function_definition', - ) + const parent = TreeSitterUtil.findParent(n, p => p.type === 'function_definition') const parentName = parent && parent.firstNamedChild ? contents.slice( @@ -407,7 +407,7 @@ export default class Analyzer { return null } - const parent = node.parent + const { parent } = node if (!parent || parent.type !== 'command') { return null @@ -471,19 +471,17 @@ export default class Analyzer { } public getAllVariableSymbols(): LSP.SymbolInformation[] { - return this.getAllSymbols().filter( - (symbol) => symbol.kind === LSP.SymbolKind.Variable, - ) + return this.getAllSymbols().filter(symbol => symbol.kind === LSP.SymbolKind.Variable) } private getAllSymbols(): LSP.SymbolInformation[] { // NOTE: this could be cached, it takes < 1 ms to generate for a project with 250 bash files... const symbols: LSP.SymbolInformation[] = [] - Object.keys(this.uriToDeclarations).forEach((uri) => { - Object.keys(this.uriToDeclarations[uri]).forEach((name) => { + Object.keys(this.uriToDeclarations).forEach(uri => { + Object.keys(this.uriToDeclarations[uri]).forEach(name => { const declarationNames = this.uriToDeclarations[uri][name] || [] - declarationNames.forEach((d) => symbols.push(d)) + declarationNames.forEach(d => symbols.push(d)) }) }) diff --git a/server/src/executables.ts b/server/src/executables.ts index ef6fea9f1..d41d5587c 100644 --- a/server/src/executables.ts +++ b/server/src/executables.ts @@ -17,11 +17,11 @@ export default class Executables { */ public static fromPath(path: string): Promise { const paths = path.split(':') - const promises = paths.map((x) => findExecutablesInPath(x)) + const promises = paths.map(x => findExecutablesInPath(x)) return Promise.all(promises) .then(ArrayUtil.flatten) .then(ArrayUtil.uniq) - .then((executables) => new Executables(executables)) + .then(executables => new Executables(executables)) } private executables: Set diff --git a/server/src/server.ts b/server/src/server.ts index 47b2d8f8e..947de5ef3 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,8 +1,8 @@ +import * as Process from 'child_process' import * as path from 'path' +import * as Path from 'path' import * as TurndownService from 'turndown' import * as LSP from 'vscode-languageserver' -import * as Process from 'child_process' -import * as Path from 'path' import { TextDocument } from 'vscode-languageserver-textdocument' import Analyzer from './analyser' @@ -41,7 +41,7 @@ export default class BashServer { return Promise.all([ Executables.fromPath(PATH), Analyzer.fromRoot({ connection, rootPath, parser }), - ]).then((xs) => { + ]).then(xs => { const executables = xs[0] const analyzer = xs[1] return new BashServer(connection, executables, analyzer) @@ -72,7 +72,7 @@ export default class BashServer { // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. this.documents.listen(this.connection) - this.documents.onDidChangeContent((change) => { + this.documents.onDidChangeContent(change => { const { uri } = change.document const diagnostics = this.analyzer.analyze(uri, change.document) if (config.getHighlightParsingError()) { @@ -168,9 +168,8 @@ export default class BashServer { currentUri, symbolUri, )}${symbolDocumentation}` - : `${symbolKindToDescription(symbol.kind)} defined on line ${ - symbolStarLine + 1 - }${symbolDocumentation}` + : `${symbolKindToDescription(symbol.kind)} defined on line ${symbolStarLine + + 1}${symbolDocumentation}` } private getCompletionItemsForSymbols({ @@ -258,7 +257,7 @@ export default class BashServer { currentUri, }) // do not return hover referencing for the current line - .filter((symbol) => symbol.location.range.start.line !== params.position.line) + .filter(symbol => symbol.location.range.start.line !== params.position.line) .map((symbol: LSP.SymbolInformation) => this.getDocumentationForSymbol({ currentUri, symbol }), ) @@ -302,7 +301,7 @@ export default class BashServer { return this.analyzer .findOccurrences(params.textDocument.uri, word) - .map((n) => ({ range: n.range })) + .map(n => ({ range: n.range })) } private onReferences(params: LSP.ReferenceParams): LSP.Location[] | null { @@ -382,7 +381,7 @@ export default class BashServer { return symbolCompletions } - const reservedWordsCompletions = ReservedWords.LIST.map((reservedWord) => ({ + const reservedWordsCompletions = ReservedWords.LIST.map(reservedWord => ({ label: reservedWord, kind: LSP.SymbolKind.Interface, // ?? data: { @@ -393,8 +392,8 @@ export default class BashServer { const programCompletions = this.executables .list() - .filter((executable) => !Builtins.isBuiltin(executable)) - .map((executable) => { + .filter(executable => !Builtins.isBuiltin(executable)) + .map(executable => { return { label: executable, kind: LSP.SymbolKind.Function, @@ -405,7 +404,7 @@ export default class BashServer { } }) - const builtinsCompletions = Builtins.LIST.map((builtin) => ({ + const builtinsCompletions = Builtins.LIST.map(builtin => ({ label: builtin, kind: LSP.SymbolKind.Interface, // ?? data: { @@ -414,7 +413,7 @@ export default class BashServer { }, })) - const optionsCompletions = options.map((option) => ({ + const optionsCompletions = options.map(option => ({ label: option, kind: LSP.SymbolKind.Interface, data: { @@ -433,7 +432,7 @@ export default class BashServer { if (word) { // Filter to only return suffixes of the current word - return allCompletions.filter((item) => item.label.startsWith(word)) + return allCompletions.filter(item => item.label.startsWith(word)) } return allCompletions @@ -486,15 +485,15 @@ function deduplicateSymbols({ const getSymbolId = ({ name, kind }: LSP.SymbolInformation) => `${name}${kind}` - const symbolsCurrentFile = symbols.filter((s) => isCurrentFile(s)) + const symbolsCurrentFile = symbols.filter(s => isCurrentFile(s)) const symbolsOtherFiles = symbols - .filter((s) => !isCurrentFile(s)) + .filter(s => !isCurrentFile(s)) // Remove identical symbols matching current file .filter( - (symbolOtherFiles) => + symbolOtherFiles => !symbolsCurrentFile.some( - (symbolCurrentFile) => + symbolCurrentFile => getSymbolId(symbolCurrentFile) === getSymbolId(symbolOtherFiles), ), ) From 7d6916d8efe0adaa7173af31126ad1fd2b304f99 Mon Sep 17 00:00:00 2001 From: otreblan Date: Wed, 9 Jun 2021 06:59:09 -0500 Subject: [PATCH 04/13] Add commandNameAtPoint test --- server/src/__tests__/analyzer.test.ts | 18 ++++++++++++++++++ server/src/analyser.ts | 12 +++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/server/src/__tests__/analyzer.test.ts b/server/src/__tests__/analyzer.test.ts index 7520acc2b..c313b47b6 100644 --- a/server/src/__tests__/analyzer.test.ts +++ b/server/src/__tests__/analyzer.test.ts @@ -112,6 +112,24 @@ describe('wordAtPoint', () => { }) }) +describe('commandNameAtPoint', () => { + it('returns current command name at a given point', () => { + analyzer.analyze(CURRENT_URI, FIXTURES.INSTALL) + expect(analyzer.commandNameAtPoint(CURRENT_URI, 15, 0)).toEqual(null) + + expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 2)).toEqual('curl') + expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 15)).toEqual('curl') + expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 19)).toEqual('curl') + + expect(analyzer.commandNameAtPoint(CURRENT_URI, 26, 4)).toEqual('echo') + expect(analyzer.commandNameAtPoint(CURRENT_URI, 26, 9)).toEqual('echo') + + expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 13)).toEqual('env') + expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 24)).toEqual('grep') + expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 44)).toEqual('sed') + }) +}) + describe('findSymbolCompletions', () => { it('return a list of symbols across the workspace', () => { analyzer.analyze('install.sh', FIXTURES.INSTALL) diff --git a/server/src/analyser.ts b/server/src/analyser.ts index b8b930d78..7e4440247 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -401,19 +401,17 @@ export default class Analyzer { * Find the name of the command at the given point. */ public commandNameAtPoint(uri: string, line: number, column: number): string | null { - const node = this.nodeAtPoint(uri, line, column) + let node = this.nodeAtPoint(uri, line, column) - if (!node || node.childCount > 0 || node.text.trim() === '') { - return null + while (node && node.type !== 'command') { + node = node.parent } - const { parent } = node - - if (!parent || parent.type !== 'command') { + if (!node) { return null } - const firstChild = parent.firstNamedChild + const firstChild = node.firstNamedChild if (!firstChild || firstChild.type !== 'command_name') { return null From c2ec4920e1de2fd4b4e6098380619be6b3181930 Mon Sep 17 00:00:00 2001 From: otreblan Date: Wed, 9 Jun 2021 08:24:19 -0500 Subject: [PATCH 05/13] Test onCompletion for command options --- server/src/__tests__/analyzer.test.ts | 2 +- server/src/__tests__/server.test.ts | 35 +++++++++++++++++++++++++++ testing/fixtures.ts | 2 ++ testing/fixtures/options.sh | 3 +++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 testing/fixtures/options.sh diff --git a/server/src/__tests__/analyzer.test.ts b/server/src/__tests__/analyzer.test.ts index c313b47b6..d1f41a7da 100644 --- a/server/src/__tests__/analyzer.test.ts +++ b/server/src/__tests__/analyzer.test.ts @@ -268,7 +268,7 @@ describe('fromRoot', () => { expect(connection.window.showWarningMessage).not.toHaveBeenCalled() // if you add a .sh file to testing/fixtures, update this value - const FIXTURE_FILES_MATCHING_GLOB = 11 + const FIXTURE_FILES_MATCHING_GLOB = 12 // Intro, stats on glob, one file skipped due to shebang, and outro const LOG_LINES = FIXTURE_FILES_MATCHING_GLOB + 4 diff --git a/server/src/__tests__/server.test.ts b/server/src/__tests__/server.test.ts index 6dd4cd0ba..50eda7f2b 100644 --- a/server/src/__tests__/server.test.ts +++ b/server/src/__tests__/server.test.ts @@ -267,6 +267,41 @@ describe('server', () => { ) }) + it('responds to onCompletion with options list when command name is found', async () => { + const { connection, server } = await initializeServer() + server.register(connection) + + const onCompletion = connection.onCompletion.mock.calls[0][0] + + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.OPTIONS, + }, + position: { + // grep --line- + line: 2, + character: 12, + }, + }, + {} as any, + {} as any, + ) + + expect(result).toEqual( + expect.arrayContaining([ + { + data: { + name: expect.stringMatching(RegExp('--line-.*')), + type: CompletionItemDataType.Symbol, + }, + kind: expect.any(Number), + label: expect.stringMatching(RegExp('--line-.*')), + }, + ]), + ) + }) + it('responds to onCompletion with entire list when no word is found', async () => { const { connection, server } = await initializeServer() server.register(connection) diff --git a/testing/fixtures.ts b/testing/fixtures.ts index 7f71c04c7..09be8e85e 100644 --- a/testing/fixtures.ts +++ b/testing/fixtures.ts @@ -21,6 +21,7 @@ export const FIXTURE_URI = { PARSE_PROBLEMS: `file://${path.join(FIXTURE_FOLDER, 'parse-problems.sh')}`, SOURCING: `file://${path.join(FIXTURE_FOLDER, 'sourcing.sh')}`, COMMENT_DOC: `file://${path.join(FIXTURE_FOLDER, 'comment-doc-on-hover.sh')}`, + OPTIONS: `file://${path.join(FIXTURE_FOLDER, 'options.sh')}`, } export const FIXTURE_DOCUMENT = { @@ -30,6 +31,7 @@ export const FIXTURE_DOCUMENT = { PARSE_PROBLEMS: getDocument(FIXTURE_URI.PARSE_PROBLEMS), SOURCING: getDocument(FIXTURE_URI.SOURCING), COMMENT_DOC: getDocument(FIXTURE_URI.COMMENT_DOC), + OPTIONS: getDocument(FIXTURE_URI.OPTIONS), } export default FIXTURE_DOCUMENT diff --git a/testing/fixtures/options.sh b/testing/fixtures/options.sh new file mode 100644 index 000000000..39da180a6 --- /dev/null +++ b/testing/fixtures/options.sh @@ -0,0 +1,3 @@ +grep - +grep -- +grep --line- From f95652689eee4db4b2e09e21f02748ff1cc2e846 Mon Sep 17 00:00:00 2001 From: Alberto Oporto Ames Date: Sun, 13 Jun 2021 17:19:30 -0500 Subject: [PATCH 06/13] Trim and filter results Co-authored-by: Kenneth Skovhus --- server/src/server.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/server.ts b/server/src/server.ts index 947de5ef3..ebbd0229b 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -577,5 +577,9 @@ function getCommandOptions(name: string, word: string): string[] { word, ]) - return options.stdout.toString().split('\t') + return options.stdout + .toString() + .split('\t') + .map(l => l.trim()) + .filter(l => l.length > 0) } From 25da9ac36291951333eff8adf7da0d7b83a72f48 Mon Sep 17 00:00:00 2001 From: otreblan Date: Sun, 13 Jun 2021 17:25:15 -0500 Subject: [PATCH 07/13] Generic way of getting the path to bash_completion --- server/src/get-options.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/get-options.sh b/server/src/get-options.sh index abcb8721d..f45cf472b 100755 --- a/server/src/get-options.sh +++ b/server/src/get-options.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash -source /usr/share/bash-completion/bash_completion +DATADIR="$(pkg-config --variable=datadir bash-completion)" + +source "$DATADIR/bash-completion/bash_completion" COMP_LINE="$*" COMP_WORDS=("$@") From 0206cb5dd8a8ee6179fcac8e14c9c45fa361ab20 Mon Sep 17 00:00:00 2001 From: otreblan Date: Sun, 13 Jun 2021 17:31:02 -0500 Subject: [PATCH 08/13] set -uo pipefail --- server/src/get-options.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/get-options.sh b/server/src/get-options.sh index f45cf472b..5e969907e 100755 --- a/server/src/get-options.sh +++ b/server/src/get-options.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -uo pipefail + DATADIR="$(pkg-config --variable=datadir bash-completion)" source "$DATADIR/bash-completion/bash_completion" From 54527cf5c3dc9b6d0751859ac2c0ddb5bf2b6534 Mon Sep 17 00:00:00 2001 From: otreblan Date: Sun, 13 Jun 2021 17:56:20 -0500 Subject: [PATCH 09/13] No pipefail --- server/src/get-options.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/get-options.sh b/server/src/get-options.sh index 5e969907e..0f5025704 100755 --- a/server/src/get-options.sh +++ b/server/src/get-options.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -uo pipefail +set -u DATADIR="$(pkg-config --variable=datadir bash-completion)" From d8b2adc8678493d197e4be273a01566f8ae2f70a Mon Sep 17 00:00:00 2001 From: otreblan Date: Sun, 13 Jun 2021 18:10:01 -0500 Subject: [PATCH 10/13] No set --- server/src/get-options.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/get-options.sh b/server/src/get-options.sh index 0f5025704..f45cf472b 100755 --- a/server/src/get-options.sh +++ b/server/src/get-options.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -set -u - DATADIR="$(pkg-config --variable=datadir bash-completion)" source "$DATADIR/bash-completion/bash_completion" From f902cf4b26885398e992591417a1de384cbe8e4d Mon Sep 17 00:00:00 2001 From: otreblan Date: Sun, 13 Jun 2021 18:18:46 -0500 Subject: [PATCH 11/13] Disable _longopt by default --- server/src/get-options.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/get-options.sh b/server/src/get-options.sh index f45cf472b..d429832a3 100755 --- a/server/src/get-options.sh +++ b/server/src/get-options.sh @@ -15,7 +15,12 @@ _command_offset 0 2> /dev/null if (( ${#COMPREPLY[@]} == 0 )) then - _longopt "${COMP_WORDS[0]}" + # Disabled by default because _longopt executes the program + # to get its options. + if (( ${BASH_LSP_COMPLETE_LONGOPTS} == 1 )) + then + _longopt "${COMP_WORDS[0]}" + fi fi printf "%s\t" "${COMPREPLY[@]}" From 682de8a62e19553be093be0f1620a44f5c4bc2ef Mon Sep 17 00:00:00 2001 From: otreblan Date: Sun, 13 Jun 2021 18:35:49 -0500 Subject: [PATCH 12/13] Return an empty array when get-completions fails --- server/src/server.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/server.ts b/server/src/server.ts index ebbd0229b..05dac3ef5 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -577,6 +577,10 @@ function getCommandOptions(name: string, word: string): string[] { word, ]) + if (options.status !== 0) { + return [] + } + return options.stdout .toString() .split('\t') From 3cff08ddbe37c19ee2d12d64d3be4729d9ed9d2f Mon Sep 17 00:00:00 2001 From: otreblan Date: Sun, 4 Jul 2021 14:48:11 -0500 Subject: [PATCH 13/13] Exit get-options.sh if bash-completion isn't installed --- server/src/get-options.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/get-options.sh b/server/src/get-options.sh index d429832a3..46e26a28a 100755 --- a/server/src/get-options.sh +++ b/server/src/get-options.sh @@ -2,6 +2,12 @@ DATADIR="$(pkg-config --variable=datadir bash-completion)" +# Exit if bash-completion isn't installed. +if (( $? != 0 )) +then + exit 1 +fi + source "$DATADIR/bash-completion/bash_completion" COMP_LINE="$*"