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 definition on hover for variables defined by BitBake #5

Merged
merged 3 commits into from
Sep 26, 2023
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
11 changes: 11 additions & 0 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.

Expand All @@ -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.
7 changes: 6 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
],
"configuration": {
"type": "object",
"title": "Language Server for Bitbake configuration",
"title": "BitBake",
"properties": {
"bitbake.loggingLevel": {
"type": "string",
Expand Down Expand Up @@ -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."
}
}
},
Expand Down
79 changes: 79 additions & 0 deletions server/src/BitBakeDocScanner.ts
Original file line number Diff line number Diff line change
@@ -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<VariableInfos>

// Infos that can't be parsed properly from the doc
const variableInfosOverrides: Record<string, VariableInfosOverride> = {
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:`(?<name>[A-Z_]*?)`\n(?<definition>.*?)(?=^ {3}:term:|$(?!\n))/gsm

export class BitBakeDocScanner {
private _variablesInfos: Record<string, VariableInfos> = {}
private _variablesRegex = /(?!)/g // Initialize with dummy regex that won't match anything so we don't have to check for undefined

get variablesInfos (): Record<string, VariableInfos> {
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: longuest -> longest

// 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')
}
}
56 changes: 53 additions & 3 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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>(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)

Expand All @@ -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
},
Expand All @@ -48,7 +56,8 @@ connection.onInitialize((params): InitializeResult => {
commands: [
'bitbake.rescan-project'
]
}
},
hoverProvider: true
}
}
})
Expand All @@ -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)}`)
})

Expand All @@ -72,6 +82,7 @@ interface BitbakeSettings {
pathToBashScriptInterpreter: string
machine: string
generateWorkingFolder: boolean
pathToBitbakeFolder: string
}

function setSymbolScanner (newSymbolScanner: SymbolScanner | null): void {
Expand All @@ -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) => {
Expand Down Expand Up @@ -157,5 +170,42 @@ connection.onDefinition((textDocumentPositionParams: TextDocumentPositionParams)
return contextHandler.getDefinition(textDocumentPositionParams, documentAsText)
})

connection.onHover(async (params): Promise<Hover | undefined> => {
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()