From e9316fc66d1d66e66adf61d2dcbd002c8bcaf301 Mon Sep 17 00:00:00 2001 From: skovhus Date: Tue, 17 Jan 2023 10:10:44 +0100 Subject: [PATCH 1/3] Get rid of redundant property --- server/src/__tests__/server.test.ts | 9 --------- server/src/server.ts | 12 ++++-------- server/src/types.ts | 1 - 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/server/src/__tests__/server.test.ts b/server/src/__tests__/server.test.ts index dcac9dc70..cac5c1075 100644 --- a/server/src/__tests__/server.test.ts +++ b/server/src/__tests__/server.test.ts @@ -484,7 +484,6 @@ describe('server', () => { expect.arrayContaining([ { data: { - name: 'rm', type: CompletionItemDataType.Executable, }, kind: expect.any(Number), @@ -634,7 +633,6 @@ describe('server', () => { Array [ Object { "data": Object { - "name": "BLUE", "type": 3, }, "documentation": Object { @@ -666,7 +664,6 @@ describe('server', () => { Array [ Object { "data": Object { - "name": "add_a_user", "type": 3, }, "documentation": Object { @@ -708,7 +705,6 @@ describe('server', () => { Array [ Object { "data": Object { - "name": "BOLD", "type": 3, }, "documentation": undefined, @@ -747,7 +743,6 @@ describe('server', () => { Array [ Object { "data": Object { - "name": "BOLD", "type": 3, }, "documentation": undefined, @@ -756,7 +751,6 @@ describe('server', () => { }, Object { "data": Object { - "name": "RED", "type": 3, }, "documentation": Object { @@ -768,7 +762,6 @@ describe('server', () => { }, Object { "data": Object { - "name": "GREEN", "type": 3, }, "documentation": Object { @@ -780,7 +773,6 @@ describe('server', () => { }, Object { "data": Object { - "name": "BLUE", "type": 3, }, "documentation": Object { @@ -792,7 +784,6 @@ describe('server', () => { }, Object { "data": Object { - "name": "RESET", "type": 3, }, "documentation": Object { diff --git a/server/src/server.ts b/server/src/server.ts index 22a4a1726..3c01045a1 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -344,7 +344,6 @@ export default class BashServer { label: symbol.name, kind: symbolKindToCompletionKind(symbol.kind), data: { - name: symbol.name, type: CompletionItemDataType.Symbol, }, documentation: @@ -503,7 +502,6 @@ export default class BashServer { label: reservedWord, kind: LSP.CompletionItemKind.Keyword, data: { - name: reservedWord, type: CompletionItemDataType.ReservedWord, }, })) @@ -516,7 +514,6 @@ export default class BashServer { label: executable, kind: LSP.CompletionItemKind.Function, data: { - name: executable, type: CompletionItemDataType.Executable, }, } @@ -526,7 +523,6 @@ export default class BashServer { label: builtin, kind: LSP.CompletionItemKind.Function, data: { - name: builtin, type: CompletionItemDataType.Builtin, }, })) @@ -535,7 +531,6 @@ export default class BashServer { label: option, kind: LSP.CompletionItemKind.Constant, data: { - name: option, type: CompletionItemDataType.Symbol, }, })) @@ -560,10 +555,11 @@ export default class BashServer { item: LSP.CompletionItem, ): Promise { const { - data: { name, type }, + label, + data: { type }, } = item as BashCompletionItem - logger.debug(`onCompletionResolve name=${name} type=${type}`) + logger.debug(`onCompletionResolve label=${label} type=${type}`) try { let documentation = null @@ -573,7 +569,7 @@ export default class BashServer { type === CompletionItemDataType.Builtin || type === CompletionItemDataType.ReservedWord ) { - documentation = await getShellDocumentation({ word: name }) + documentation = await getShellDocumentation({ word: label }) } return documentation diff --git a/server/src/types.ts b/server/src/types.ts index a171bbe87..3ea537347 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -10,6 +10,5 @@ export enum CompletionItemDataType { export interface BashCompletionItem extends LSP.CompletionItem { data: { type: CompletionItemDataType - name: string } } From 0e22fe75550233e572ebece6b82c2240edbfe3dd Mon Sep 17 00:00:00 2001 From: skovhus Date: Tue, 17 Jan 2023 10:11:18 +0100 Subject: [PATCH 2/3] Move snippets to server --- server/src/__tests__/snippets.test.ts | 31 ++++ server/src/server.ts | 2 + server/src/snippets.ts | 168 +++++++++++++++++++ server/src/types.ts | 1 + vscode-client/package.json | 6 - vscode-client/snippets/convention.md | 17 -- vscode-client/snippets/snippets.json | 228 -------------------------- 7 files changed, 202 insertions(+), 251 deletions(-) create mode 100644 server/src/__tests__/snippets.test.ts create mode 100644 server/src/snippets.ts delete mode 100644 vscode-client/snippets/convention.md delete mode 100644 vscode-client/snippets/snippets.json diff --git a/server/src/__tests__/snippets.test.ts b/server/src/__tests__/snippets.test.ts new file mode 100644 index 000000000..8d4bce233 --- /dev/null +++ b/server/src/__tests__/snippets.test.ts @@ -0,0 +1,31 @@ +import { MarkupContent } from 'vscode-languageserver' + +import { SNIPPETS } from '../snippets' + +describe('snippets', () => { + it('should have unique labels', () => { + const labels = SNIPPETS.map((snippet) => snippet.label) + const uniqueLabels = new Set(labels) + expect(labels.length).toBe(uniqueLabels.size) + }) + + SNIPPETS.forEach(({ label, documentation }) => { + it(`contains the label in the documentation for "${label}"`, () => { + const stringDocumentation = (documentation as MarkupContent)?.value + expect(stringDocumentation).toBeDefined() + if (stringDocumentation) { + expect(stringDocumentation).toContain(label) + const secondLine = stringDocumentation.split('\n')[1] + try { + expect( + secondLine.startsWith(label) || secondLine.startsWith(`[${label}]`), + ).toBe(true) + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Did not start with label: ${label}`, secondLine) + throw error + } + } + }) + }) +}) diff --git a/server/src/server.ts b/server/src/server.ts index 3c01045a1..dc05bd846 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -14,6 +14,7 @@ import Executables from './executables' import { initializeParser } from './parser' import * as ReservedWords from './reserved-words' import { Linter } from './shellcheck' +import { SNIPPETS } from './snippets' import { BashCompletionItem, CompletionItemDataType } from './types' import { uniqueBasedOnHash } from './util/array' import { logger, setLogConnection, setLogLevel } from './util/logger' @@ -541,6 +542,7 @@ export default class BashServer { ...programCompletions, ...builtinsCompletions, ...optionsCompletions, + ...SNIPPETS, ] if (word) { diff --git a/server/src/snippets.ts b/server/src/snippets.ts new file mode 100644 index 000000000..774640870 --- /dev/null +++ b/server/src/snippets.ts @@ -0,0 +1,168 @@ +/** + * Naming convention for `label`: + * - is always a language keyword, builtin name or expansion symbol like `:-`. + * - If a snippet is for a builtin then builtin name is used. + * - If a snippet is for expansion then expansion symbol is used. + * - If a snippet is for a specific external program like **awk** then program name must be added to `prefix` like this: + * `awk:{{snippet-prefix}}`. + */ +import { CompletionItemKind, InsertTextFormat, MarkupKind } from 'vscode-languageserver' + +import { BashCompletionItem } from './types' + +export const SNIPPETS: BashCompletionItem[] = [ + { + label: 'shebang', + insertText: '#!/usr/bin/env ${1|bash,sh|}', + }, + { + label: 'if', + insertText: ['if ${1:command}; then', '\t$0', 'fi'].join('\n'), + }, + { + label: 'if-else', + insertText: ['if ${1:command}; then', '\t${2:echo}', 'else', '\t$0', 'fi'].join('\n'), + }, + { + label: 'while', + insertText: ['while ${1:command}; do', '\t$0', 'done'].join('\n'), + }, + { + label: 'until', + insertText: ['until ${1:command}; do', '\t$0', 'done'].join('\n'), + }, + { + label: 'for', + insertText: ['for ${1:variable} in ${2:list}; do', '\t$0', 'done'].join('\n'), + }, + { + label: 'function', + insertText: ['${1:function_name}() {', '\t$0', '}'].join('\n'), + }, + { + label: 'main', + insertText: ['main() {', '\t$0', '}'].join('\n'), + }, + { + documentation: '[:-] expansion', + label: ':-', + insertText: '"\\${${1:variable}:-${2:default}}"', + }, + { + documentation: '[:=] expansion', + label: ':=', + insertText: '"\\${${1:variable}:=${2:default}}"', + }, + { + documentation: '[:?] expansion', + label: ':?', + insertText: '"\\${${1:variable}:?${2:error_message}}"', + }, + { + documentation: '[:+] expansion', + label: ':+', + insertText: '"\\${${1:variable}:+${2:alternative}}"', + }, + { + documentation: '[#] expansion', + label: '#', + insertText: '"\\${${1:variable}#${2:pattern}}"', + }, + { + documentation: '[##] expansion', + label: '##', + insertText: '"\\${${1:variable}##${2:pattern}}"', + }, + { + documentation: '[%] expansion', + label: '%', + insertText: '"\\${${1:variable}%${2:pattern}}"', + }, + { + documentation: '[%%] expansion', + label: '%%', + insertText: '"\\${${1:variable}%%${2:pattern}}"', + }, + { + documentation: '[..] brace expansion', + label: '..', + insertText: '{${1:from}..${2:to}}', + }, + { + label: 'echo', + insertText: 'echo "${1:message}"', + }, + { + label: 'printf', + insertText: 'printf \'%s\' "${1:message}"', + }, + { + label: 'source', + insertText: 'source "${1:path/to/file}"', + }, + { + label: 'alias', + insertText: 'alias ${1:name}=${2:value}', + }, + { + label: 'cd', + insertText: 'cd "${1:path/to/directory}"', + }, + { + label: 'getopts', + insertText: 'getopts ${1:optstring} ${2:name}', + }, + { + label: 'jobs', + insertText: 'jobs -x ${1:command}', + }, + { + label: 'kill', + insertText: 'kill ${1|-l,-L|}', + }, + { + label: 'let', + insertText: 'let ${1:argument}', + }, + { + label: 'test', + insertText: + '[[ ${1:argument1} ${2|-ef,-nt,-ot,==,=,!=,=~,<,>,-eq,-ne,-lt,-le,-gt,-ge|} ${3:argument2} ]]', + }, + { + documentation: '[dev]ice name', + label: 'dev', + insertText: '/dev/${1|null,stdin,stdout,stderr|}', + }, + { + label: 'sed:filter-lines', + insertText: + "sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '/${3:pattern}/' ${4:path/to/file}", + }, + { + label: 'awk:filter-lines', + insertText: "awk '/${1:pattern}/' ${2:path/to/file}", + }, +].map((item) => ({ + ...item, + documentation: { + value: [ + markdownBlock( + `${item.documentation || item.label} (bash-language-server)\n\n`, + 'man', + ), + markdownBlock(item.insertText, 'bash'), + ].join('\n'), + kind: MarkupKind.Markdown, + }, + + insertTextFormat: InsertTextFormat.Snippet, + data: { + type: CompletionItemKind.Snippet, + }, +})) + +function markdownBlock(text: string, language: string): string { + const tripleQoute = '```' + return [tripleQoute + language, text, tripleQoute].join('\n') +} diff --git a/server/src/types.ts b/server/src/types.ts index 3ea537347..3989d0588 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -5,6 +5,7 @@ export enum CompletionItemDataType { Executable, ReservedWord, Symbol, + Snippet, } export interface BashCompletionItem extends LSP.CompletionItem { diff --git a/vscode-client/package.json b/vscode-client/package.json index 2302333ce..75dd53e18 100644 --- a/vscode-client/package.json +++ b/vscode-client/package.json @@ -27,12 +27,6 @@ ], "main": "./out/extension", "contributes": { - "snippets": [ - { - "language": "shellscript", - "path": "./snippets/snippets.json" - } - ], "configuration": { "type": "object", "title": "Bash IDE configuration", diff --git a/vscode-client/snippets/convention.md b/vscode-client/snippets/convention.md deleted file mode 100644 index 002433053..000000000 --- a/vscode-client/snippets/convention.md +++ /dev/null @@ -1,17 +0,0 @@ -# Snippet naming convention - -- Snippet name always language keyword, builtin name or expansion symbol like `:-`. -- `description` always a short snippet explanation without leading articles and trailing punctuation. - Mnemonics with square brackets are always used to denote what chars are included in snippet `prefix` until they match snippet - names (`-` is ignored while checking for matching). -- `prefix` can be just a string or array containing two item. The string or first item always follow these rules: - - If a snippet is for language construct then first two letters of the first word from `description` are used - plus the first letter from the second one (if it's exist). - - If a snippet is for a builtin then builtin name is used. - - If a snippet is for expansion then expansion symbol is used. - - If a snippet is for a specific external program like **awk** then program name must be added to `prefix` like this: - `awk:{{snippet-prefix}}`. - The second one is always a noun existing to make snippets more memorizable. -- `body` is always array. -- If both short and long options available placeholder must be used to let user to chose the option's style - but the first alternative should always be the long option. diff --git a/vscode-client/snippets/snippets.json b/vscode-client/snippets/snippets.json deleted file mode 100644 index c281451dd..000000000 --- a/vscode-client/snippets/snippets.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "shebang": { - "description": "shebang", - "prefix": "shebang", - "body": [ - "#!/usr/bin/env ${1|bash,sh|}" - ] - }, - "if": { - "description": "if", - "prefix": "if", - "body": [ - "if ${1:command}; then", - "\t$0", - "fi" - ] - }, - "if-else": { - "description": "if else", - "prefix": "if-else", - "body": [ - "if ${1:command}; then", - "\t${2:echo}", - "else", - "\t$0", - "fi" - ] - }, - "while": { - "description": "while", - "prefix": "while", - "body": [ - "while ${1:command}; do", - "\t$0", - "done" - ] - }, - "until": { - "description": "until", - "prefix": "until", - "body": [ - "until ${1:command}; do", - "\t$0", - "done" - ] - }, - "for": { - "description": "for", - "prefix": "for", - "body": [ - "for ${1:variable} in ${2:list}; do", - "\t$0", - "done" - ] - }, - "function": { - "description": "function", - "prefix": "function", - "body": [ - "${1:function_name}() {", - "\t$0", - "}" - ] - }, - "main": { - "description": "main", - "prefix": "main", - "body": [ - "main() {", - "\t$0", - "}" - ] - }, - ":-": { - "description": "[:-] expansion", - "prefix": ":-", - "body": [ - "\"\\${${1:variable}:-${2:default}}\"" - ] - }, - ":=": { - "description": "[:=] expansion", - "prefix": ":=", - "body": [ - "\"\\${${1:variable}:=${2:default}}\"" - ] - }, - ":?": { - "description": "[:?] expansion", - "prefix": ":?", - "body": [ - "\"\\${${1:variable}:?${2:error_message}}\"" - ] - }, - ":+": { - "description": "[:+] expansion", - "prefix": ":+", - "body": [ - "\"\\${${1:variable}:+${2:alternative}}\"" - ] - }, - "#": { - "description": "[#] expansion", - "prefix": "#", - "body": [ - "\"\\${${1:variable}#${2:pattern}}\"" - ] - }, - "##": { - "description": "[##] expansion", - "prefix": "##", - "body": [ - "\"\\${${1:variable}##${2:pattern}}\"" - ] - }, - "%": { - "description": "[%] expansion", - "prefix": "%", - "body": [ - "\"\\${${1:variable}%${2:pattern}}\"" - ] - }, - "%%": { - "description": "[%%] expansion", - "prefix": "%%", - "body": [ - "\"\\${${1:variable}%%${2:pattern}}\"" - ] - }, - "..": { - "description": "brace expansion", - "prefix": "..", - "body": [ - "{${1:from}..${2:to}}" - ] - }, - "echo": { - "description": "echo", - "prefix": "echo", - "body": [ - "echo \"${1:message}\"" - ] - }, - "printf": { - "description": "printf", - "prefix": "printf", - "body": [ - "printf '%s' \"${1:message}\"" - ] - }, - "source": { - "description": "source", - "prefix": "source", - "body": [ - "source \"${1:path/to/file}\"" - ] - }, - "alias": { - "description": "alias", - "prefix": "alias", - "body": [ - "alias ${1:name}=${2:value}" - ] - }, - "cd": { - "description": "cd", - "prefix": "cd", - "body": [ - "cd \"${1:path/to/directory}\"" - ] - }, - "getopts": { - "description": "getopts", - "prefix": "getopts", - "body": [ - "getopts ${1:optstring} ${2:name}" - ] - }, - "jobs": { - "description": "jobs", - "prefix": "jobs", - "body": [ - "jobs -x ${1:command}" - ] - }, - "kill": { - "description": "kill", - "prefix": "kill", - "body": [ - "kill ${1|-l,-L|}" - ] - }, - "let": { - "description": "let", - "prefix": "let", - "body": [ - "let ${1:argument}" - ] - }, - "test": { - "description": "test", - "prefix": "test", - "body": [ - "[[ ${1:argument1} ${2|-ef,-nt,-ot,==,=,!=,=~,<,>,-eq,-ne,-lt,-le,-gt,-ge|} ${3:argument2} ]]" - ] - }, - "stream": { - "description": "[dev]ice name", - "prefix": "dev", - "body": [ - "/dev/${1|null,stdin,stdout,stderr|}" - ] - }, - "sed:filter-lines": { - "description": "sed:filter-lines", - "prefix": "sed:filter-lines", - "body": [ - "sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '/${3:pattern}/' ${4:path/to/file}" - ] - }, - "awk:filter-lines": { - "description": "awk:filter-lines", - "prefix": "awk:filter-lines", - "body": [ - "awk '/${1:pattern}/' ${2:path/to/file}" - ] - } -} \ No newline at end of file From 3c1ba1f847f7c1f5fe184e241bd8b66f8e66adf4 Mon Sep 17 00:00:00 2001 From: skovhus Date: Tue, 17 Jan 2023 10:22:02 +0100 Subject: [PATCH 3/3] Release new server version 4.5.0 --- server/CHANGELOG.md | 4 ++++ server/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md index 3ce580d40..9506a65f3 100644 --- a/server/CHANGELOG.md +++ b/server/CHANGELOG.md @@ -1,5 +1,9 @@ # Bash Language Server +## 4.5.0 + +- Include 30 snippets for language constructs (e.g. `if`), builtins (e.g. `test`), expansions (e.g. `[##]`), and external programs (e.g. `sed`) https://github.com/bash-lsp/bash-language-server/pull/683 + ## 4.4.0 - Improve source command parser and include diagnostics when parser fails https://github.com/bash-lsp/bash-language-server/pull/673 diff --git a/server/package.json b/server/package.json index 451e3f9c5..69841ae5d 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.4.0", + "version": "4.5.0", "main": "./out/server.js", "typings": "./out/server.d.ts", "bin": {