diff --git a/client/README.md b/client/README.md index 1c81d8d3..aa8c0d22 100644 --- a/client/README.md +++ b/client/README.md @@ -1,5 +1,10 @@ # BitBake recipe language support in Visual Studio Code +## Set BitBake's path +Some features require to know where your BitBake's folder is located. The extension will by default assume it is located at the root of the project in a folder named `bitbake`. If your BitBake folder is located somewhere else, set its path in the settings in order to have full features. + +To access BitBake's settings: Files -> Preferences -> Settings [Ctrl+,]. The BitBake's settings are under Extensions. + ## Features ### Syntax highlighting @@ -19,6 +24,7 @@ The following suggestions are currently supported: * Context-based suggestions for all symbols within the include hierarchy ### Go to definition +*This functionnality requires to [provide the BitBake's folder](#set-bitbakes-path)* *CTRL and click* may be used to open the file associated with a class, inc-file, recipe or variable. If more than one definition exists, a list of definitions is provided. @@ -29,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/client/package.json b/client/package.json index 9492b9df..bbe2bcbb 100644 --- a/client/package.json +++ b/client/package.json @@ -49,7 +49,7 @@ ], "configuration": { "type": "object", - "title": "Language Server for Bitbake configuration", + "title": "BitBake", "properties": { "bitbake.loggingLevel": { "type": "string", @@ -85,6 +85,11 @@ "type": "string", "default": "", "description": "This setting is used to forward the machine name to bitbake." + }, + "bitbake.pathToBitbakeFolder": { + "type": "string", + "default": "./bitbake", + "description": "This setting is used to specify the path to the bitbake folder." } } }, 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 6581b8dc..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' @@ -23,7 +25,11 @@ import logger from 'winston' // Create a connection for the server. The connection uses Node's IPC as a transport const connection: Connection = createConnection(ProposedFeatures.all) const documents = new TextDocuments(TextDocument) +// It seems our 'documents' variable is failing to handle files properly (documents.all() gives an empty list) +// 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) @@ -39,7 +45,9 @@ connection.onInitialize((params): InitializeResult => { return { capabilities: { - textDocumentSync: TextDocumentSyncKind.Incremental, + // TODO: replace for TextDocumentSyncKind.Incremental (should be more efficient) + // Issue is our 'documents' variable is failing to track the files + textDocumentSync: TextDocumentSyncKind.Full, completionProvider: { resolveProvider: true }, @@ -48,7 +56,8 @@ connection.onInitialize((params): InitializeResult => { commands: [ 'bitbake.rescan-project' ] - } + }, + hoverProvider: true } } }) @@ -57,6 +66,7 @@ connection.onInitialize((params): InitializeResult => { // when the text document first opened or when its content has changed. documents.onDidChangeContent((change) => { // TODO: add symbol parsing here + // TODO: This should be called when a file is modified. Understand why it is not. logger.debug(`onDidChangeContent: ${JSON.stringify(change)}`) }) @@ -72,6 +82,7 @@ interface BitbakeSettings { pathToBashScriptInterpreter: string machine: string generateWorkingFolder: boolean + pathToBitbakeFolder: string } function setSymbolScanner (newSymbolScanner: SymbolScanner | null): void { @@ -87,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) => { @@ -157,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()