Skip to content

Commit 2b4d8a6

Browse files
authored
Merge pull request #294 from otreblan/master
Complete command line arguments
2 parents a16b288 + bf5e329 commit 2b4d8a6

File tree

7 files changed

+184
-4
lines changed

7 files changed

+184
-4
lines changed

server/src/__tests__/analyzer.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ describe('wordAtPoint', () => {
112112
})
113113
})
114114

115+
describe('commandNameAtPoint', () => {
116+
it('returns current command name at a given point', () => {
117+
analyzer.analyze(CURRENT_URI, FIXTURES.INSTALL)
118+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 15, 0)).toEqual(null)
119+
120+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 2)).toEqual('curl')
121+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 15)).toEqual('curl')
122+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 19)).toEqual('curl')
123+
124+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 26, 4)).toEqual('echo')
125+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 26, 9)).toEqual('echo')
126+
127+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 13)).toEqual('env')
128+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 24)).toEqual('grep')
129+
expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 44)).toEqual('sed')
130+
})
131+
})
132+
115133
describe('findSymbolCompletions', () => {
116134
it('return a list of symbols across the workspace', () => {
117135
analyzer.analyze('install.sh', FIXTURES.INSTALL)
@@ -250,7 +268,7 @@ describe('fromRoot', () => {
250268
expect(connection.window.showWarningMessage).not.toHaveBeenCalled()
251269

252270
// if you add a .sh file to testing/fixtures, update this value
253-
const FIXTURE_FILES_MATCHING_GLOB = 11
271+
const FIXTURE_FILES_MATCHING_GLOB = 12
254272

255273
// Intro, stats on glob, one file skipped due to shebang, and outro
256274
const LOG_LINES = FIXTURE_FILES_MATCHING_GLOB + 4

server/src/__tests__/server.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,41 @@ describe('server', () => {
267267
)
268268
})
269269

270+
it('responds to onCompletion with options list when command name is found', async () => {
271+
const { connection, server } = await initializeServer()
272+
server.register(connection)
273+
274+
const onCompletion = connection.onCompletion.mock.calls[0][0]
275+
276+
const result = await onCompletion(
277+
{
278+
textDocument: {
279+
uri: FIXTURE_URI.OPTIONS,
280+
},
281+
position: {
282+
// grep --line-
283+
line: 2,
284+
character: 12,
285+
},
286+
},
287+
{} as any,
288+
{} as any,
289+
)
290+
291+
expect(result).toEqual(
292+
expect.arrayContaining([
293+
{
294+
data: {
295+
name: expect.stringMatching(RegExp('--line-.*')),
296+
type: CompletionItemDataType.Symbol,
297+
},
298+
kind: expect.any(Number),
299+
label: expect.stringMatching(RegExp('--line-.*')),
300+
},
301+
]),
302+
)
303+
})
304+
270305
it('responds to onCompletion with entire list when no word is found', async () => {
271306
const { connection, server } = await initializeServer()
272307
server.register(connection)

server/src/analyser.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,17 +367,28 @@ export default class Analyzer {
367367
}
368368

369369
/**
370-
* Find the full word at the given point.
370+
* Find the node at the given point.
371371
*/
372-
public wordAtPoint(uri: string, line: number, column: number): string | null {
372+
private nodeAtPoint(
373+
uri: string,
374+
line: number,
375+
column: number,
376+
): Parser.SyntaxNode | null {
373377
const document = this.uriToTreeSitterTrees[uri]
374378

375379
if (!document.rootNode) {
376380
// Check for lacking rootNode (due to failed parse?)
377381
return null
378382
}
379383

380-
const node = document.rootNode.descendantForPosition({ row: line, column })
384+
return document.rootNode.descendantForPosition({ row: line, column })
385+
}
386+
387+
/**
388+
* Find the full word at the given point.
389+
*/
390+
public wordAtPoint(uri: string, line: number, column: number): string | null {
391+
const node = this.nodeAtPoint(uri, line, column)
381392

382393
if (!node || node.childCount > 0 || node.text.trim() === '') {
383394
return null
@@ -386,6 +397,29 @@ export default class Analyzer {
386397
return node.text.trim()
387398
}
388399

400+
/**
401+
* Find the name of the command at the given point.
402+
*/
403+
public commandNameAtPoint(uri: string, line: number, column: number): string | null {
404+
let node = this.nodeAtPoint(uri, line, column)
405+
406+
while (node && node.type !== 'command') {
407+
node = node.parent
408+
}
409+
410+
if (!node) {
411+
return null
412+
}
413+
414+
const firstChild = node.firstNamedChild
415+
416+
if (!firstChild || firstChild.type !== 'command_name') {
417+
return null
418+
}
419+
420+
return firstChild.text.trim()
421+
}
422+
389423
/**
390424
* Find a block of comments above a line position
391425
*/

server/src/get-options.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
3+
DATADIR="$(pkg-config --variable=datadir bash-completion)"
4+
5+
# Exit if bash-completion isn't installed.
6+
if (( $? != 0 ))
7+
then
8+
exit 1
9+
fi
10+
11+
source "$DATADIR/bash-completion/bash_completion"
12+
13+
COMP_LINE="$*"
14+
COMP_WORDS=("$@")
15+
COMP_CWORD="${#COMP_WORDS[@]}"
16+
((COMP_CWORD--))
17+
COMP_POINT="${#COMP_LINE}"
18+
COMP_WORDBREAKS='"'"'><=;|&(:"
19+
20+
_command_offset 0 2> /dev/null
21+
22+
if (( ${#COMPREPLY[@]} == 0 ))
23+
then
24+
# Disabled by default because _longopt executes the program
25+
# to get its options.
26+
if (( ${BASH_LSP_COMPLETE_LONGOPTS} == 1 ))
27+
then
28+
_longopt "${COMP_WORDS[0]}"
29+
fi
30+
fi
31+
32+
printf "%s\t" "${COMPREPLY[@]}"

server/src/server.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import * as Process from 'child_process'
12
import * as path from 'path'
3+
import * as Path from 'path'
24
import * as TurndownService from 'turndown'
35
import * as LSP from 'vscode-languageserver'
46
import { TextDocument } from 'vscode-languageserver-textdocument'
@@ -123,6 +125,16 @@ export default class BashServer {
123125
)
124126
}
125127

128+
private getCommandNameAtPoint(
129+
params: LSP.ReferenceParams | LSP.TextDocumentPositionParams,
130+
): string | null {
131+
return this.analyzer.commandNameAtPoint(
132+
params.textDocument.uri,
133+
params.position.line,
134+
params.position.character,
135+
)
136+
}
137+
126138
private logRequest({
127139
request,
128140
params,
@@ -323,6 +335,22 @@ export default class BashServer {
323335
return []
324336
}
325337

338+
let options: string[] = []
339+
if (word && word.startsWith('-')) {
340+
const commandName = this.getCommandNameAtPoint({
341+
...params,
342+
position: {
343+
line: params.position.line,
344+
// Go one character back to get completion on the current word
345+
character: Math.max(params.position.character - 1, 0),
346+
},
347+
})
348+
349+
if (commandName) {
350+
options = getCommandOptions(commandName, word)
351+
}
352+
}
353+
326354
const currentUri = params.textDocument.uri
327355

328356
// TODO: an improvement here would be to detect if the current word is
@@ -385,11 +413,21 @@ export default class BashServer {
385413
},
386414
}))
387415

416+
const optionsCompletions = options.map(option => ({
417+
label: option,
418+
kind: LSP.SymbolKind.Interface,
419+
data: {
420+
name: option,
421+
type: CompletionItemDataType.Symbol,
422+
},
423+
}))
424+
388425
const allCompletions = [
389426
...reservedWordsCompletions,
390427
...symbolCompletions,
391428
...programCompletions,
392429
...builtinsCompletions,
430+
...optionsCompletions,
393431
]
394432

395433
if (word) {
@@ -531,3 +569,21 @@ const getMarkdownContent = (documentation: string): LSP.MarkupContent => ({
531569
// Passed as markdown for syntax highlighting
532570
kind: 'markdown' as const,
533571
})
572+
573+
function getCommandOptions(name: string, word: string): string[] {
574+
// TODO: The options could be cached.
575+
const options = Process.spawnSync(Path.join(__dirname, '../src/get-options.sh'), [
576+
name,
577+
word,
578+
])
579+
580+
if (options.status !== 0) {
581+
return []
582+
}
583+
584+
return options.stdout
585+
.toString()
586+
.split('\t')
587+
.map(l => l.trim())
588+
.filter(l => l.length > 0)
589+
}

testing/fixtures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const FIXTURE_URI = {
2121
PARSE_PROBLEMS: `file://${path.join(FIXTURE_FOLDER, 'parse-problems.sh')}`,
2222
SOURCING: `file://${path.join(FIXTURE_FOLDER, 'sourcing.sh')}`,
2323
COMMENT_DOC: `file://${path.join(FIXTURE_FOLDER, 'comment-doc-on-hover.sh')}`,
24+
OPTIONS: `file://${path.join(FIXTURE_FOLDER, 'options.sh')}`,
2425
}
2526

2627
export const FIXTURE_DOCUMENT = {
@@ -30,6 +31,7 @@ export const FIXTURE_DOCUMENT = {
3031
PARSE_PROBLEMS: getDocument(FIXTURE_URI.PARSE_PROBLEMS),
3132
SOURCING: getDocument(FIXTURE_URI.SOURCING),
3233
COMMENT_DOC: getDocument(FIXTURE_URI.COMMENT_DOC),
34+
OPTIONS: getDocument(FIXTURE_URI.OPTIONS),
3335
}
3436

3537
export default FIXTURE_DOCUMENT

testing/fixtures/options.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
grep -
2+
grep --
3+
grep --line-

0 commit comments

Comments
 (0)