diff --git a/client/README.md b/client/README.md index dec41a9e..aa8c0d22 100644 --- a/client/README.md +++ b/client/README.md @@ -35,3 +35,8 @@ The go to definition feature currently behaves as follows: | class or inc-file | file | | recipe | recipe definition and all bbappends | | symbol | all symbols within the include hierarchy | + +### Show definitions of BitBake's defined variables on hover +*This functionnality requires to [provide the BitBake's folder](#set-bitbakes-path)* + +Place your cursor over a variable. If it is a BitBake defined variable, then its definition from the documentation will be displayed. \ No newline at end of file diff --git a/server/src/BitBakeDocScanner.ts b/server/src/BitBakeDocScanner.ts new file mode 100644 index 00000000..9b2995e0 --- /dev/null +++ b/server/src/BitBakeDocScanner.ts @@ -0,0 +1,79 @@ +import path from 'path' +import fs from 'fs' + +type SuffixType = 'layer' | 'providedItem' | undefined + +export interface VariableInfos { + name: string + definition: string + validFiles?: RegExp[] // Files on which the variable is defined. If undefined, the variable is defined in all files. + suffixType?: SuffixType +} + +type VariableInfosOverride = Partial + +// Infos that can't be parsed properly from the doc +const variableInfosOverrides: Record = { + BBFILE_PATTERN: { + suffixType: 'layer' + }, + LAYERDEPENDS: { + suffixType: 'layer' + }, + LAYERDIR: { + validFiles: [/^.*\/conf\/layer.conf$/] + }, + LAYERDIR_RE: { + validFiles: [/^.*\/conf\/layer.conf$/] + }, + LAYERVERSION: { + suffixType: 'layer' + }, + PREFERRED_PROVIDER: { + suffixType: 'providedItem' + } +} + +const variablesFolder = 'doc/bitbake-user-manual/bitbake-user-manual-ref-variables.rst' +const variablesRegexForDoc = /^ {3}:term:`(?[A-Z_]*?)`\n(?.*?)(?=^ {3}:term:|$(?!\n))/gsm + +export class BitBakeDocScanner { + private _variablesInfos: Record = {} + private _variablesRegex = /(?!)/g // Initialize with dummy regex that won't match anything so we don't have to check for undefined + + get variablesInfos (): Record { + return this._variablesInfos + } + + get variablesRegex (): RegExp { + return this._variablesRegex + } + + parse (pathToBitbakeFolder: string): void { + const file = fs.readFileSync(path.join(pathToBitbakeFolder, variablesFolder), 'utf8') + for (const match of file.matchAll(variablesRegexForDoc)) { + const name = match.groups?.name + // Naive silly inneficient incomplete conversion to markdown + const definition = match.groups?.definition + .replace(/^ {3}/gm, '') + .replace(/:term:|:ref:/g, '') + .replace(/\.\. (note|important)::/g, (_match, p1) => { return `**${p1}**` }) + .replace(/::/g, ':') + .replace(/``/g, '`') + if (name === undefined || definition === undefined) { + return + } + this._variablesInfos[name] = { + name, + definition, + ...variableInfosOverrides[name] + } + } + const variablesNames = Object.keys(this._variablesInfos) + // Sort from longuest to shortest in order to make the regex greedy + // Otherwise it would match B before BB_PRESERVE_ENV + variablesNames.sort((a, b) => b.length - a.length) + const variablesRegExpString = `(${variablesNames.join('|')})` + this._variablesRegex = new RegExp(variablesRegExpString, 'gi') + } +} diff --git a/server/src/server.ts b/server/src/server.ts index d915b8b3..5493b9bf 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -12,8 +12,10 @@ import { type CompletionItem, type Definition, ProposedFeatures, - TextDocumentSyncKind + TextDocumentSyncKind, + type Hover } from 'vscode-languageserver/node' +import { BitBakeDocScanner } from './BitBakeDocScanner' import { BitBakeProjectScanner } from './BitBakeProjectScanner' import { ContextHandler } from './ContextHandler' import { SymbolScanner } from './SymbolScanner' @@ -27,6 +29,7 @@ const documents = new TextDocuments(TextDocument) // Until we manage to fix this, we use this documentMap to store the content of the files // Does it have any other purpose? const documentMap = new Map< string, string[] >() +const bitBakeDocScanner = new BitBakeDocScanner() const bitBakeProjectScanner: BitBakeProjectScanner = new BitBakeProjectScanner(connection) const contextHandler: ContextHandler = new ContextHandler(bitBakeProjectScanner) @@ -53,7 +56,8 @@ connection.onInitialize((params): InitializeResult => { commands: [ 'bitbake.rescan-project' ] - } + }, + hoverProvider: true } } }) @@ -78,6 +82,7 @@ interface BitbakeSettings { pathToBashScriptInterpreter: string machine: string generateWorkingFolder: boolean + pathToBitbakeFolder: string } function setSymbolScanner (newSymbolScanner: SymbolScanner | null): void { @@ -93,6 +98,8 @@ connection.onDidChangeConfiguration((change) => { bitBakeProjectScanner.generateWorkingPath = settings.bitbake.generateWorkingFolder bitBakeProjectScanner.scriptInterpreter = settings.bitbake.pathToBashScriptInterpreter bitBakeProjectScanner.machineName = settings.bitbake.machine + const bitBakeFolder = settings.bitbake.pathToBitbakeFolder + bitBakeDocScanner.parse(bitBakeFolder) }) connection.onDidChangeWatchedFiles((change) => { @@ -163,5 +170,42 @@ connection.onDefinition((textDocumentPositionParams: TextDocumentPositionParams) return contextHandler.getDefinition(textDocumentPositionParams, documentAsText) }) +connection.onHover(async (params): Promise => { + const { position, textDocument } = params + const documentAsText = documentMap.get(textDocument.uri) + const textLine = documentAsText?.[position.line] + if (textLine === undefined) { + return undefined + } + const matches = textLine.matchAll(bitBakeDocScanner.variablesRegex) + for (const match of matches) { + const name = match[1].toUpperCase() + if (name === undefined || match.index === undefined) { + continue + } + const start = match.index + const end = start + name.length + if ((start > position.character) || (end <= position.character)) { + continue + } + + const definition = bitBakeDocScanner.variablesInfos[name]?.definition + const hover: Hover = { + contents: { + kind: 'markdown', + value: `**${name}**\n___\n${definition}` + }, + range: { + start: position, + end: { + ...position, + character: end + } + } + } + return hover + } +}) + // Listen on the connection connection.listen()