Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add hover for Yocto defined variables in embedded Python #36

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 _pythonDatastoreFunction: 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 pythonDatastoreFunction (): string[] {
return this._pythonDatastoreFunction
}

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

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

// 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 parsePythonDatastoreFunction (): 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 pythonDatastoreFunction: string[] = []
for (const match of file.matchAll(pattern)) {
const name = match.groups?.name
if (name === undefined) {
return
}
pythonDatastoreFunction.push(name)
}
this._pythonDatastoreFunction = pythonDatastoreFunction
}
}

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.parsePythonDatastoreFunction()
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.isPythonDatastoreVariable(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 isPythonDatastoreVariable (
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.pythonDatastoreFunction.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
Loading