Skip to content

Commit fd9e56c

Browse files
authored
Merge pull request #683 from bash-lsp/snippets-v2
Move snippets from vscode extension to server
2 parents 7ca8ad3 + 3c1ba1f commit fd9e56c

File tree

10 files changed

+211
-270
lines changed

10 files changed

+211
-270
lines changed

Diff for: server/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Bash Language Server
22

3+
## 4.5.0
4+
5+
- 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
6+
37
## 4.4.0
48

59
- Improve source command parser and include diagnostics when parser fails https://github.com/bash-lsp/bash-language-server/pull/673

Diff for: server/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "A language server for Bash",
44
"author": "Mads Hartmann",
55
"license": "MIT",
6-
"version": "4.4.0",
6+
"version": "4.5.0",
77
"main": "./out/server.js",
88
"typings": "./out/server.d.ts",
99
"bin": {

Diff for: server/src/__tests__/server.test.ts

-9
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,6 @@ describe('server', () => {
484484
expect.arrayContaining([
485485
{
486486
data: {
487-
name: 'rm',
488487
type: CompletionItemDataType.Executable,
489488
},
490489
kind: expect.any(Number),
@@ -634,7 +633,6 @@ describe('server', () => {
634633
Array [
635634
Object {
636635
"data": Object {
637-
"name": "BLUE",
638636
"type": 3,
639637
},
640638
"documentation": Object {
@@ -666,7 +664,6 @@ describe('server', () => {
666664
Array [
667665
Object {
668666
"data": Object {
669-
"name": "add_a_user",
670667
"type": 3,
671668
},
672669
"documentation": Object {
@@ -708,7 +705,6 @@ describe('server', () => {
708705
Array [
709706
Object {
710707
"data": Object {
711-
"name": "BOLD",
712708
"type": 3,
713709
},
714710
"documentation": undefined,
@@ -747,7 +743,6 @@ describe('server', () => {
747743
Array [
748744
Object {
749745
"data": Object {
750-
"name": "BOLD",
751746
"type": 3,
752747
},
753748
"documentation": undefined,
@@ -756,7 +751,6 @@ describe('server', () => {
756751
},
757752
Object {
758753
"data": Object {
759-
"name": "RED",
760754
"type": 3,
761755
},
762756
"documentation": Object {
@@ -768,7 +762,6 @@ describe('server', () => {
768762
},
769763
Object {
770764
"data": Object {
771-
"name": "GREEN",
772765
"type": 3,
773766
},
774767
"documentation": Object {
@@ -780,7 +773,6 @@ describe('server', () => {
780773
},
781774
Object {
782775
"data": Object {
783-
"name": "BLUE",
784776
"type": 3,
785777
},
786778
"documentation": Object {
@@ -792,7 +784,6 @@ describe('server', () => {
792784
},
793785
Object {
794786
"data": Object {
795-
"name": "RESET",
796787
"type": 3,
797788
},
798789
"documentation": Object {

Diff for: server/src/__tests__/snippets.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { MarkupContent } from 'vscode-languageserver'
2+
3+
import { SNIPPETS } from '../snippets'
4+
5+
describe('snippets', () => {
6+
it('should have unique labels', () => {
7+
const labels = SNIPPETS.map((snippet) => snippet.label)
8+
const uniqueLabels = new Set(labels)
9+
expect(labels.length).toBe(uniqueLabels.size)
10+
})
11+
12+
SNIPPETS.forEach(({ label, documentation }) => {
13+
it(`contains the label in the documentation for "${label}"`, () => {
14+
const stringDocumentation = (documentation as MarkupContent)?.value
15+
expect(stringDocumentation).toBeDefined()
16+
if (stringDocumentation) {
17+
expect(stringDocumentation).toContain(label)
18+
const secondLine = stringDocumentation.split('\n')[1]
19+
try {
20+
expect(
21+
secondLine.startsWith(label) || secondLine.startsWith(`[${label}]`),
22+
).toBe(true)
23+
} catch (error) {
24+
// eslint-disable-next-line no-console
25+
console.error(`Did not start with label: ${label}`, secondLine)
26+
throw error
27+
}
28+
}
29+
})
30+
})
31+
})

Diff for: server/src/server.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Executables from './executables'
1414
import { initializeParser } from './parser'
1515
import * as ReservedWords from './reserved-words'
1616
import { Linter } from './shellcheck'
17+
import { SNIPPETS } from './snippets'
1718
import { BashCompletionItem, CompletionItemDataType } from './types'
1819
import { uniqueBasedOnHash } from './util/array'
1920
import { logger, setLogConnection, setLogLevel } from './util/logger'
@@ -344,7 +345,6 @@ export default class BashServer {
344345
label: symbol.name,
345346
kind: symbolKindToCompletionKind(symbol.kind),
346347
data: {
347-
name: symbol.name,
348348
type: CompletionItemDataType.Symbol,
349349
},
350350
documentation:
@@ -503,7 +503,6 @@ export default class BashServer {
503503
label: reservedWord,
504504
kind: LSP.CompletionItemKind.Keyword,
505505
data: {
506-
name: reservedWord,
507506
type: CompletionItemDataType.ReservedWord,
508507
},
509508
}))
@@ -516,7 +515,6 @@ export default class BashServer {
516515
label: executable,
517516
kind: LSP.CompletionItemKind.Function,
518517
data: {
519-
name: executable,
520518
type: CompletionItemDataType.Executable,
521519
},
522520
}
@@ -526,7 +524,6 @@ export default class BashServer {
526524
label: builtin,
527525
kind: LSP.CompletionItemKind.Function,
528526
data: {
529-
name: builtin,
530527
type: CompletionItemDataType.Builtin,
531528
},
532529
}))
@@ -535,7 +532,6 @@ export default class BashServer {
535532
label: option,
536533
kind: LSP.CompletionItemKind.Constant,
537534
data: {
538-
name: option,
539535
type: CompletionItemDataType.Symbol,
540536
},
541537
}))
@@ -546,6 +542,7 @@ export default class BashServer {
546542
...programCompletions,
547543
...builtinsCompletions,
548544
...optionsCompletions,
545+
...SNIPPETS,
549546
]
550547

551548
if (word) {
@@ -560,10 +557,11 @@ export default class BashServer {
560557
item: LSP.CompletionItem,
561558
): Promise<LSP.CompletionItem> {
562559
const {
563-
data: { name, type },
560+
label,
561+
data: { type },
564562
} = item as BashCompletionItem
565563

566-
logger.debug(`onCompletionResolve name=${name} type=${type}`)
564+
logger.debug(`onCompletionResolve label=${label} type=${type}`)
567565

568566
try {
569567
let documentation = null
@@ -573,7 +571,7 @@ export default class BashServer {
573571
type === CompletionItemDataType.Builtin ||
574572
type === CompletionItemDataType.ReservedWord
575573
) {
576-
documentation = await getShellDocumentation({ word: name })
574+
documentation = await getShellDocumentation({ word: label })
577575
}
578576

579577
return documentation

Diff for: server/src/snippets.ts

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* Naming convention for `label`:
3+
* - is always a language keyword, builtin name or expansion symbol like `:-`.
4+
* - If a snippet is for a builtin then builtin name is used.
5+
* - If a snippet is for expansion then expansion symbol is used.
6+
* - If a snippet is for a specific external program like **awk** then program name must be added to `prefix` like this:
7+
* `awk:{{snippet-prefix}}`.
8+
*/
9+
import { CompletionItemKind, InsertTextFormat, MarkupKind } from 'vscode-languageserver'
10+
11+
import { BashCompletionItem } from './types'
12+
13+
export const SNIPPETS: BashCompletionItem[] = [
14+
{
15+
label: 'shebang',
16+
insertText: '#!/usr/bin/env ${1|bash,sh|}',
17+
},
18+
{
19+
label: 'if',
20+
insertText: ['if ${1:command}; then', '\t$0', 'fi'].join('\n'),
21+
},
22+
{
23+
label: 'if-else',
24+
insertText: ['if ${1:command}; then', '\t${2:echo}', 'else', '\t$0', 'fi'].join('\n'),
25+
},
26+
{
27+
label: 'while',
28+
insertText: ['while ${1:command}; do', '\t$0', 'done'].join('\n'),
29+
},
30+
{
31+
label: 'until',
32+
insertText: ['until ${1:command}; do', '\t$0', 'done'].join('\n'),
33+
},
34+
{
35+
label: 'for',
36+
insertText: ['for ${1:variable} in ${2:list}; do', '\t$0', 'done'].join('\n'),
37+
},
38+
{
39+
label: 'function',
40+
insertText: ['${1:function_name}() {', '\t$0', '}'].join('\n'),
41+
},
42+
{
43+
label: 'main',
44+
insertText: ['main() {', '\t$0', '}'].join('\n'),
45+
},
46+
{
47+
documentation: '[:-] expansion',
48+
label: ':-',
49+
insertText: '"\\${${1:variable}:-${2:default}}"',
50+
},
51+
{
52+
documentation: '[:=] expansion',
53+
label: ':=',
54+
insertText: '"\\${${1:variable}:=${2:default}}"',
55+
},
56+
{
57+
documentation: '[:?] expansion',
58+
label: ':?',
59+
insertText: '"\\${${1:variable}:?${2:error_message}}"',
60+
},
61+
{
62+
documentation: '[:+] expansion',
63+
label: ':+',
64+
insertText: '"\\${${1:variable}:+${2:alternative}}"',
65+
},
66+
{
67+
documentation: '[#] expansion',
68+
label: '#',
69+
insertText: '"\\${${1:variable}#${2:pattern}}"',
70+
},
71+
{
72+
documentation: '[##] expansion',
73+
label: '##',
74+
insertText: '"\\${${1:variable}##${2:pattern}}"',
75+
},
76+
{
77+
documentation: '[%] expansion',
78+
label: '%',
79+
insertText: '"\\${${1:variable}%${2:pattern}}"',
80+
},
81+
{
82+
documentation: '[%%] expansion',
83+
label: '%%',
84+
insertText: '"\\${${1:variable}%%${2:pattern}}"',
85+
},
86+
{
87+
documentation: '[..] brace expansion',
88+
label: '..',
89+
insertText: '{${1:from}..${2:to}}',
90+
},
91+
{
92+
label: 'echo',
93+
insertText: 'echo "${1:message}"',
94+
},
95+
{
96+
label: 'printf',
97+
insertText: 'printf \'%s\' "${1:message}"',
98+
},
99+
{
100+
label: 'source',
101+
insertText: 'source "${1:path/to/file}"',
102+
},
103+
{
104+
label: 'alias',
105+
insertText: 'alias ${1:name}=${2:value}',
106+
},
107+
{
108+
label: 'cd',
109+
insertText: 'cd "${1:path/to/directory}"',
110+
},
111+
{
112+
label: 'getopts',
113+
insertText: 'getopts ${1:optstring} ${2:name}',
114+
},
115+
{
116+
label: 'jobs',
117+
insertText: 'jobs -x ${1:command}',
118+
},
119+
{
120+
label: 'kill',
121+
insertText: 'kill ${1|-l,-L|}',
122+
},
123+
{
124+
label: 'let',
125+
insertText: 'let ${1:argument}',
126+
},
127+
{
128+
label: 'test',
129+
insertText:
130+
'[[ ${1:argument1} ${2|-ef,-nt,-ot,==,=,!=,=~,<,>,-eq,-ne,-lt,-le,-gt,-ge|} ${3:argument2} ]]',
131+
},
132+
{
133+
documentation: '[dev]ice name',
134+
label: 'dev',
135+
insertText: '/dev/${1|null,stdin,stdout,stderr|}',
136+
},
137+
{
138+
label: 'sed:filter-lines',
139+
insertText:
140+
"sed ${1|--regexp-extended,-E|} ${2|--quiet,-n|} '/${3:pattern}/' ${4:path/to/file}",
141+
},
142+
{
143+
label: 'awk:filter-lines',
144+
insertText: "awk '/${1:pattern}/' ${2:path/to/file}",
145+
},
146+
].map((item) => ({
147+
...item,
148+
documentation: {
149+
value: [
150+
markdownBlock(
151+
`${item.documentation || item.label} (bash-language-server)\n\n`,
152+
'man',
153+
),
154+
markdownBlock(item.insertText, 'bash'),
155+
].join('\n'),
156+
kind: MarkupKind.Markdown,
157+
},
158+
159+
insertTextFormat: InsertTextFormat.Snippet,
160+
data: {
161+
type: CompletionItemKind.Snippet,
162+
},
163+
}))
164+
165+
function markdownBlock(text: string, language: string): string {
166+
const tripleQoute = '```'
167+
return [tripleQoute + language, text, tripleQoute].join('\n')
168+
}

Diff for: server/src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ export enum CompletionItemDataType {
55
Executable,
66
ReservedWord,
77
Symbol,
8+
Snippet,
89
}
910

1011
export interface BashCompletionItem extends LSP.CompletionItem {
1112
data: {
1213
type: CompletionItemDataType
13-
name: string
1414
}
1515
}

Diff for: vscode-client/package.json

-6
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@
2727
],
2828
"main": "./out/extension",
2929
"contributes": {
30-
"snippets": [
31-
{
32-
"language": "shellscript",
33-
"path": "./snippets/snippets.json"
34-
}
35-
],
3630
"configuration": {
3731
"type": "object",
3832
"title": "Bash IDE configuration",

0 commit comments

Comments
 (0)