Skip to content

Commit

Permalink
Feat: Add hover for Yocto defined variables in embedded Python
Browse files Browse the repository at this point in the history
  • Loading branch information
idillon-sfl committed Dec 20, 2023
1 parent 6e91d26 commit edf730d
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 2 deletions.
27 changes: 27 additions & 0 deletions server/src/BitBakeDocScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class BitBakeDocScanner {
private _yoctoVariableInfo: VariableInfo[] = []
private _variableFlagInfo: VariableFlagInfo[] = []
private _yoctoTaskInfo: DocInfo[] = []
private _functionForAccessingDatastore: string[] = []
private _docPath: string = path.join(__dirname, '../../client/resources/docs') // This default path is for the test. The path after the compilation can be different
private readonly _keywordInfo: DocInfo[] = KEYWORDS

Expand All @@ -97,11 +98,16 @@ export class BitBakeDocScanner {
return this._keywordInfo
}

get functionForAccessingDatastore (): string[] {
return this._functionForAccessingDatastore
}

public clearScannedDocs (): void {
this._bitbakeVariableInfo = []
this._yoctoVariableInfo = []
this._variableFlagInfo = []
this._yoctoTaskInfo = []
this._functionForAccessingDatastore = []
}

public setDocPathAndParse (extensionPath: string): void {
Expand All @@ -110,6 +116,7 @@ export class BitBakeDocScanner {
this.parseBitbakeVariablesFile()
this.parseYoctoVariablesFile()
this.parseYoctoTaskFile()
this.parseFunctionsForAccessingDatastore()
}

// TODO: Generalize these parse functions. They all read a file, match some content and store it.
Expand Down Expand Up @@ -267,6 +274,26 @@ export class BitBakeDocScanner {
}
this._variableFlagInfo = variableFlagInfo
}

public parseFunctionsForAccessingDatastore (): void {
const filePath = path.join(this._docPath, 'bitbake-user-manual-metadata.rst')
const pattern = /^ {3}\* - ``d\.(?<name>.*)\("X"(.*)\)``/gm
let file = ''
try {
file = fs.readFileSync(filePath, 'utf8')
} catch {
logger.warn(`Failed to read file at ${filePath}`)
}
const functionsForAccessingDatastore: string[] = []
for (const match of file.matchAll(pattern)) {
const name = match.groups?.name
if (name === undefined) {
return
}
functionsForAccessingDatastore.push(name)
}
this._functionForAccessingDatastore = functionsForAccessingDatastore
}
}

export const bitBakeDocScanner = new BitBakeDocScanner()
18 changes: 17 additions & 1 deletion server/src/__tests__/fixtures/hover.bb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,20 @@ my_do_build(){

inherit dummy
include dummy.inc
require dummy.inc
require dummy.inc

python (){
d.getVar("DESCRIPTION")
d.setVar('DESCRIPTION', 'value')
b.getVar('DESCRIPTION')
d.test('DESCRIPTION')
d.getVar("FOO")
e.data.getVar('DESCRIPTION')
}

def test ():
d.setVar('DESCRIPTION')

VAR = "${@d.getVar("DESCRIPTION")}"

d.getVar("DESCRIPTION")
139 changes: 139 additions & 0 deletions server/src/__tests__/hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,4 +315,143 @@ describe('on hover', () => {
})
)
})

it('shows definition on hovering variable in Python functions for accessing datastore', async () => {
bitBakeDocScanner.parseBitbakeVariablesFile()
bitBakeDocScanner.parseFunctionsForAccessingDatastore()
await analyzer.analyze({
uri: DUMMY_URI,
document: FIXTURE_DOCUMENT.HOVER
})

const shouldShow1 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 35,
character: 14
}
})

const shouldShow2 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 36,
character: 14
}
})

const shouldShow3 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 40,
character: 19
}
})

const shouldShow4 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 46,
character: 20
}
})

const shouldShow5 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 44,
character: 14
}
})

const shouldNotShow1 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 37,
character: 14
}
})

const shouldNotShow2 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 38,
character: 12
}
})

const shouldNotShow3 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 39,
character: 19
}
})

const shouldNotShow4 = await onHoverHandler({
textDocument: {
uri: DUMMY_URI
},
position: {
line: 48,
character: 10
}
})

expect(shouldShow1).toEqual({
contents: {
kind: 'markdown',
value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n'
}
})

expect(shouldShow2).toEqual({
contents: {
kind: 'markdown',
value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n'
}
})

expect(shouldShow3).toEqual({
contents: {
kind: 'markdown',
value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n'
}
})

expect(shouldShow4).toEqual({
contents: {
kind: 'markdown',
value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n'
}
})

expect(shouldShow5).toEqual({
contents: {
kind: 'markdown',
value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n'
}
})

expect(shouldNotShow1).toBe(null)
expect(shouldNotShow2).toBe(null)
expect(shouldNotShow3).toBe(null)
expect(shouldNotShow4).toBe(null)
})
})
2 changes: 1 addition & 1 deletion server/src/connectionHandlers/onHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function onHoverHandler (params: HoverParams): Promise<Hover | null
}
// Show documentation of a bitbake variable
// Triggers on global declaration expressions like "VAR = 'foo'" and inside variable expansion like "FOO = ${VAR}" but skip the ones like "python VAR(){}"
const canShowHoverDefinitionForVariableName: boolean = (analyzer.getGlobalDeclarationSymbols(textDocument.uri).some((symbol) => symbol.name === word) && analyzer.isIdentifierOfVariableAssignment(params)) || analyzer.isVariableExpansion(textDocument.uri, position.line, position.character)
const canShowHoverDefinitionForVariableName: boolean = (analyzer.getGlobalDeclarationSymbols(textDocument.uri).some((symbol) => symbol.name === word) && analyzer.isIdentifierOfVariableAssignment(params)) || analyzer.isVariableExpansion(textDocument.uri, position.line, position.character) || analyzer.isVariableFromPythonDatastore(textDocument.uri, position.line, position.character)
if (canShowHoverDefinitionForVariableName) {
const found = [
...bitBakeDocScanner.bitbakeVariableInfo.filter((bitbakeVariable) => !bitBakeDocScanner.yoctoVariableInfo.some(yoctoVariable => yoctoVariable.name === bitbakeVariable.name)),
Expand Down
45 changes: 45 additions & 0 deletions server/src/tree-sitter/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { logger } from '../lib/src/utils/OutputLogger'
import fs from 'fs'
import path from 'path'
import { bitBakeProjectScannerClient } from '../BitbakeProjectScannerClient'
import { bitBakeDocScanner } from '../BitBakeDocScanner'
const DEBOUNCE_TIME_MS = 500

interface AnalyzedDocument {
Expand Down Expand Up @@ -274,6 +275,50 @@ export default class Analyzer {
(n?.type === ':' && n?.parent?.type === 'ERROR' && n?.parent?.previousSibling?.type === 'override') // when having MYVAR:append: = '123' and the second or later colon is typed
}

public isInsidePythonRegion (
uri: string,
line: number,
column: number
): boolean {
let n = this.nodeAtPoint(uri, line, column)

while (n?.parent !== null && n?.parent !== undefined) {
if (TreeSitterUtils.isInlinePython(n.parent) || TreeSitterUtils.isPythonDefinition(n.parent)) {
return true
}
n = n.parent
}

return false
}

public isVariableFromPythonDatastore (
uri: string,
line: number,
column: number
): boolean {
const n = this.nodeAtPoint(uri, line, column)
if (!this.isInsidePythonRegion(uri, line, column)) {
return false
}
// Example:
// n.text: FOO
// n.parent.text: 'FOO'
// n.parent.parent.text: ('FOO')
// n.parent.parent.parent.text: d.getVar('FOO')
const parentParentParent = n?.parent?.parent?.parent
if (parentParentParent?.type !== 'call') {
return false
}
const match = parentParentParent.text.match(/^(d|e\.data)\.(?<name>.*)\((?<params>.*)\)$/) // d.name(params), e.data.name(params)
const functionName = match?.groups?.name
if (functionName === undefined || !(bitBakeDocScanner.functionForAccessingDatastore.includes(functionName))) {
return false
}
const variable = match?.groups?.params?.split(',')[0]?.trim().replace(/('|")/g, '')
return variable === n?.text
}

/**
* Check if the variable expansion syntax is being typed. Only for expressions that reference variables. \
* Example:
Expand Down

0 comments on commit edf730d

Please sign in to comment.