Skip to content

Commit 573b737

Browse files
Merge pull request #3 from bash-lsp/master
merges original master into this fork head
2 parents bc0a31a + 016291a commit 573b737

File tree

14 files changed

+155
-112
lines changed

14 files changed

+155
-112
lines changed

server/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# Bash Language Server
22

3+
## 1.16.0
4+
5+
* Improved completion handler for parameter expansions (https://github.com/bash-lsp/bash-language-server/pull/237)
6+
37
## 1.15.0
48

59
* Use comments above symbols for documentation (https://github.com/bash-lsp/bash-language-server/pull/234, https://github.com/bash-lsp/bash-language-server/pull/235)
610

7-
811
## 1.14.0
912

1013
* onHover and onCompletion documentation improvements (https://github.com/bash-lsp/bash-language-server/pull/230)

server/package.json

Lines changed: 1 addition & 1 deletion
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": "1.15.0",
6+
"version": "1.16.0",
77
"publisher": "mads-hartmann",
88
"main": "./out/server.js",
99
"typings": "./out/server.d.ts",

server/src/__tests__/__snapshots__/server.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ exports[`server initializes and responds to capabilities 1`] = `
44
Object {
55
"completionProvider": Object {
66
"resolveProvider": true,
7+
"triggerCharacters": Array [
8+
"$",
9+
"{",
10+
],
711
},
812
"definitionProvider": true,
913
"documentHighlightProvider": true,

server/src/__tests__/analyzer.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,26 @@ describe('findSymbolsForFile', () => {
8787
describe('wordAtPoint', () => {
8888
it('returns current word at a given point', () => {
8989
analyzer.analyze(CURRENT_URI, FIXTURES.INSTALL)
90+
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 0)).toEqual(null)
91+
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 1)).toEqual(null)
92+
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 2)).toEqual(null)
93+
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 3)).toEqual(null)
94+
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 4)).toEqual('rm')
9095
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 5)).toEqual('rm')
96+
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 6)).toEqual(null)
97+
expect(analyzer.wordAtPoint(CURRENT_URI, 25, 7)).toEqual('npm-install-')
9198

92-
// FIXME: grammar issue: else is not found
93-
// expect(analyzer.wordAtPoint(CURRENT_URI, 24, 5)).toEqual('else')
99+
expect(analyzer.wordAtPoint(CURRENT_URI, 24, 2)).toEqual('else')
100+
expect(analyzer.wordAtPoint(CURRENT_URI, 24, 3)).toEqual('else')
101+
expect(analyzer.wordAtPoint(CURRENT_URI, 24, 5)).toEqual('else')
102+
expect(analyzer.wordAtPoint(CURRENT_URI, 24, 7)).toEqual(null)
94103

95104
expect(analyzer.wordAtPoint(CURRENT_URI, 30, 1)).toEqual(null)
96105

106+
expect(analyzer.wordAtPoint(CURRENT_URI, 30, 2)).toEqual('ret')
97107
expect(analyzer.wordAtPoint(CURRENT_URI, 30, 3)).toEqual('ret')
98108
expect(analyzer.wordAtPoint(CURRENT_URI, 30, 4)).toEqual('ret')
99-
expect(analyzer.wordAtPoint(CURRENT_URI, 30, 5)).toEqual('ret')
109+
expect(analyzer.wordAtPoint(CURRENT_URI, 30, 5)).toEqual('=')
100110

101111
expect(analyzer.wordAtPoint(CURRENT_URI, 38, 5)).toEqual('configures')
102112
})

server/src/__tests__/server.test.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ describe('server', () => {
181181
{} as any,
182182
)
183183

184-
expect(result2).toMatchInlineSnapshot(`null`)
184+
expect(result2).toMatchInlineSnapshot(`Array []`)
185185
})
186186

187187
it('responds to onWorkspaceSymbol', async () => {
@@ -279,17 +279,17 @@ describe('server', () => {
279279
uri: FIXTURE_URI.INSTALL,
280280
},
281281
position: {
282-
// else
283-
line: 24,
284-
character: 5,
282+
// empty space
283+
line: 26,
284+
character: 0,
285285
},
286286
},
287287
{} as any,
288288
{} as any,
289289
)
290290

291291
// Entire list
292-
expect(result && 'length' in result && result.length > 50).toBe(true)
292+
expect(result && 'length' in result && result.length).toBeGreaterThanOrEqual(50)
293293
})
294294

295295
it('responds to onCompletion with empty list when word is a comment', async () => {
@@ -418,4 +418,31 @@ Helper function to add a user",
418418
]
419419
`)
420420
})
421+
422+
it('responds to onCompletion with all variables when starting to expand parameters', async () => {
423+
const { connection, server } = await initializeServer()
424+
server.register(connection)
425+
426+
const onCompletion = connection.onCompletion.mock.calls[0][0]
427+
428+
const result: any = await onCompletion(
429+
{
430+
textDocument: {
431+
uri: FIXTURE_URI.SOURCING,
432+
},
433+
position: {
434+
// $
435+
line: 14,
436+
character: 7,
437+
},
438+
},
439+
{} as any,
440+
{} as any,
441+
)
442+
443+
// they are all variables
444+
expect(Array.from(new Set(result.map((item: any) => item.kind)))).toEqual([
445+
lsp.CompletionItemKind.Variable,
446+
])
447+
})
421448
})

server/src/analyser.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -371,32 +371,19 @@ export default class Analyzer {
371371
*/
372372
public wordAtPoint(uri: string, line: number, column: number): string | null {
373373
const document = this.uriToTreeSitterTrees[uri]
374-
const contents = this.uriToFileContent[uri]
375374

376375
if (!document.rootNode) {
377376
// Check for lacking rootNode (due to failed parse?)
378377
return null
379378
}
380379

381-
const point = { row: line, column }
382-
383-
const node = TreeSitterUtil.namedLeafDescendantForPosition(point, document.rootNode)
380+
const node = document.rootNode.descendantForPosition({ row: line, column })
384381

385-
if (!node) {
382+
if (!node || node.childCount > 0 || node.text.trim() === '') {
386383
return null
387384
}
388385

389-
const start = node.startIndex
390-
const end = node.endIndex
391-
const name = contents.slice(start, end)
392-
393-
// Hack. Might be a problem with the grammar.
394-
// TODO: Document this with a test case
395-
if (name.endsWith('=')) {
396-
return name.slice(0, name.length - 1)
397-
}
398-
399-
return name
386+
return node.text.trim()
400387
}
401388

402389
/**
@@ -447,6 +434,10 @@ export default class Analyzer {
447434
return null
448435
}
449436

437+
public getAllVariableSymbols(): LSP.SymbolInformation[] {
438+
return this.getAllSymbols().filter(symbol => symbol.kind === LSP.SymbolKind.Variable)
439+
}
440+
450441
private getAllSymbols(): LSP.SymbolInformation[] {
451442
// NOTE: this could be cached, it takes < 1 ms to generate for a project with 250 bash files...
452443
const symbols: LSP.SymbolInformation[] = []

server/src/server.ts

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { BashCompletionItem, CompletionItemDataType } from './types'
1313
import { uniqueBasedOnHash } from './util/array'
1414
import { getShellDocumentation } from './util/sh'
1515

16+
const PARAMETER_EXPANSION_PREFIXES = new Set(['$', '${'])
17+
1618
/**
1719
* The BashServer glues together the separate components to implement
1820
* the various parts of the Language Server Protocol.
@@ -100,6 +102,7 @@ export default class BashServer {
100102
textDocumentSync: LSP.TextDocumentSyncKind.Full,
101103
completionProvider: {
102104
resolveProvider: true,
105+
triggerCharacters: ['$', '{'],
103106
},
104107
hoverProvider: true,
105108
documentHighlightProvider: true,
@@ -198,22 +201,28 @@ export default class BashServer {
198201
const explainshellEndpoint = config.getExplainshellEndpoint()
199202
if (explainshellEndpoint) {
200203
this.connection.console.log(`Query ${explainshellEndpoint}`)
201-
const response = await this.analyzer.getExplainshellDocumentation({
202-
params,
203-
endpoint: explainshellEndpoint,
204-
})
204+
try {
205+
const response = await this.analyzer.getExplainshellDocumentation({
206+
params,
207+
endpoint: explainshellEndpoint,
208+
})
205209

206-
if (response.status === 'error') {
207-
this.connection.console.log(
208-
`getExplainshellDocumentation returned: ${JSON.stringify(response, null, 4)}`,
209-
)
210-
} else {
211-
return {
212-
contents: {
213-
kind: 'markdown',
214-
value: new TurndownService().turndown(response.helpHTML),
215-
},
210+
if (response.status === 'error') {
211+
this.connection.console.log(
212+
`getExplainshellDocumentation returned: ${JSON.stringify(response, null, 4)}`,
213+
)
214+
} else {
215+
return {
216+
contents: {
217+
kind: 'markdown',
218+
value: new TurndownService().turndown(response.helpHTML),
219+
},
220+
}
216221
}
222+
} catch (error) {
223+
this.connection.console.warn(
224+
`getExplainshellDocumentation exception: ${error.message}`,
225+
)
217226
}
218227
}
219228

@@ -273,9 +282,11 @@ export default class BashServer {
273282
): LSP.DocumentHighlight[] | null {
274283
const word = this.getWordAtPoint(params)
275284
this.logRequest({ request: 'onDocumentHighlight', params, word })
285+
276286
if (!word) {
277-
return null
287+
return []
278288
}
289+
279290
return this.analyzer
280291
.findOccurrences(params.textDocument.uri, word)
281292
.map(n => ({ range: n.range }))
@@ -291,22 +302,51 @@ export default class BashServer {
291302
}
292303

293304
private onCompletion(params: LSP.TextDocumentPositionParams): BashCompletionItem[] {
294-
const word = this.getWordAtPoint(params)
305+
const word = this.getWordAtPoint({
306+
...params,
307+
position: {
308+
line: params.position.line,
309+
// Go one character back to get completion on the current word
310+
character: Math.max(params.position.character - 1, 0),
311+
},
312+
})
295313
this.logRequest({ request: 'onCompletion', params, word })
296314

315+
if (word && word.startsWith('#')) {
316+
// Inside a comment block
317+
return []
318+
}
319+
297320
const currentUri = params.textDocument.uri
298321

322+
// TODO: an improvement here would be to detect if the current word is
323+
// not only a parameter expansion prefix, but also if the word is actually
324+
// inside a parameter expansion (e.g. auto completing on a word $MY_VARIA).
325+
const shouldCompleteOnVariables = word
326+
? PARAMETER_EXPANSION_PREFIXES.has(word)
327+
: false
328+
299329
const symbolCompletions =
300330
word === null
301331
? []
302332
: this.getCompletionItemsForSymbols({
303-
symbols: this.analyzer.findSymbolsMatchingWord({
304-
exactMatch: false,
305-
word,
306-
}),
333+
symbols: shouldCompleteOnVariables
334+
? this.analyzer.getAllVariableSymbols()
335+
: this.analyzer.findSymbolsMatchingWord({
336+
exactMatch: false,
337+
word,
338+
}),
307339
currentUri,
308340
})
309341

342+
if (shouldCompleteOnVariables) {
343+
// In case we auto complete on a word that starts a parameter expansion,
344+
// we do not return anything else than variable/parameter suggestions.
345+
// Note: that LSP clients should not call onCompletion in the middle
346+
// of a word, so the following should work for client.
347+
return symbolCompletions
348+
}
349+
310350
const reservedWordsCompletions = ReservedWords.LIST.map(reservedWord => ({
311351
label: reservedWord,
312352
kind: LSP.SymbolKind.Interface, // ??
@@ -347,11 +387,6 @@ export default class BashServer {
347387
]
348388

349389
if (word) {
350-
if (word.startsWith('#')) {
351-
// Inside a comment block
352-
return []
353-
}
354-
355390
// Filter to only return suffixes of the current word
356391
return allCompletions.filter(item => item.label.startsWith(word))
357392
}

server/src/util/__tests__/sh.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ describe('getDocumentation', () => {
2020
expect(lines[1]).toContain('list directory contents')
2121
})
2222

23+
it('skips documentation for some builtins', async () => {
24+
const result = await sh.getShellDocumentation({ word: 'else' })
25+
expect(result).toEqual(null)
26+
})
27+
2328
it('sanity checks the given word', async () => {
2429
await expect(sh.getShellDocumentation({ word: 'ls foo' })).rejects.toThrow()
2530
})

server/src/util/sh.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export function execShellScript(body: string): Promise<string> {
2424
})
2525
}
2626

27+
// Currently only reserved words where documentation doesn't make sense.
28+
// At least on OS X these just return the builtin man. On ubuntu there
29+
// are no documentaiton for them.
30+
const WORDS_WITHOUT_DOCUMENTATION = new Set([
31+
'else',
32+
'fi',
33+
'then',
34+
'esac',
35+
'elif',
36+
'done',
37+
])
38+
2739
/**
2840
* Get documentation for the given word by usingZZ help and man.
2941
*/
@@ -36,6 +48,10 @@ export async function getShellDocumentationWithoutCache({
3648
throw new Error(`lookupDocumentation should be given a word, received "${word}"`)
3749
}
3850

51+
if (WORDS_WITHOUT_DOCUMENTATION.has(word)) {
52+
return null
53+
}
54+
3955
const DOCUMENTATION_COMMANDS = [
4056
{ type: 'help', command: `help ${word} | col -bx` },
4157
// We have experimented with setting MANWIDTH to different values for reformatting.

0 commit comments

Comments
 (0)