Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit c23778c

Browse files
authored
Merge pull request #237 from bash-lsp/word-at-point-improvements
Code completion improvements
2 parents e441064 + e6f759b commit c23778c

File tree

11 files changed

+125
-92
lines changed

11 files changed

+125
-92
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: 40 additions & 11 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,
@@ -273,9 +276,11 @@ export default class BashServer {
273276
): LSP.DocumentHighlight[] | null {
274277
const word = this.getWordAtPoint(params)
275278
this.logRequest({ request: 'onDocumentHighlight', params, word })
279+
276280
if (!word) {
277-
return null
281+
return []
278282
}
283+
279284
return this.analyzer
280285
.findOccurrences(params.textDocument.uri, word)
281286
.map(n => ({ range: n.range }))
@@ -291,22 +296,51 @@ export default class BashServer {
291296
}
292297

293298
private onCompletion(params: LSP.TextDocumentPositionParams): BashCompletionItem[] {
294-
const word = this.getWordAtPoint(params)
299+
const word = this.getWordAtPoint({
300+
...params,
301+
position: {
302+
line: params.position.line,
303+
// Go one character back to get completion on the current word
304+
character: Math.max(params.position.character - 1, 0),
305+
},
306+
})
295307
this.logRequest({ request: 'onCompletion', params, word })
296308

309+
if (word && word.startsWith('#')) {
310+
// Inside a comment block
311+
return []
312+
}
313+
297314
const currentUri = params.textDocument.uri
298315

316+
// TODO: an improvement here would be to detect if the current word is
317+
// not only a parameter expansion prefix, but also if the word is actually
318+
// inside a parameter expansion (e.g. auto completing on a word $MY_VARIA).
319+
const shouldCompleteOnVariables = word
320+
? PARAMETER_EXPANSION_PREFIXES.has(word)
321+
: false
322+
299323
const symbolCompletions =
300324
word === null
301325
? []
302326
: this.getCompletionItemsForSymbols({
303-
symbols: this.analyzer.findSymbolsMatchingWord({
304-
exactMatch: false,
305-
word,
306-
}),
327+
symbols: shouldCompleteOnVariables
328+
? this.analyzer.getAllVariableSymbols()
329+
: this.analyzer.findSymbolsMatchingWord({
330+
exactMatch: false,
331+
word,
332+
}),
307333
currentUri,
308334
})
309335

336+
if (shouldCompleteOnVariables) {
337+
// In case we auto complete on a word that starts a parameter expansion,
338+
// we do not return anything else than variable/parameter suggestions.
339+
// Note: that LSP clients should not call onCompletion in the middle
340+
// of a word, so the following should work for client.
341+
return symbolCompletions
342+
}
343+
310344
const reservedWordsCompletions = ReservedWords.LIST.map(reservedWord => ({
311345
label: reservedWord,
312346
kind: LSP.SymbolKind.Interface, // ??
@@ -347,11 +381,6 @@ export default class BashServer {
347381
]
348382

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

‎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.

‎server/src/util/tree-sitter.ts

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Range } from 'vscode-languageserver/lib/main'
2-
import { Point,SyntaxNode } from 'web-tree-sitter'
2+
import { SyntaxNode } from 'web-tree-sitter'
33

44
export function forEach(node: SyntaxNode, cb: (n: SyntaxNode) => void) {
55
cb(node)
@@ -52,57 +52,3 @@ export function findParent(
5252
}
5353
return null
5454
}
55-
56-
/**
57-
* Given a tree and a point, try to find the named leaf node that the point corresponds to.
58-
* This is a helper for wordAtPoint, useful in cases where the point occurs at the boundary of
59-
* a word so the normal behavior of "namedDescendantForPosition" does not find the desired leaf.
60-
* For example, if you do
61-
* > (new Parser()).setLanguage(bash).parse("echo 42").rootNode.descendantForIndex(4).text
62-
* then you get 'echo 42', not the leaf node for 'echo'.
63-
*
64-
* TODO: the need for this function might reveal a flaw in tree-sitter-bash.
65-
*/
66-
export function namedLeafDescendantForPosition(
67-
point: Point,
68-
rootNode: SyntaxNode,
69-
): SyntaxNode | null {
70-
const node = rootNode.namedDescendantForPosition(point)
71-
72-
if (node.childCount === 0) {
73-
return node
74-
} else {
75-
// The node wasn't a leaf. Try to figure out what word we should use.
76-
const nodeToUse = searchForLeafNode(point, node)
77-
if (nodeToUse) {
78-
return nodeToUse
79-
} else {
80-
return null
81-
}
82-
}
83-
}
84-
85-
function searchForLeafNode(point: Point, parent: SyntaxNode): SyntaxNode | null {
86-
let child = parent.firstNamedChild
87-
88-
while (child) {
89-
if (
90-
pointsEqual(child.startPosition, point) ||
91-
pointsEqual(child.endPosition, point)
92-
) {
93-
if (child.childCount === 0) {
94-
return child
95-
} else {
96-
return searchForLeafNode(point, child)
97-
}
98-
}
99-
100-
child = child.nextNamedSibling
101-
}
102-
103-
return null
104-
}
105-
106-
function pointsEqual(point1: Point, point2: Point) {
107-
return point1.row === point2.row && point1.column === point2.column
108-
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /