Skip to content

Commit 509f0b4

Browse files
Merge pull request #1 from bash-lsp/master
brings in upstream changes
2 parents d03a676 + ae9843a commit 509f0b4

File tree

9 files changed

+227
-50
lines changed

9 files changed

+227
-50
lines changed

server/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Bash Language Server
22

3+
## 1.15.0
4+
5+
* 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)
6+
7+
38
## 1.14.0
49

510
* 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.14.0",
6+
"version": "1.15.0",
77
"publisher": "mads-hartmann",
88
"main": "./out/server.js",
99
"typings": "./out/server.d.ts",

server/src/__tests__/analyzer.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,35 @@ describe('findSymbolCompletions', () => {
192192
})
193193
})
194194

195+
describe('commentsAbove', () => {
196+
it('returns a string of a comment block above a line', () => {
197+
analyzer.analyze(CURRENT_URI, FIXTURES.COMMENT_DOC)
198+
expect(analyzer.commentsAbove(CURRENT_URI, 22)).toEqual('doc for func_one')
199+
})
200+
201+
it('handles line breaks in comments', () => {
202+
analyzer.analyze(CURRENT_URI, FIXTURES.COMMENT_DOC)
203+
expect(analyzer.commentsAbove(CURRENT_URI, 28)).toEqual(
204+
'doc for func_two\nhas two lines',
205+
)
206+
})
207+
208+
it('only returns connected comments', () => {
209+
analyzer.analyze(CURRENT_URI, FIXTURES.COMMENT_DOC)
210+
expect(analyzer.commentsAbove(CURRENT_URI, 36)).toEqual('doc for func_three')
211+
})
212+
213+
it('returns null if no comment found', () => {
214+
analyzer.analyze(CURRENT_URI, FIXTURES.COMMENT_DOC)
215+
expect(analyzer.commentsAbove(CURRENT_URI, 45)).toEqual(null)
216+
})
217+
218+
it('works for variables', () => {
219+
analyzer.analyze(CURRENT_URI, FIXTURES.COMMENT_DOC)
220+
expect(analyzer.commentsAbove(CURRENT_URI, 42)).toEqual('works for variables')
221+
})
222+
})
223+
195224
describe('fromRoot', () => {
196225
it('initializes an analyzer from a root', async () => {
197226
const parser = await initializeParser()
@@ -210,7 +239,8 @@ describe('fromRoot', () => {
210239

211240
expect(connection.window.showWarningMessage).not.toHaveBeenCalled()
212241

213-
const FIXTURE_FILES_MATCHING_GLOB = 10
242+
// if you add a .sh file to testing/fixtures, update this value
243+
const FIXTURE_FILES_MATCHING_GLOB = 11
214244

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

server/src/__tests__/server.test.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,33 @@ describe('server', () => {
7676
})
7777
})
7878

79+
it('responds to onHover with function documentation extracted from comments', async () => {
80+
const { connection, server } = await initializeServer()
81+
server.register(connection)
82+
83+
const onHover = connection.onHover.mock.calls[0][0]
84+
85+
const result = await onHover(
86+
{
87+
textDocument: {
88+
uri: FIXTURE_URI.COMMENT_DOC,
89+
},
90+
position: {
91+
line: 17,
92+
character: 0,
93+
},
94+
},
95+
{} as any,
96+
{} as any,
97+
)
98+
99+
expect(result).toBeDefined()
100+
expect(result).toEqual({
101+
contents:
102+
'Function defined on line 8\n\nthis is a comment\ndescribing the function\nhello_world\nthis function takes two arguments',
103+
})
104+
})
105+
79106
it('responds to onDocumentHighlight', async () => {
80107
const { connection, server } = await initializeServer()
81108
server.register(connection)
@@ -225,7 +252,7 @@ describe('server', () => {
225252
)
226253

227254
// Limited set (not using snapshot due to different executables on CI and locally)
228-
expect(result && 'length' in result && result.length < 5).toBe(true)
255+
expect(result && 'length' in result && result.length < 8).toBe(true)
229256
expect(result).toEqual(
230257
expect.arrayContaining([
231258
{
@@ -340,18 +367,20 @@ describe('server', () => {
340367
)
341368

342369
expect(resultFunction).toMatchInlineSnapshot(`
343-
Array [
344-
Object {
345-
"data": Object {
346-
"name": "add_a_user",
347-
"type": 3,
348-
},
349-
"documentation": "Function defined in ../issue101.sh",
350-
"kind": 3,
351-
"label": "add_a_user",
352-
},
353-
]
354-
`)
370+
Array [
371+
Object {
372+
"data": Object {
373+
"name": "add_a_user",
374+
"type": 3,
375+
},
376+
"documentation": "Function defined in ../issue101.sh
377+
378+
Helper function to add a user",
379+
"kind": 3,
380+
"label": "add_a_user",
381+
},
382+
]
383+
`)
355384
})
356385

357386
it('responds to onCompletion with local symbol when word is found in multiple files', async () => {

server/src/analyser.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,54 @@ export default class Analyzer {
399399
return name
400400
}
401401

402+
/**
403+
* Find a block of comments above a line position
404+
*/
405+
public commentsAbove(uri: string, line: number): string | null {
406+
const doc = this.uriToTextDocument[uri]
407+
408+
const commentBlock = []
409+
410+
// start from the line above
411+
let commentBlockIndex = line - 1
412+
413+
// will return the comment string without the comment '#'
414+
// and without leading whitespace, or null if the line 'l'
415+
// is not a comment line
416+
const getComment = (l: string): null | string => {
417+
// this regexp has to be defined within the function
418+
const commentRegExp = /^\s*#\s*(.*)/g
419+
const matches = commentRegExp.exec(l)
420+
return matches ? matches[1].trim() : null
421+
}
422+
423+
let currentLine = doc.getText({
424+
start: { line: commentBlockIndex, character: 0 },
425+
end: { line: commentBlockIndex + 1, character: 0 },
426+
})
427+
428+
// iterate on every line above and including
429+
// the current line until getComment returns null
430+
let currentComment: string | null = ''
431+
while ((currentComment = getComment(currentLine))) {
432+
commentBlock.push(currentComment)
433+
commentBlockIndex -= 1
434+
currentLine = doc.getText({
435+
start: { line: commentBlockIndex, character: 0 },
436+
end: { line: commentBlockIndex + 1, character: 0 },
437+
})
438+
}
439+
440+
if (commentBlock.length) {
441+
// since we searched from bottom up, we then reverse
442+
// the lines so that it reads top down.
443+
return commentBlock.reverse().join('\n')
444+
}
445+
446+
// no comments found above line:
447+
return null
448+
}
449+
402450
private getAllSymbols(): LSP.SymbolInformation[] {
403451
// NOTE: this could be cached, it takes < 1 ms to generate for a project with 250 bash files...
404452
const symbols: LSP.SymbolInformation[] = []

server/src/server.ts

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,54 @@ export default class BashServer {
135135
)
136136
}
137137

138+
private getDocumentationForSymbol({
139+
currentUri,
140+
symbol,
141+
}: {
142+
symbol: LSP.SymbolInformation
143+
currentUri: string
144+
}): string {
145+
const symbolUri = symbol.location.uri
146+
const symbolStarLine = symbol.location.range.start.line
147+
148+
const commentAboveSymbol = this.analyzer.commentsAbove(symbolUri, symbolStarLine)
149+
const symbolDocumentation = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : ''
150+
151+
return symbolUri !== currentUri
152+
? `${symbolKindToDescription(symbol.kind)} defined in ${path.relative(
153+
currentUri,
154+
symbolUri,
155+
)}${symbolDocumentation}`
156+
: `${symbolKindToDescription(symbol.kind)} defined on line ${symbolStarLine +
157+
1}${symbolDocumentation}`
158+
}
159+
160+
private getCompletionItemsForSymbols({
161+
symbols,
162+
currentUri,
163+
}: {
164+
symbols: LSP.SymbolInformation[]
165+
currentUri: string
166+
}): BashCompletionItem[] {
167+
return deduplicateSymbols({ symbols, currentUri }).map(
168+
(symbol: LSP.SymbolInformation) => ({
169+
label: symbol.name,
170+
kind: symbolKindToCompletionKind(symbol.kind),
171+
data: {
172+
name: symbol.name,
173+
type: CompletionItemDataType.Symbol,
174+
},
175+
documentation:
176+
symbol.location.uri !== currentUri
177+
? this.getDocumentationForSymbol({
178+
currentUri,
179+
symbol,
180+
})
181+
: undefined,
182+
}),
183+
)
184+
}
185+
138186
private async onHover(
139187
params: LSP.TextDocumentPositionParams,
140188
): Promise<LSP.Hover | null> {
@@ -190,13 +238,7 @@ export default class BashServer {
190238
// do not return hover referencing for the current line
191239
.filter(symbol => symbol.location.range.start.line !== params.position.line)
192240
.map((symbol: LSP.SymbolInformation) =>
193-
symbol.location.uri !== currentUri
194-
? `${symbolKindToDescription(symbol.kind)} defined in ${path.relative(
195-
currentUri,
196-
symbol.location.uri,
197-
)}`
198-
: `${symbolKindToDescription(symbol.kind)} defined on line ${symbol.location
199-
.range.start.line + 1}`,
241+
this.getDocumentationForSymbol({ currentUri, symbol }),
200242
)
201243

202244
if (symbolDocumentation.length === 1) {
@@ -257,7 +299,7 @@ export default class BashServer {
257299
const symbolCompletions =
258300
word === null
259301
? []
260-
: getCompletionItemsForSymbols({
302+
: this.getCompletionItemsForSymbols({
261303
symbols: this.analyzer.findSymbolsMatchingWord({
262304
exactMatch: false,
263305
word,
@@ -380,32 +422,6 @@ function deduplicateSymbols({
380422
return uniqueBasedOnHash([...symbolsCurrentFile, ...symbolsOtherFiles], getSymbolId)
381423
}
382424

383-
function getCompletionItemsForSymbols({
384-
symbols,
385-
currentUri,
386-
}: {
387-
symbols: LSP.SymbolInformation[]
388-
currentUri: string
389-
}): BashCompletionItem[] {
390-
return deduplicateSymbols({ symbols, currentUri }).map(
391-
(symbol: LSP.SymbolInformation) => ({
392-
label: symbol.name,
393-
kind: symbolKindToCompletionKind(symbol.kind),
394-
data: {
395-
name: symbol.name,
396-
type: CompletionItemDataType.Symbol,
397-
},
398-
documentation:
399-
symbol.location.uri !== currentUri
400-
? `${symbolKindToDescription(symbol.kind)} defined in ${path.relative(
401-
currentUri,
402-
symbol.location.uri,
403-
)}`
404-
: undefined,
405-
}),
406-
)
407-
}
408-
409425
function symbolKindToCompletionKind(s: LSP.SymbolKind): LSP.CompletionItemKind {
410426
switch (s) {
411427
case LSP.SymbolKind.File:

testing/fixtures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const FIXTURE_URI = {
2020
MISSING_NODE: `file://${path.join(FIXTURE_FOLDER, 'missing-node.sh')}`,
2121
PARSE_PROBLEMS: `file://${path.join(FIXTURE_FOLDER, 'parse-problems.sh')}`,
2222
SOURCING: `file://${path.join(FIXTURE_FOLDER, 'sourcing.sh')}`,
23+
COMMENT_DOC: `file://${path.join(FIXTURE_FOLDER, 'comment-doc-on-hover.sh')}`,
2324
}
2425

2526
export const FIXTURE_DOCUMENT = {
@@ -28,6 +29,7 @@ export const FIXTURE_DOCUMENT = {
2829
MISSING_NODE: getDocument(FIXTURE_URI.MISSING_NODE),
2930
PARSE_PROBLEMS: getDocument(FIXTURE_URI.PARSE_PROBLEMS),
3031
SOURCING: getDocument(FIXTURE_URI.SOURCING),
32+
COMMENT_DOC: getDocument(FIXTURE_URI.COMMENT_DOC),
3133
}
3234

3335
export default FIXTURE_DOCUMENT
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
3+
4+
# this is a comment
5+
# describing the function
6+
# hello_world
7+
# this function takes two arguments
8+
hello_world() {
9+
echo "hello world to: $1 and $2"
10+
}
11+
12+
13+
14+
# if the user hovers above the below hello_world invocation
15+
# they should see the comment doc string in a tooltip
16+
# containing the lines 4 - 7 above
17+
18+
hello_world "bob" "sally"
19+
20+
21+
22+
# doc for func_one
23+
func_one() {
24+
echo "func_one"
25+
}
26+
27+
# doc for func_two
28+
# has two lines
29+
func_two() {
30+
echo "func_two"
31+
}
32+
33+
34+
# this is not included
35+
36+
# doc for func_three
37+
func_three() {
38+
echo "func_three"
39+
}
40+
41+
42+
# works for variables
43+
my_var="pizza"
44+
45+
46+
my_other_var="no comments above me :("
47+

testing/fixtures/issue101.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/sh
2-
# A simple script with a function...
32

3+
# Helper function to add a user
44
add_a_user()
55
{
66
USER=$1

0 commit comments

Comments
 (0)