diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md index 405fa84cd..55a89195b 100644 --- a/server/CHANGELOG.md +++ b/server/CHANGELOG.md @@ -1,5 +1,10 @@ # Bash Language Server +## 4.5.2 + +- fixed `onReferences` to respect the `context.includeDeclaration` flag +- removed unnecessary dependency `urijs` + ## 4.5.1 - Include grouped variables and functions when finding global declarations https://github.com/bash-lsp/bash-language-server/pull/685 diff --git a/server/package.json b/server/package.json index 9bf2a9838..5b03bf963 100644 --- a/server/package.json +++ b/server/package.json @@ -3,7 +3,7 @@ "description": "A language server for Bash", "author": "Mads Hartmann", "license": "MIT", - "version": "4.5.1", + "version": "4.5.2", "main": "./out/server.js", "typings": "./out/server.d.ts", "bin": { @@ -21,7 +21,6 @@ "fuzzy-search": "3.2.1", "node-fetch": "2.6.8", "turndown": "7.1.1", - "urijs": "1.19.11", "vscode-languageserver": "8.0.2", "vscode-languageserver-textdocument": "1.0.8", "web-tree-sitter": "0.20.7", diff --git a/server/src/__tests__/__snapshots__/server.test.ts.snap b/server/src/__tests__/__snapshots__/server.test.ts.snap deleted file mode 100644 index 82b7a1250..000000000 --- a/server/src/__tests__/__snapshots__/server.test.ts.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`server initializes and responds to capabilities 1`] = ` -Object { - "codeActionProvider": Object { - "codeActionKinds": Array [ - "quickfix", - ], - "resolveProvider": false, - "workDoneProgress": false, - }, - "completionProvider": Object { - "resolveProvider": true, - "triggerCharacters": Array [ - "$", - "{", - ], - }, - "definitionProvider": true, - "documentHighlightProvider": true, - "documentSymbolProvider": true, - "hoverProvider": true, - "referencesProvider": true, - "textDocumentSync": 1, - "workspaceSymbolProvider": true, -} -`; diff --git a/server/src/__tests__/server.test.ts b/server/src/__tests__/server.test.ts index 44c2e095e..79241917b 100644 --- a/server/src/__tests__/server.test.ts +++ b/server/src/__tests__/server.test.ts @@ -10,6 +10,7 @@ import { FIXTURE_FOLDER, FIXTURE_URI, REPO_ROOT_FOLDER, + updateSnapshotUris, } from '../../../testing/fixtures' import { getMockConnection } from '../../../testing/mocks' import LspServer from '../server' @@ -26,21 +27,32 @@ jest.spyOn(Logger.prototype, 'log').mockImplementation(() => { // noop }) -async function initializeServer( - { rootPath }: { rootPath?: string } = { rootPath: pathToFileURL(FIXTURE_FOLDER).href }, -) { +async function initializeServer({ + capabilities, + configurationObject, + rootPath, +}: { + capabilities?: LSP.ClientCapabilities + configurationObject?: unknown + rootPath?: string +} = {}) { const diagnostics: Array = [] const connection = getMockConnection() const server = await LspServer.initialize(connection, { - rootPath, + rootPath: rootPath || pathToFileURL(FIXTURE_FOLDER).href, rootUri: null, processId: 42, - capabilities: {} as any, + capabilities: capabilities || {}, workspaceFolders: null, }) + if (configurationObject) { + const getConfiguration = connection.workspace.getConfiguration as any + getConfiguration.mockResolvedValue(configurationObject) + } + server.register(connection) const onInitialized = connection.onInitialized.mock.calls[0][0] const { backgroundAnalysisCompleted } = (await onInitialized({})) as any @@ -57,914 +69,1270 @@ async function initializeServer( describe('server', () => { it('initializes and responds to capabilities', async () => { const { server } = await initializeServer() - expect(server.capabilities()).toMatchSnapshot() + expect(server.capabilities()).toMatchInlineSnapshot(` + Object { + "codeActionProvider": Object { + "codeActionKinds": Array [ + "quickfix", + ], + "resolveProvider": false, + "workDoneProgress": false, + }, + "completionProvider": Object { + "resolveProvider": true, + "triggerCharacters": Array [ + "$", + "{", + ], + }, + "definitionProvider": true, + "documentHighlightProvider": true, + "documentSymbolProvider": true, + "hoverProvider": true, + "referencesProvider": true, + "textDocumentSync": 1, + "workspaceSymbolProvider": true, + } + `) }) it('register LSP connection', async () => { const { connection } = await initializeServer() - expect(connection.onHover).toHaveBeenCalledTimes(1) + expect(connection.onCodeAction).toHaveBeenCalledTimes(1) + expect(connection.onCompletion).toHaveBeenCalledTimes(1) + expect(connection.onCompletionResolve).toHaveBeenCalledTimes(1) expect(connection.onDefinition).toHaveBeenCalledTimes(1) - expect(connection.onDocumentSymbol).toHaveBeenCalledTimes(1) - expect(connection.onWorkspaceSymbol).toHaveBeenCalledTimes(1) expect(connection.onDocumentHighlight).toHaveBeenCalledTimes(1) + expect(connection.onDocumentSymbol).toHaveBeenCalledTimes(1) + expect(connection.onHover).toHaveBeenCalledTimes(1) expect(connection.onReferences).toHaveBeenCalledTimes(1) - expect(connection.onCompletion).toHaveBeenCalledTimes(1) - expect(connection.onCompletionResolve).toHaveBeenCalledTimes(1) - expect(connection.onCodeAction).toHaveBeenCalledTimes(1) + expect(connection.onWorkspaceSymbol).toHaveBeenCalledTimes(1) }) - it('responds to onHover', async () => { - const { connection } = await initializeServer() - - const onHover = connection.onHover.mock.calls[0][0] - - const result = await onHover( - { - textDocument: { - uri: FIXTURE_URI.INSTALL, - }, - position: { - line: 25, - character: 5, + it('allows for defining workspace configuration', async () => { + const { connection } = await initializeServer({ + capabilities: { + workspace: { + configuration: true, }, }, - {} as any, - {} as any, - ) - - expect(result).toBeDefined() - expect(result).toEqual({ - contents: { - kind: 'markdown', - value: expect.stringContaining('remove directories'), + configurationObject: { + explainshellEndpoint: 'foo', }, }) + + expect(connection.workspace.getConfiguration).toHaveBeenCalled() + expect(Logger.prototype.log).not.toHaveBeenCalledWith(expect.any(Number), [ + expect.stringContaining('updateConfiguration: failed'), + ]) }) - it('responds to onHover with function documentation extracted from comments', async () => { - const { connection } = await initializeServer() + it('ignores invalid workspace configuration', async () => { + const { connection } = await initializeServer({ + capabilities: { + workspace: { + configuration: true, + }, + }, + configurationObject: { + explainshellEndpoint: 42, + }, + }) - const onHover = connection.onHover.mock.calls[0][0] + expect(connection.workspace.getConfiguration).toHaveBeenCalled() + expect(Logger.prototype.log).toHaveBeenCalledWith(expect.any(Number), [ + expect.stringContaining('updateConfiguration: failed'), + ]) + }) - const result = await onHover( - { - textDocument: { - uri: FIXTURE_URI.COMMENT_DOC, - }, - position: { - line: 17, - character: 0, + it('responds to onDidChangeConfiguration', async () => { + const { connection } = await initializeServer({ + capabilities: { + workspace: { + configuration: true, }, }, - {} as any, - {} as any, - ) + }) - expect(result).toBeDefined() - expect(result).toMatchInlineSnapshot(` - Object { - "contents": Object { - "kind": "markdown", - "value": "Function: **hello_world** - *defined on line 8* - - \`\`\`txt - this is a comment - describing the function - hello_world - this function takes two arguments - \`\`\`", - }, - } - `) - }) + const onDidChangeConfiguration = connection.onDidChangeConfiguration.mock.calls[0][0] - it('responds to onDefinition', async () => { - const { connection } = await initializeServer() + onDidChangeConfiguration({ settings: { bashIde: { explainshellEndpoint: 42 } } }) - const onDefinition = connection.onDefinition.mock.calls[0][0] + expect(connection.workspace.getConfiguration).toHaveBeenCalled() + expect(Logger.prototype.log).toHaveBeenCalledWith(expect.any(Number), [ + expect.stringContaining('updateConfiguration: failed'), + ]) + }) - const result = await onDefinition( - { - textDocument: { - uri: FIXTURE_URI.SOURCING, - }, - position: { character: 10, line: 2 }, - }, - {} as any, - {} as any, - ) - - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "range": Object { - "end": Object { - "character": 0, - "line": 0, - }, - "start": Object { - "character": 0, - "line": 0, - }, + describe('onCodeAction', () => { + it('responds to onCodeAction', async () => { + const { connection, server } = await initializeServer() + const document = FIXTURE_DOCUMENT.COMMENT_DOC + + await server.analyzeAndLintDocument(document) + + expect(connection.sendDiagnostics).toHaveBeenCalledTimes(1) + const { diagnostics } = connection.sendDiagnostics.mock.calls[0][0] + const fixableDiagnostic = diagnostics.filter(({ code }) => code === 'SC2086')[0] + + expect(fixableDiagnostic).toMatchInlineSnapshot(` + Object { + "code": "SC2086", + "codeDescription": Object { + "href": "https://www.shellcheck.net/wiki/SC2086", + }, + "message": "Double quote to prevent globbing and word splitting.", + "range": Object { + "end": Object { + "character": 13, + "line": 55, + }, + "start": Object { + "character": 5, + "line": 55, + }, + }, + "severity": 3, + "source": "shellcheck", + "tags": undefined, + } + `) + + // TODO: we could find the diagnostics and then use the range to test the code action + + const onCodeAction = connection.onCodeAction.mock.calls[0][0] + + const result = await onCodeAction( + { + textDocument: { + uri: FIXTURE_URI.COMMENT_DOC, + }, + range: fixableDiagnostic.range, + context: { + diagnostics: [fixableDiagnostic], }, - "uri": "file://${FIXTURE_FOLDER}extension.inc", }, - ] - `) + {} as any, + {} as any, + ) + + expect(result).toHaveLength(1) + const codeAction = (result as CodeAction[])[0] + expect(codeAction.diagnostics).toEqual([fixableDiagnostic]) + expect(codeAction.diagnostics).toEqual([fixableDiagnostic]) + + expect( + codeAction.edit?.changes && codeAction.edit?.changes[FIXTURE_URI.COMMENT_DOC], + ).toMatchInlineSnapshot(` + Array [ + Object { + "newText": "\\"", + "range": Object { + "end": Object { + "character": 13, + "line": 55, + }, + "start": Object { + "character": 13, + "line": 55, + }, + }, + }, + Object { + "newText": "\\"", + "range": Object { + "end": Object { + "character": 5, + "line": 55, + }, + "start": Object { + "character": 5, + "line": 55, + }, + }, + }, + ] + `) + }) }) - it('responds to onDocumentSymbol', async () => { - const { connection } = await initializeServer() + describe('onCompletion', () => { + it('responds to onCompletion with filtered list when word is found', async () => { + const { connection } = await initializeServer() - const onDocumentSymbol = connection.onDocumentSymbol.mock.calls[0][0] + const onCompletion = connection.onCompletion.mock.calls[0][0] - const result = await onDocumentSymbol( - { - textDocument: { - uri: FIXTURE_URI.SOURCING, - }, - }, - {} as any, - {} as any, - ) - - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "kind": 13, - "location": Object { - "range": Object { - "end": Object { - "character": 16, - "line": 10, - }, - "start": Object { - "character": 0, - "line": 10, - }, - }, - "uri": "file://${FIXTURE_FOLDER}sourcing.sh", + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.INSTALL, + }, + position: { + // rm + line: 25, + character: 5, }, - "name": "BOLD", }, - Object { - "kind": 12, - "location": Object { - "range": Object { - "end": Object { - "character": 1, - "line": 22, - }, - "start": Object { - "character": 0, - "line": 20, - }, + {} as any, + {} as any, + ) + + // Limited set (not using snapshot due to different executables on CI and locally) + expect(result && 'length' in result && result.length < 8).toBe(true) + expect(result).toEqual( + expect.arrayContaining([ + { + data: { + type: CompletionItemDataType.Executable, }, - "uri": "file://${FIXTURE_FOLDER}sourcing.sh", + kind: expect.any(Number), + label: 'rm', }, - "name": "loadlib", - }, - ] - `) - }) + ]), + ) + }) - it('responds to onDocumentHighlight', async () => { - const { connection } = await initializeServer() + it('responds to onCompletion with options list when command name is found', async () => { + // This doesn't work on all hosts: + const getOptionsResult = Process.spawnSync( + Path.join(__dirname, '../src/get-options.sh'), + ['find', '-'], + ) - const onDocumentHighlight = connection.onDocumentHighlight.mock.calls[0][0] + if (getOptionsResult.status !== 0) { + // eslint-disable-next-line no-console + console.warn('Skipping onCompletion test as get-options.sh failed') + return + } - const result1 = await onDocumentHighlight( - { - textDocument: { - uri: FIXTURE_URI.ISSUE206, - }, - position: { - // FOO - line: 0, - character: 10, - }, - }, - {} as any, - {} as any, - ) - - expect(result1).toMatchInlineSnapshot(` - Array [ - Object { - "range": Object { - "end": Object { - "character": 12, - "line": 0, - }, - "start": Object { - "character": 9, - "line": 0, - }, + const { connection } = await initializeServer() + + const onCompletion = connection.onCompletion.mock.calls[0][0] + + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.OPTIONS, + }, + position: { + // grep --line- + line: 2, + character: 12, }, }, - Object { - "range": Object { - "end": Object { - "character": 28, - "line": 1, - }, - "start": Object { - "character": 25, - "line": 1, + {} 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-.*')), }, - }, - ] - `) + ]), + ) + }) - const result2 = await onDocumentHighlight( - { - textDocument: { - uri: FIXTURE_URI.ISSUE206, - }, - position: { - // readonly cannot be parsed as a word - line: 0, - character: 0, - }, - }, - {} as any, - {} as any, - ) + it('responds to onCompletion with entire list when no word is found', async () => { + const { connection } = await initializeServer() - expect(result2).toMatchInlineSnapshot(`Array []`) + const onCompletion = connection.onCompletion.mock.calls[0][0] - const result3 = await onDocumentHighlight( - { - textDocument: { - uri: FIXTURE_URI.SCOPE, - }, - position: { - // X - line: 32, - character: 8, - }, - }, - {} as any, - {} as any, - ) - - expect(result3).toMatchInlineSnapshot(` - Array [ - Object { - "range": Object { - "end": Object { - "character": 1, - "line": 2, - }, - "start": Object { - "character": 0, - "line": 2, - }, + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.INSTALL, }, - }, - Object { - "range": Object { - "end": Object { - "character": 1, - "line": 4, - }, - "start": Object { - "character": 0, - "line": 4, - }, + position: { + // empty space + line: 26, + character: 0, }, }, - Object { - "range": Object { - "end": Object { - "character": 9, - "line": 8, - }, - "start": Object { - "character": 8, - "line": 8, - }, + {} as any, + {} as any, + ) + + // Entire list + expect(result && 'length' in result && result.length).toBeGreaterThanOrEqual(50) + }) + + it('responds to onCompletion with empty list when the following characters is not an empty string or whitespace', async () => { + const { connection } = await initializeServer() + + const onCompletion = connection.onCompletion.mock.calls[0][0] + + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.INSTALL, }, - }, - Object { - "range": Object { - "end": Object { - "character": 11, - "line": 12, - }, - "start": Object { - "character": 10, - "line": 12, - }, + position: { + // { + line: 271, + character: 21, }, }, - Object { - "range": Object { - "end": Object { - "character": 13, - "line": 15, - }, - "start": Object { - "character": 12, - "line": 15, - }, + {} as any, + {} as any, + ) + + expect(result).toEqual([]) + }) + + it('responds to onCompletion with empty list when word is a comment', async () => { + const { connection } = await initializeServer() + + const onCompletion = connection.onCompletion.mock.calls[0][0] + + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.INSTALL, }, - }, - Object { - "range": Object { - "end": Object { - "character": 13, - "line": 19, - }, - "start": Object { - "character": 12, - "line": 19, - }, + position: { + // inside comment + line: 2, + character: 1, }, }, - Object { - "range": Object { - "end": Object { - "character": 15, - "line": 20, - }, - "start": Object { - "character": 14, - "line": 20, - }, + {} as any, + {} as any, + ) + + expect(result).toEqual([]) + }) + + it('responds to onCompletion with empty list when word is {', async () => { + const { connection } = await initializeServer() + + const onCompletion = connection.onCompletion.mock.calls[0][0] + + const result = await onCompletion( + { + textDocument: { + uri: FIXTURE_URI.ISSUE101, }, - }, - Object { - "range": Object { - "end": Object { - "character": 11, - "line": 29, - }, - "start": Object { - "character": 10, - "line": 29, - }, + position: { + // the opening brace '{' to 'add_a_user' + line: 4, + character: 0, }, }, - Object { - "range": Object { - "end": Object { - "character": 9, - "line": 32, - }, - "start": Object { - "character": 8, - "line": 32, - }, - }, - }, - ] - `) - }) + {} as any, + {} as any, + ) - it('responds to onWorkspaceSymbol', async () => { - const { connection } = await initializeServer() + expect(result).toEqual([]) + }) - const onWorkspaceSymbol = connection.onWorkspaceSymbol.mock.calls[0][0] + it('responds to onCompletion when word is found in another file', async () => { + const { connection } = await initializeServer() - async function lookupAndExpectNpmConfigLoglevelResult(query: string) { - const result = await onWorkspaceSymbol( + const onCompletion = connection.onCompletion.mock.calls[0][0] + + const resultVariable = await onCompletion( { - query, + textDocument: { + uri: FIXTURE_URI.SOURCING, + }, + position: { + // $BLU (variable) + line: 6, + character: 7, + }, }, {} as any, {} as any, ) - expect(result).toEqual([ + expect(resultVariable).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object { + "type": 3, + }, + "documentation": Object { + "kind": "markdown", + "value": "Variable: **BLUE** - *defined in extension.inc*", + }, + "kind": 6, + "label": "BLUE", + }, + ] + `) + + const resultFunction = await onCompletion( { - kind: expect.any(Number), - location: { - range: { - end: { character: 27, line: 40 }, - start: { character: 0, line: 40 }, - }, - uri: expect.stringContaining('/testing/fixtures/install.sh'), + textDocument: { + uri: FIXTURE_URI.SOURCING, + }, + position: { + // add_a_us (function) + line: 8, + character: 7, }, - name: 'npm_config_loglevel', }, - ]) - } + {} as any, + {} as any, + ) - await lookupAndExpectNpmConfigLoglevelResult('npm_config_loglevel') // exact - await lookupAndExpectNpmConfigLoglevelResult('config_log') // in the middle - await lookupAndExpectNpmConfigLoglevelResult('npmloglevel') // fuzzy - }) + expect(resultFunction).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object { + "type": 3, + }, + "documentation": Object { + "kind": "markdown", + "value": "Function: **add_a_user** - *defined in issue101.sh* + + \`\`\`txt + Helper function to add a user + \`\`\`", + }, + "kind": 3, + "label": "add_a_user", + }, + ] + `) + }) - it('responds to onCompletion with filtered list when word is found', async () => { - const { connection } = await initializeServer() + it('responds to onCompletion with local symbol when word is found in multiple files', async () => { + const { connection } = await initializeServer() - const onCompletion = connection.onCompletion.mock.calls[0][0] + const onCompletion = connection.onCompletion.mock.calls[0][0] - const result = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.INSTALL, - }, - position: { - // rm - line: 25, - character: 5, - }, - }, - {} as any, - {} as any, - ) - - // Limited set (not using snapshot due to different executables on CI and locally) - expect(result && 'length' in result && result.length < 8).toBe(true) - expect(result).toEqual( - expect.arrayContaining([ + const result = await onCompletion( { - data: { - type: CompletionItemDataType.Executable, + textDocument: { + uri: FIXTURE_URI.SOURCING, + }, + position: { + // BOL (BOLD is defined in multiple places) + line: 12, + character: 7, }, - kind: expect.any(Number), - label: 'rm', }, - ]), - ) - }) - - it('responds to onCompletion with options list when command name is found', async () => { - // This doesn't work on all hosts: - const getOptionsResult = Process.spawnSync( - Path.join(__dirname, '../src/get-options.sh'), - ['find', '-'], - ) - - if (getOptionsResult.status !== 0) { - // eslint-disable-next-line no-console - console.warn('Skipping onCompletion test as get-options.sh failed') - return - } + {} as any, + {} as any, + ) - const { connection } = await initializeServer() + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object { + "type": 3, + }, + "documentation": undefined, + "kind": 6, + "label": "BOLD", + }, + ] + `) + }) - const onCompletion = connection.onCompletion.mock.calls[0][0] + it('responds to onCompletion with all variables when starting to expand parameters', async () => { + const { connection } = await initializeServer({ rootPath: REPO_ROOT_FOLDER }) - const result = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.OPTIONS, - }, - position: { - // grep --line- - line: 2, - character: 12, - }, - }, - {} as any, - {} as any, - ) + const onCompletion = connection.onCompletion.mock.calls[0][0] - expect(result).toEqual( - expect.arrayContaining([ + const result = await onCompletion( { - data: { - name: expect.stringMatching(RegExp('--line-.*')), - type: CompletionItemDataType.Symbol, + textDocument: { + uri: FIXTURE_URI.SOURCING, + }, + position: { + // $ + line: 14, + character: 7, }, - kind: expect.any(Number), - label: expect.stringMatching(RegExp('--line-.*')), }, - ]), - ) + {} as any, + {} as any, + ) + + // they are all variables + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object { + "type": 3, + }, + "documentation": undefined, + "kind": 6, + "label": "BOLD", + }, + Object { + "data": Object { + "type": 3, + }, + "documentation": Object { + "kind": "markdown", + "value": "Variable: **RED** - *defined in extension.inc*", + }, + "kind": 6, + "label": "RED", + }, + Object { + "data": Object { + "type": 3, + }, + "documentation": Object { + "kind": "markdown", + "value": "Variable: **GREEN** - *defined in extension.inc*", + }, + "kind": 6, + "label": "GREEN", + }, + Object { + "data": Object { + "type": 3, + }, + "documentation": Object { + "kind": "markdown", + "value": "Variable: **BLUE** - *defined in extension.inc*", + }, + "kind": 6, + "label": "BLUE", + }, + Object { + "data": Object { + "type": 3, + }, + "documentation": Object { + "kind": "markdown", + "value": "Variable: **RESET** - *defined in extension.inc*", + }, + "kind": 6, + "label": "RESET", + }, + ] + `) + }) }) - it('responds to onCompletion with entire list when no word is found', async () => { - const { connection } = await initializeServer() + describe('onCompletionResolve', () => { + it('resolves documentation for buitins', async () => { + const { connection } = await initializeServer({ rootPath: REPO_ROOT_FOLDER }) - const onCompletion = connection.onCompletion.mock.calls[0][0] + const onCompletionResolve = connection.onCompletionResolve.mock.calls[0][0] - const result = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.INSTALL, - }, - position: { - // empty space - line: 26, - character: 0, + const item = { + data: { + type: CompletionItemDataType.Builtin, }, - }, - {} as any, - {} as any, - ) + kind: LSP.CompletionItemKind.Function, + label: 'echo', + } + const result = await onCompletionResolve(item, {} as any) - // Entire list - expect(result && 'length' in result && result.length).toBeGreaterThanOrEqual(50) - }) + expect(result).toEqual({ + ...item, + documentation: { + kind: 'markdown', + value: expect.stringContaining('Write arguments to the standard output'), + }, + }) + }) - it('responds to onCompletion with empty list when the following characters is not an empty string or whitespace', async () => { - const { connection } = await initializeServer() + it('ignores unknown items', async () => { + const { connection } = await initializeServer({ rootPath: REPO_ROOT_FOLDER }) - const onCompletion = connection.onCompletion.mock.calls[0][0] + const onCompletionResolve = connection.onCompletionResolve.mock.calls[0][0] - const result = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.INSTALL, + const item = { + data: { + type: CompletionItemDataType.Symbol, }, - position: { - // { - line: 271, - character: 21, - }, - }, - {} as any, - {} as any, - ) + kind: LSP.CompletionItemKind.Function, + label: 'foobar', + } + const result = await onCompletionResolve(item, {} as any) - expect(result).toEqual([]) + expect(result).toEqual({ + ...item, + documentation: undefined, + }) + }) }) - it('responds to onCompletion with empty list when word is a comment', async () => { - const { connection } = await initializeServer() + describe('onDefinition', () => { + it('responds to onDefinition', async () => { + const { connection } = await initializeServer() - const onCompletion = connection.onCompletion.mock.calls[0][0] + const onDefinition = connection.onDefinition.mock.calls[0][0] - const result = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.INSTALL, - }, - position: { - // inside comment - line: 2, - character: 1, + const result = await onDefinition( + { + textDocument: { + uri: FIXTURE_URI.SOURCING, + }, + position: { character: 10, line: 2 }, }, - }, - {} as any, - {} as any, - ) + {} as any, + {} as any, + ) - expect(result).toEqual([]) + expect(updateSnapshotUris(result)).toMatchInlineSnapshot(` + Array [ + Object { + "range": Object { + "end": Object { + "character": 0, + "line": 0, + }, + "start": Object { + "character": 0, + "line": 0, + }, + }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc", + }, + ] + `) + }) }) - it('responds to onCompletion with empty list when word is {', async () => { - const { connection } = await initializeServer() + describe('onDocumentHighlight', () => { + it('responds to onDocumentHighlight', async () => { + const { connection } = await initializeServer() - const onCompletion = connection.onCompletion.mock.calls[0][0] + const onDocumentHighlight = connection.onDocumentHighlight.mock.calls[0][0] - const result = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.ISSUE101, - }, - position: { - // the opening brace '{' to 'add_a_user' - line: 4, - character: 0, + const result1 = await onDocumentHighlight( + { + textDocument: { + uri: FIXTURE_URI.ISSUE206, + }, + position: { + // FOO + line: 0, + character: 10, + }, }, - }, - {} as any, - {} as any, - ) - - expect(result).toEqual([]) - }) + {} as any, + {} as any, + ) - it('responds to onCompletion when word is found in another file', async () => { - const { connection } = await initializeServer() + expect(result1).toMatchInlineSnapshot(` + Array [ + Object { + "range": Object { + "end": Object { + "character": 12, + "line": 0, + }, + "start": Object { + "character": 9, + "line": 0, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 28, + "line": 1, + }, + "start": Object { + "character": 25, + "line": 1, + }, + }, + }, + ] + `) + + const result2 = await onDocumentHighlight( + { + textDocument: { + uri: FIXTURE_URI.ISSUE206, + }, + position: { + // readonly is a declaration command so not parsed correctly by findOccurrences + line: 0, + character: 0, + }, + }, + {} as any, + {} as any, + ) - const onCompletion = connection.onCompletion.mock.calls[0][0] + expect(result2).toMatchInlineSnapshot(`Array []`) - const resultVariable = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.SOURCING, - }, - position: { - // $BLU (variable) - line: 6, - character: 7, - }, - }, - {} as any, - {} as any, - ) - - expect(resultVariable).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "type": 3, - }, - "documentation": Object { - "kind": "markdown", - "value": "Variable: **BLUE** - *defined in extension.inc*", - }, - "kind": 6, - "label": "BLUE", + const result3 = await onDocumentHighlight( + { + textDocument: { + uri: FIXTURE_URI.SCOPE, + }, + position: { + // X + line: 32, + character: 8, + }, }, - ] - `) + {} as any, + {} as any, + ) - const resultFunction = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.SOURCING, - }, - position: { - // add_a_us (function) - line: 8, - character: 7, - }, - }, - {} as any, - {} as any, - ) - - expect(resultFunction).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "type": 3, - }, - "documentation": Object { - "kind": "markdown", - "value": "Function: **add_a_user** - *defined in issue101.sh* - - \`\`\`txt - Helper function to add a user - \`\`\`", - }, - "kind": 3, - "label": "add_a_user", - }, - ] - `) + expect(result3).toMatchInlineSnapshot(` + Array [ + Object { + "range": Object { + "end": Object { + "character": 1, + "line": 2, + }, + "start": Object { + "character": 0, + "line": 2, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 1, + "line": 4, + }, + "start": Object { + "character": 0, + "line": 4, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 9, + "line": 8, + }, + "start": Object { + "character": 8, + "line": 8, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 11, + "line": 12, + }, + "start": Object { + "character": 10, + "line": 12, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 13, + "line": 15, + }, + "start": Object { + "character": 12, + "line": 15, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 13, + "line": 19, + }, + "start": Object { + "character": 12, + "line": 19, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 15, + "line": 20, + }, + "start": Object { + "character": 14, + "line": 20, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 11, + "line": 29, + }, + "start": Object { + "character": 10, + "line": 29, + }, + }, + }, + Object { + "range": Object { + "end": Object { + "character": 9, + "line": 32, + }, + "start": Object { + "character": 8, + "line": 32, + }, + }, + }, + ] + `) + }) }) - it('responds to onCompletion with local symbol when word is found in multiple files', async () => { - const { connection } = await initializeServer() + describe('onDocumentSymbol', () => { + it('responds to onDocumentSymbol', async () => { + const { connection } = await initializeServer() - const onCompletion = connection.onCompletion.mock.calls[0][0] + const onDocumentSymbol = connection.onDocumentSymbol.mock.calls[0][0] - const result = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.SOURCING, - }, - position: { - // BOL (BOLD is defined in multiple places) - line: 12, - character: 7, - }, - }, - {} as any, - {} as any, - ) - - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "type": 3, - }, - "documentation": undefined, - "kind": 6, - "label": "BOLD", + const result = await onDocumentSymbol( + { + textDocument: { + uri: FIXTURE_URI.SOURCING, + }, }, - ] - `) + {} as any, + {} as any, + ) + + expect(updateSnapshotUris(result)).toMatchInlineSnapshot(` + Array [ + Object { + "kind": 13, + "location": Object { + "range": Object { + "end": Object { + "character": 16, + "line": 10, + }, + "start": Object { + "character": 0, + "line": 10, + }, + }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/sourcing.sh", + }, + "name": "BOLD", + }, + Object { + "kind": 12, + "location": Object { + "range": Object { + "end": Object { + "character": 1, + "line": 22, + }, + "start": Object { + "character": 0, + "line": 20, + }, + }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/sourcing.sh", + }, + "name": "loadlib", + }, + ] + `) + }) }) - it('responds to onCompletion with all variables when starting to expand parameters', async () => { - const { connection } = await initializeServer({ rootPath: REPO_ROOT_FOLDER }) + describe('onHover', () => { + it('responds with documentation for command', async () => { + const { connection } = await initializeServer() - const onCompletion = connection.onCompletion.mock.calls[0][0] + const onHover = connection.onHover.mock.calls[0][0] - const result: any = await onCompletion( - { - textDocument: { - uri: FIXTURE_URI.SOURCING, - }, - position: { - // $ - line: 14, - character: 7, + const result = await onHover( + { + textDocument: { + uri: FIXTURE_URI.INSTALL, + }, + position: { + // rm + line: 25, + character: 5, + }, }, - }, - {} as any, - {} as any, - ) + {} as any, + {} as any, + ) - // they are all variables - expect(Array.from(new Set(result.map((item: any) => item.kind)))).toEqual([ - LSP.CompletionItemKind.Variable, - ]) - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "type": 3, - }, - "documentation": undefined, - "kind": 6, - "label": "BOLD", + expect(result).toBeDefined() + expect(result).toEqual({ + contents: { + kind: 'markdown', + value: expect.stringContaining('remove directories'), }, - Object { - "data": Object { - "type": 3, + }) + }) + + it('responds with function documentation extracted from comments', async () => { + const { connection } = await initializeServer() + + const onHover = connection.onHover.mock.calls[0][0] + + const result = await onHover( + { + textDocument: { + uri: FIXTURE_URI.COMMENT_DOC, }, - "documentation": Object { - "kind": "markdown", - "value": "Variable: **RED** - *defined in extension.inc*", + position: { + line: 17, + character: 0, }, - "kind": 6, - "label": "RED", }, - Object { - "data": Object { - "type": 3, + {} as any, + {} as any, + ) + + expect(result).toBeDefined() + expect(result).toMatchInlineSnapshot(` + Object { + "contents": Object { + "kind": "markdown", + "value": "Function: **hello_world** - *defined on line 8* + + \`\`\`txt + this is a comment + describing the function + hello_world + this function takes two arguments + \`\`\`", + }, + } + `) + }) + + it('displays correct documentation for symbols in file that override path executables', async () => { + const { connection } = await initializeServer() + + const onHover = connection.onHover.mock.calls[0][0] + + const result = await onHover( + { + textDocument: { + uri: FIXTURE_URI.OVERRIDE_SYMBOL, }, - "documentation": Object { - "kind": "markdown", - "value": "Variable: **GREEN** - *defined in extension.inc*", + position: { + line: 9, + character: 1, }, - "kind": 6, - "label": "GREEN", }, - Object { - "data": Object { - "type": 3, + {} as any, + {} as any, + ) + + expect(result).toBeDefined() + expect(result).toMatchInlineSnapshot(` + Object { + "contents": Object { + "kind": "markdown", + "value": "Function: **ls** - *defined on line 6* + + \`\`\`txt + override documentation for \`ls\` symbol + \`\`\`", + }, + } + `) + }) + + it('returns executable documentation if the function is not redefined', async () => { + const { connection } = await initializeServer() + + const onHover = connection.onHover.mock.calls[0][0] + + const getHoverResult = (position: LSP.Position) => + onHover( + { + textDocument: { + uri: FIXTURE_URI.OVERRIDE_SYMBOL, + }, + position, }, - "documentation": Object { - "kind": "markdown", - "value": "Variable: **BLUE** - *defined in extension.inc*", + {} as any, + {} as any, + ) + + const result1 = await getHoverResult({ line: 2, character: 1 }) + expect(result1).toBeDefined() + expect((result1 as any)?.contents.value).toContain('list directory contents') + + // return null same result if the cursor is on the arguments + const result2 = await getHoverResult({ line: 2, character: 3 }) + expect(result2).toBeDefined() + expect((result2 as any)?.contents.value).toBeUndefined() + }) + + it.skip('returns documentation from explainshell', async () => { + // Skipped as this requires a running explainshell server (and the code is hard to mock) + // docker container run --name explainshell --restart always -p 127.0.0.1:6000:5000 -d spaceinvaderone/explainshell + + const { connection } = await initializeServer({ + capabilities: { + workspace: { + configuration: true, }, - "kind": 6, - "label": "BLUE", }, - Object { - "data": Object { - "type": 3, - }, - "documentation": Object { - "kind": "markdown", - "value": "Variable: **RESET** - *defined in extension.inc*", - }, - "kind": 6, - "label": "RESET", + configurationObject: { + explainshellEndpoint: 'http://localhost:6000', }, - ] - `) + }) + const onHover = connection.onHover.mock.calls[0][0] + + const getHoverResult = (position: LSP.Position) => + onHover( + { + textDocument: { + uri: FIXTURE_URI.OVERRIDE_SYMBOL, + }, + position, + }, + {} as any, + {} as any, + ) + + const result1 = await getHoverResult({ line: 2, character: 1 }) + expect(result1).toBeDefined() + expect((result1 as any)?.contents.value).toEqual('list directory contents') + + // return explain shell result for the arguments + const result2 = await getHoverResult({ line: 2, character: 3 }) + expect(result2).toBeDefined() + expect((result2 as any)?.contents.value).toEqual( + '**\\-l** use a long listing format', + ) + }) }) - it('responds to onCodeAction', async () => { - const { connection, server } = await initializeServer() - const document = FIXTURE_DOCUMENT.COMMENT_DOC + describe('onReferences', () => { + async function getOnReferencesTestCase() { + const { connection } = await initializeServer() + const onReferences = connection.onReferences.mock.calls[0][0] + + const callOnReferences = ({ + includeDeclarationOfCurrentSymbol, + uri, + position, + }: { + includeDeclarationOfCurrentSymbol: boolean + uri: string + position: LSP.Position + }) => + updateSnapshotUris( + onReferences( + { + textDocument: { + uri, + }, + position, + context: { + includeDeclaration: includeDeclarationOfCurrentSymbol, + }, + }, + {} as any, + {} as any, + ), + ) - await server.analyzeAndLintDocument(document) + return { + callOnReferences, + } + } - expect(connection.sendDiagnostics).toHaveBeenCalledTimes(1) - const { diagnostics } = connection.sendDiagnostics.mock.calls[0][0] - const fixableDiagnostic = diagnostics.filter(({ code }) => code === 'SC2086')[0] + it('returns null if the word is not found', async () => { + const { callOnReferences } = await getOnReferencesTestCase() + const result = await callOnReferences({ + position: { line: 34, character: 1 }, // empty line + uri: FIXTURE_URI.INSTALL, + includeDeclarationOfCurrentSymbol: true, + }) + expect(result).toBeNull() + }) - expect(fixableDiagnostic).toMatchInlineSnapshot(` - Object { - "code": "SC2086", - "codeDescription": Object { - "href": "https://www.shellcheck.net/wiki/SC2086", - }, - "message": "Double quote to prevent globbing and word splitting.", - "range": Object { - "end": Object { - "character": 13, - "line": 55, - }, - "start": Object { - "character": 5, - "line": 55, - }, - }, - "severity": 3, - "source": "shellcheck", - "tags": undefined, + it('returns references to builtins and executables across the workspace', async () => { + const { callOnReferences } = await getOnReferencesTestCase() + const result = await callOnReferences({ + position: { line: 263, character: 5 }, // echo + uri: FIXTURE_URI.INSTALL, + includeDeclarationOfCurrentSymbol: true, + }) + expect(Array.isArray(result)).toBe(true) + if (Array.isArray(result)) { + expect(result.length).toBeGreaterThan(50) + expect(new Set(result.map((v) => v.uri)).size).toBeGreaterThan(5) } - `) + }) - // TODO: we could find the diagnostics and then use the range to test the code action + it('returns references depending on the context flag', async () => { + const { callOnReferences } = await getOnReferencesTestCase() - const onCodeAction = connection.onCodeAction.mock.calls[0][0] + const resultIncludingCurrentSymbol = await callOnReferences({ + position: { line: 50, character: 10 }, // npm_config_loglevel + uri: FIXTURE_URI.INSTALL, + includeDeclarationOfCurrentSymbol: true, + }) - const result = await onCodeAction( - { - textDocument: { - uri: FIXTURE_URI.COMMENT_DOC, - }, - range: fixableDiagnostic.range, - context: { - diagnostics: [fixableDiagnostic], - }, - }, - {} as any, - {} as any, - ) - - expect(result).toHaveLength(1) - const codeAction = (result as CodeAction[])[0] - expect(codeAction.diagnostics).toEqual([fixableDiagnostic]) - expect(codeAction.diagnostics).toEqual([fixableDiagnostic]) - - expect(codeAction.edit?.changes && codeAction.edit?.changes[FIXTURE_URI.COMMENT_DOC]) - .toMatchInlineSnapshot(` - Array [ - Object { - "newText": "\\"", - "range": Object { - "end": Object { - "character": 13, - "line": 55, + const resultExcludingCurrentSymbol = await callOnReferences({ + position: { line: 50, character: 10 }, // npm_config_loglevel + uri: FIXTURE_URI.INSTALL, + includeDeclarationOfCurrentSymbol: false, + }) + + expect(resultIncludingCurrentSymbol).toMatchInlineSnapshot(` + Array [ + Object { + "range": Object { + "end": Object { + "character": 19, + "line": 40, + }, + "start": Object { + "character": 0, + "line": 40, + }, }, - "start": Object { - "character": 13, - "line": 55, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh", + }, + Object { + "range": Object { + "end": Object { + "character": 21, + "line": 48, + }, + "start": Object { + "character": 2, + "line": 48, + }, }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh", }, - }, - Object { - "newText": "\\"", - "range": Object { - "end": Object { - "character": 5, - "line": 55, + Object { + "range": Object { + "end": Object { + "character": 26, + "line": 50, + }, + "start": Object { + "character": 7, + "line": 50, + }, + }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh", + }, + Object { + "range": Object { + "end": Object { + "character": 26, + "line": 42, + }, + "start": Object { + "character": 7, + "line": 42, + }, + }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh", + }, + ] + `) + + expect(resultExcludingCurrentSymbol).toMatchInlineSnapshot(` + Array [ + Object { + "range": Object { + "end": Object { + "character": 19, + "line": 40, + }, + "start": Object { + "character": 0, + "line": 40, + }, }, - "start": Object { - "character": 5, - "line": 55, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh", + }, + Object { + "range": Object { + "end": Object { + "character": 21, + "line": 48, + }, + "start": Object { + "character": 2, + "line": 48, + }, }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh", }, - }, - ] - `) + Object { + "range": Object { + "end": Object { + "character": 26, + "line": 42, + }, + "start": Object { + "character": 7, + "line": 42, + }, + }, + "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh", + }, + ] + `) + }) }) - it('displays correct documentation for symbols in file that override path executables', async () => { - const { connection, server } = await initializeServer() - server.register(connection) + describe('onWorkspaceSymbol', () => { + it('responds to onWorkspaceSymbol', async () => { + const { connection } = await initializeServer() - const onHover = connection.onHover.mock.calls[0][0] + const onWorkspaceSymbol = connection.onWorkspaceSymbol.mock.calls[0][0] - const result = await onHover( - { - textDocument: { - uri: FIXTURE_URI.OVERRIDE_SYMBOL, - }, - position: { - line: 9, - character: 1, - }, - }, - {} as any, - {} as any, - ) - - expect(result).toBeDefined() - expect(result).toMatchInlineSnapshot(` - Object { - "contents": Object { - "kind": "markdown", - "value": "Function: **ls** - *defined on line 6* - - \`\`\`txt - override documentation for \`ls\` symbol - \`\`\`", - }, + async function lookupAndExpectNpmConfigLoglevelResult(query: string) { + const result = await onWorkspaceSymbol( + { + query, + }, + {} as any, + {} as any, + ) + + expect(result).toEqual([ + { + kind: expect.any(Number), + location: { + range: { + end: { character: 27, line: 40 }, + start: { character: 0, line: 40 }, + }, + uri: expect.stringContaining('/testing/fixtures/install.sh'), + }, + name: 'npm_config_loglevel', + }, + ]) } - `) - }) - - it('returns executable documentation if the function is not redefined', async () => { - const { connection, server } = await initializeServer() - server.register(connection) - const onHover = connection.onHover.mock.calls[0][0] - - const result = await onHover( - { - textDocument: { - uri: FIXTURE_URI.OVERRIDE_SYMBOL, - }, - position: { - line: 2, - character: 1, - }, - }, - {} as any, - {} as any, - ) - - expect(result).toBeDefined() - expect((result as any)?.contents.value).toContain('list directory contents') + await lookupAndExpectNpmConfigLoglevelResult('npm_config_loglevel') // exact + await lookupAndExpectNpmConfigLoglevelResult('config_log') // in the middle + await lookupAndExpectNpmConfigLoglevelResult('npmloglevel') // fuzzy + }) }) }) diff --git a/server/src/analyser.ts b/server/src/analyser.ts index 5ae384e9a..90c524f96 100644 --- a/server/src/analyser.ts +++ b/server/src/analyser.ts @@ -1,7 +1,6 @@ import * as fs from 'fs' import * as FuzzySearch from 'fuzzy-search' import fetch from 'node-fetch' -import * as URI from 'urijs' import * as url from 'url' import { isDeepStrictEqual } from 'util' import * as LSP from 'vscode-languageserver/node' @@ -265,9 +264,9 @@ export default class Analyzer { /** * Find all the locations where the given word was defined or referenced. + * This will include commands, functions, variables, etc. * - * FIXME: take position into account - * FIXME: take file into account + * It's currently not scope-aware, see findOccurrences. */ public findReferences(word: string): LSP.Location[] { const uris = Object.keys(this.uriToAnalyzedDocument) @@ -275,11 +274,14 @@ export default class Analyzer { } /** - * Find all occurrences (references or definitions) of a word in the given file. + * Find all occurrences of a word in the given file. * It's currently not scope-aware. * - * FIXME: should this take the scope into account? I guess it should - * as this is used for highlighting. + * This will include commands, functions, variables, etc. + * + * It's currently not scope-aware, meaning references does include + * references to functions and variables that has the same name but + * are defined in different files. */ public findOccurrences(uri: string, word: string): LSP.Location[] { const analyzedDocument = this.uriToAnalyzedDocument[uri] @@ -295,6 +297,7 @@ export default class Analyzer { let namedNode: Parser.SyntaxNode | null = null if (TreeSitterUtil.isReference(n)) { + // NOTE: a reference can be a command, variable, function, etc. namedNode = n.firstNamedChild || n } else if (TreeSitterUtil.isDefinition(n)) { namedNode = n.firstNamedChild @@ -380,13 +383,13 @@ export default class Analyzer { return {} } - const cmd = interestingNode.text - type ExplainshellResponse = { matches?: Array<{ helpHTML: string; start: number; end: number }> } - const url = URI(endpoint).path('/api/explain').addQuery('cmd', cmd).toString() + const searchParams = new URLSearchParams({ cmd: interestingNode.text }).toString() + const url = `${endpoint}/api/explain?${searchParams}` + const explainshellRawResponse = await fetch(url) const explainshellResponse = (await explainshellRawResponse.json()) as ExplainshellResponse diff --git a/server/src/server.ts b/server/src/server.ts index 54d1de1ab..53a8ef76d 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -18,6 +18,7 @@ import { SNIPPETS } from './snippets' import { BashCompletionItem, CompletionItemDataType } from './types' import { uniqueBasedOnHash } from './util/array' import { logger, setLogConnection, setLogLevel } from './util/logger' +import { isPositionIncludedInRange } from './util/lsp' import { getShellDocumentation } from './util/sh' const PARAMETER_EXPANSION_PREFIXES = new Set(['$', '${']) @@ -708,7 +709,14 @@ export default class BashServer { if (!word) { return null } - return this.analyzer.findReferences(word) + + const isCurrentDeclaration = (l: LSP.Location) => + l.uri === params.textDocument.uri && + isPositionIncludedInRange(params.position, l.range) + + return this.analyzer + .findReferences(word) + .filter((l) => params.context.includeDeclaration || !isCurrentDeclaration(l)) } private onWorkspaceSymbol(params: LSP.WorkspaceSymbolParams): LSP.SymbolInformation[] { diff --git a/server/yarn.lock b/server/yarn.lock index 488e05b20..ad694a173 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -216,11 +216,6 @@ turndown@7.1.1: dependencies: domino "^2.1.6" -urijs@1.19.11: - version "1.19.11" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc" - integrity sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ== - vscode-jsonrpc@8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz#f239ed2cd6004021b6550af9fd9d3e47eee3cac9" diff --git a/testing/fixtures.ts b/testing/fixtures.ts index 64261d307..468505be3 100644 --- a/testing/fixtures.ts +++ b/testing/fixtures.ts @@ -40,17 +40,28 @@ export const FIXTURE_DOCUMENT: Record = ( export const REPO_ROOT_FOLDER = path.resolve(path.join(FIXTURE_FOLDER, '../..')) -export function updateSnapshotUris(obj: any) { - if (obj.uri) { - obj.uri = obj.uri.replace(REPO_ROOT_FOLDER, '__REPO_ROOT_FOLDER__') - } - Object.values(obj).forEach((child) => { - if (Array.isArray(child)) { - child.forEach((el) => updateSnapshotUris(el)) - } else if (typeof child === 'object' && child != null) { - updateSnapshotUris(child) +export function updateSnapshotUris< + T extends Record | Array | null | undefined, +>(data: T): T { + if (data != null) { + if (Array.isArray(data)) { + data.forEach((el) => updateSnapshotUris(el)) + return data + } + + if (typeof data === 'object') { + if (data.uri) { + data.uri = data.uri.replace(REPO_ROOT_FOLDER, '__REPO_ROOT_FOLDER__') + } + Object.values(data).forEach((child) => { + if (Array.isArray(child)) { + child.forEach((el) => updateSnapshotUris(el)) + } else if (typeof child === 'object' && child != null) { + updateSnapshotUris(child) + } + }) } - }) + } - return obj + return data } diff --git a/testing/fixtures/override-executable-symbol.sh b/testing/fixtures/override-executable-symbol.sh index c5bae1067..1c1d8711f 100644 --- a/testing/fixtures/override-executable-symbol.sh +++ b/testing/fixtures/override-executable-symbol.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -ls +ls -la # override documentation for `ls` symbol ls() { diff --git a/testing/fixtures/scope.sh b/testing/fixtures/scope.sh index 6eb5fc12b..c8e8ac147 100644 --- a/testing/fixtures/scope.sh +++ b/testing/fixtures/scope.sh @@ -39,3 +39,5 @@ for i in 1 2 3 4 5 do echo "$GLOBAL_1 $i" done + +echo "$npm_config_loglevel" # this is an undefined variable, but defined in install.sh diff --git a/testing/mocks.ts b/testing/mocks.ts index 4e0948a3d..71b9fb4bb 100644 --- a/testing/mocks.ts +++ b/testing/mocks.ts @@ -9,7 +9,12 @@ export function getMockConnection(): jest.Mocked { } return { - client: {} as any, + client: { + connection: {} as any, + register: jest.fn(), + initialize: jest.fn(), + fillServerCapabilities: jest.fn(), + }, console, dispose: jest.fn(), languages: {} as any, @@ -76,6 +81,20 @@ export function getMockConnection(): jest.Mocked { showInformationMessage: jest.fn(), showWarningMessage: jest.fn(), }, - workspace: {} as any, + workspace: { + applyEdit: jest.fn(), + connection: {} as any, + fillServerCapabilities: jest.fn(), + getConfiguration: jest.fn(), + getWorkspaceFolders: jest.fn(), + initialize: jest.fn(), + onDidChangeWorkspaceFolders: jest.fn(), + onDidCreateFiles: jest.fn(), + onDidDeleteFiles: jest.fn(), + onDidRenameFiles: jest.fn(), + onWillCreateFiles: jest.fn(), + onWillDeleteFiles: jest.fn(), + onWillRenameFiles: jest.fn(), + }, } }