Skip to content

Commit 2e02f9b

Browse files
authored
Merge pull request #234 from nikita-skobov/feature-comment-docs-onhover
Feature comment docs onhover
2 parents d03a676 + 1e35af1 commit 2e02f9b

File tree

6 files changed

+168
-3
lines changed

6 files changed

+168
-3
lines changed

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: 28 additions & 1 deletion
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
{

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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ export default class BashServer {
180180
return { contents: getMarkdownContent(shellDocumentation) }
181181
}
182182
} else {
183+
const getCommentsAbove = (uri: string, line: number): string => {
184+
const comment = this.analyzer.commentsAbove(uri, line)
185+
return comment ? `\n\n${comment}` : ''
186+
}
187+
183188
const symbolDocumentation = deduplicateSymbols({
184189
symbols: this.analyzer.findSymbolsMatchingWord({
185190
exactMatch: true,
@@ -194,9 +199,15 @@ export default class BashServer {
194199
? `${symbolKindToDescription(symbol.kind)} defined in ${path.relative(
195200
currentUri,
196201
symbol.location.uri,
202+
)}${getCommentsAbove(
203+
symbol.location.uri,
204+
symbol.location.range.start.line,
197205
)}`
198206
: `${symbolKindToDescription(symbol.kind)} defined on line ${symbol.location
199-
.range.start.line + 1}`,
207+
.range.start.line + 1}${getCommentsAbove(
208+
params.textDocument.uri,
209+
symbol.location.range.start.line,
210+
)}`,
200211
)
201212

202213
if (symbolDocumentation.length === 1) {

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+

0 commit comments

Comments
 (0)