From 2977a5def5616dbb01f7e1679aca7d4b45df5e45 Mon Sep 17 00:00:00 2001 From: skovhus Date: Tue, 24 Jan 2023 21:14:29 +0100 Subject: [PATCH] WIP --- server/src/__tests__/server.test.ts | 23 +++++++++++ server/src/analyser.ts | 12 +++++- server/src/server.ts | 63 +++++++++++++++++++++++++---- server/src/util/shebang.ts | 2 +- server/src/util/sourcing.ts | 7 ++-- 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/server/src/__tests__/server.test.ts b/server/src/__tests__/server.test.ts index 79241917b..84fd82848 100644 --- a/server/src/__tests__/server.test.ts +++ b/server/src/__tests__/server.test.ts @@ -616,6 +616,29 @@ describe('server', () => { ] `) }) + + it('responds to onCompletion with source suggestions', async () => { + const { connection } = await initializeServer({ rootPath: REPO_ROOT_FOLDER }) + + const onCompletion = connection.onCompletion.mock.calls[0][0] + + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.SOURCING, + }, + position: { + // after source + line: 2, + character: 8, + }, + }, + {} as any, + {} as any, + ) + + expect(result).toMatchInlineSnapshot(`Array []`) + }) }) describe('onCompletionResolve', () => { diff --git a/server/src/analyser.ts b/server/src/analyser.ts index 90c524f96..c88881765 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -269,7 +269,7 @@ export default class Analyzer { * It's currently not scope-aware, see findOccurrences. */ public findReferences(word: string): LSP.Location[] { - const uris = Object.keys(this.uriToAnalyzedDocument) + const uris = this.getAllUris() return flattenArray(uris.map((uri) => this.findOccurrences(uri, word))) } @@ -319,6 +319,10 @@ export default class Analyzer { return locations } + public getAllUris(): string[] { + return Object.keys(this.uriToAnalyzedDocument) + } + public getAllVariables({ position, uri, @@ -412,6 +416,10 @@ export default class Analyzer { } } + public getSourcedUris(uri: string): Set { + return this.uriToAnalyzedDocument[uri]?.sourcedUris || new Set([]) + } + /** * Find the name of the command at the given point. */ @@ -519,7 +527,7 @@ export default class Analyzer { // Private methods private getReachableUris({ uri: fromUri }: { uri?: string } = {}): string[] { if (!fromUri || this.includeAllWorkspaceSymbols) { - return Object.keys(this.uriToAnalyzedDocument) + return this.getAllUris() } const uris = [fromUri, ...Array.from(this.findAllSourcedUris({ uri: fromUri }))] diff --git a/server/src/server.ts b/server/src/server.ts index 53a8ef76d..430c85850 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -20,6 +20,7 @@ import { uniqueBasedOnHash } from './util/array' import { logger, setLogConnection, setLogLevel } from './util/logger' import { isPositionIncludedInRange } from './util/lsp' import { getShellDocumentation } from './util/sh' +import { SOURCING_COMMANDS } from './util/sourcing' const PARAMETER_EXPANSION_PREFIXES = new Set(['$', '${']) const CONFIGURATION_SECTION = 'bashIde' @@ -115,7 +116,8 @@ export default class BashServer { textDocumentSync: LSP.TextDocumentSyncKind.Full, completionProvider: { resolveProvider: true, - triggerCharacters: ['$', '{'], + // ' ' is needed for completion after a command (currently only for source) + triggerCharacters: ['$', '{', ' ', '.'], }, hoverProvider: true, documentHighlightProvider: true, @@ -423,12 +425,15 @@ export default class BashServer { } private onCompletion(params: LSP.TextDocumentPositionParams): BashCompletionItem[] { + const currentUri = params.textDocument.uri + const previousCharacterPosition = Math.max(params.position.character - 1, 0) + const word = this.analyzer.wordAtPointFromTextPosition({ ...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), + character: previousCharacterPosition, }, }) @@ -439,9 +444,53 @@ export default class BashServer { return [] } - if (word === '{') { - // We should not complete when it is not prefixed by a $. - // This case needs to be here as "{" is a completionProvider triggerCharacter. + if (word && ['{', '.'].includes(word)) { + // When the current word is a "{"" or a "." we should not complete. + // A valid completion word would be "${" or a "." command followed by an empty word. + return [] + } + + const commandNameBefore = this.analyzer.commandNameAtPoint( + params.textDocument.uri, + params.position.line, + // there might be a better way using the AST: + Math.max(params.position.character - 2, 0), + ) + console.log( + '>>> commandNameBefore', + commandNameBefore, + Math.max(params.position.character - 2, 0), + ) + const { workspaceFolder } = this + if ( + workspaceFolder && + commandNameBefore && + SOURCING_COMMANDS.includes(commandNameBefore) + ) { + const uris = this.analyzer + .getAllUris() + .filter((uri) => currentUri !== uri) + .map((uri) => uri.replace(workspaceFolder, '.').replace('file://', '')) + + if (uris) { + // TODO: remove qoutes if the user already typed them + // TODO: figure out the base path based on other source commands + return uris.map((uri) => { + return { + label: uri, + kind: LSP.CompletionItemKind.File, + data: { + type: CompletionItemDataType.Symbol, + }, + } + }) + } + } + + // TODO: maybe abort if commandNameBefore is a known command + if (word === ' ') { + // TODO: test this + // No command was found, so don't complete on space return [] } @@ -463,7 +512,7 @@ export default class BashServer { params.textDocument.uri, params.position.line, // Go one character back to get completion on the current word - Math.max(params.position.character - 1, 0), + previousCharacterPosition, ) if (commandName) { @@ -471,8 +520,6 @@ export default class BashServer { } } - const currentUri = params.textDocument.uri - // TODO: an improvement here would be to detect if the current word is // not only a parameter expansion prefix, but also if the word is actually // inside a parameter expansion (e.g. auto completing on a word $MY_VARIA). diff --git a/server/src/util/shebang.ts b/server/src/util/shebang.ts index 2e0e68330..88958b4f8 100644 --- a/server/src/util/shebang.ts +++ b/server/src/util/shebang.ts @@ -1,7 +1,7 @@ const SHEBANG_REGEXP = /^#!(.*)/ const SHELL_REGEXP = /bin[/](?:env )?(\w+)/ -const BASH_DIALECTS = ['sh', 'bash', 'dash', 'ksh'] as const +const BASH_DIALECTS = ['sh', 'bash', 'dash', 'ksh'] as const // why not try to parse zsh? And let treesitter determine if it is supported type SupportedBashDialect = (typeof BASH_DIALECTS)[number] export function getShebang(fileContent: string): string | null { diff --git a/server/src/util/sourcing.ts b/server/src/util/sourcing.ts index 640e97b42..0e1cee89a 100644 --- a/server/src/util/sourcing.ts +++ b/server/src/util/sourcing.ts @@ -6,7 +6,7 @@ import * as Parser from 'web-tree-sitter' import { untildify } from './fs' import * as TreeSitterUtil from './tree-sitter' -const SOURCING_COMMANDS = ['source', '.'] +export const SOURCING_COMMANDS = ['source', '.'] export type SourceCommand = { range: LSP.Range @@ -55,11 +55,12 @@ function getSourcedPathInfoFromNode({ }: { node: Parser.SyntaxNode }): null | { sourcedPath?: string; parseError?: string } { - if (node.type === 'command') { + if (node && node.type === 'command') { const [commandNameNode, argumentNode] = node.namedChildren if ( commandNameNode.type === 'command_name' && - SOURCING_COMMANDS.includes(commandNameNode.text) + SOURCING_COMMANDS.includes(commandNameNode.text) && + argumentNode ) { if (argumentNode.type === 'word') { return {