Skip to content

Commit

Permalink
Merge pull request #5 from savoirfairelinux/Add-hover
Browse files Browse the repository at this point in the history
Feat: Add definition on hover for variables defined by BitBake
  • Loading branch information
idillon-sfl authored Sep 26, 2023
2 parents 1892112 + 5b8941f commit 056083e
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 4 deletions.
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
// 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()

0 comments on commit 056083e

Please sign in to comment.