diff --git a/server/src/__tests__/analyzer.test.ts b/server/src/__tests__/analyzer.test.ts index 5419dbc3..2c023b17 100644 --- a/server/src/__tests__/analyzer.test.ts +++ b/server/src/__tests__/analyzer.test.ts @@ -48,6 +48,44 @@ describe('analyze', () => { const globalDeclarations = analyzer.getGlobalDeclarationSymbols(DUMMY_URI) + expect(globalDeclarations).toEqual( + expect.arrayContaining([ + { + kind: 13, + location: { + range: { + end: { + character: 11, + line: 1 + }, + start: { + character: 0, + line: 1 + } + }, + uri: DUMMY_URI + }, + name: 'BAR' + }, + { + kind: 13, + location: { + range: { + end: { + character: 11, + line: 0 + }, + start: { + character: 0, + line: 0 + } + }, + uri: DUMMY_URI + }, + name: 'FOO' + } + ]) + ) expect(globalDeclarations).toMatchInlineSnapshot(` [ { @@ -241,22 +279,24 @@ describe('sourceIncludeFiles', () => { expect(symbols).toEqual( expect.arrayContaining([ expect.objectContaining({ - DESCRIPTION: expect.objectContaining({ - name: 'DESCRIPTION', - location: { - uri: FIXTURE_URI.BAR_INC, - range: { - start: { - line: 0, - character: 0 - }, - end: { - line: 0, - character: 23 + DESCRIPTION: expect.arrayContaining([ + expect.objectContaining({ + name: 'DESCRIPTION', + location: { + uri: FIXTURE_URI.BAR_INC, + range: { + start: { + line: 0, + character: 0 + }, + end: { + line: 0, + character: 23 + } } } - } - }) + }) + ]) }) ]) ) @@ -264,22 +304,24 @@ describe('sourceIncludeFiles', () => { expect(symbols).toEqual( expect.arrayContaining([ expect.objectContaining({ - DESCRIPTION: expect.objectContaining({ - name: 'DESCRIPTION', - location: { - uri: FIXTURE_URI.FOO_INC, - range: { - start: { - line: 0, - character: 0 - }, - end: { - line: 0, - character: 23 + DESCRIPTION: expect.arrayContaining([ + expect.objectContaining({ + name: 'DESCRIPTION', + location: { + uri: FIXTURE_URI.FOO_INC, + range: { + start: { + line: 0, + character: 0 + }, + end: { + line: 0, + character: 23 + } } } - } - }) + }) + ]) }) ]) ) @@ -287,24 +329,54 @@ describe('sourceIncludeFiles', () => { expect(symbols).toEqual( expect.arrayContaining([ expect.objectContaining({ - DESCRIPTION: expect.objectContaining({ - name: 'DESCRIPTION', - location: { - uri: FIXTURE_URI.BAZ_BBCLASS, - range: { - start: { - line: 0, - character: 0 - }, - end: { - line: 0, - character: 27 + DESCRIPTION: expect.arrayContaining([ + expect.objectContaining({ + name: 'DESCRIPTION', + location: { + uri: FIXTURE_URI.BAZ_BBCLASS, + range: { + start: { + line: 0, + character: 0 + }, + end: { + line: 0, + character: 27 + } } } - } - }) + }) + ]) }) ]) ) }) }) + +describe('declarations', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('gets all symbols in the declaration statements with duplicates', async () => { + const analyzer = await getAnalyzer() + const document = FIXTURE_DOCUMENT.COMPLETION + const uri = FIXTURE_URI.COMPLETION + + await analyzer.analyze({ + document, + uri + }) + + const symbols = analyzer.getGlobalDeclarationSymbols(uri) + + let occurances = 0 + symbols.forEach((symbol) => { + if (symbol.name === 'MYVAR') { + occurances++ + } + }) + + expect(occurances).toEqual(5) + }) +}) diff --git a/server/src/__tests__/completions.test.ts b/server/src/__tests__/completions.test.ts index 4c3657e8..0854dad5 100644 --- a/server/src/__tests__/completions.test.ts +++ b/server/src/__tests__/completions.test.ts @@ -196,6 +196,32 @@ describe('On Completion', () => { expect(result).toEqual([]) }) + it("doesn't provide duplicate completion items for local custom variables", async () => { + await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.COMPLETION + }) + + const result = onCompletionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 0, + character: 0 + } + }) + + let occurances = 0 + result.forEach((item) => { + if (item.label === 'MYVAR') { + occurances++ + } + }) + + expect(occurances).toBe(1) + }) + it('provides necessary suggestions when it is in variable expansion', async () => { await analyzer.analyze({ uri: DUMMY_URI, @@ -636,13 +662,19 @@ describe('On Completion', () => { character: 0 } }) + // Show only one completion item for each symbol + let occurances = 0 + result.forEach(item => { + item.label === 'DESCRIPTION' && occurances++ + }) + expect(occurances).toEqual(1) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ label: 'DESCRIPTION', labelDetails: { - description: path.relative(FIXTURE_URI.DIRECTIVE.replace('file://', ''), FIXTURE_URI.BAR_INC.replace('file://', '')) + description: path.relative(FIXTURE_URI.DIRECTIVE.replace('file://', ''), FIXTURE_URI.FOO_INC.replace('file://', '')) // In this test case, the one that remains after the filtering is the relative path from directive.bb to foo.inc } }) ]) @@ -659,27 +691,6 @@ describe('On Completion', () => { ]) ) - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - label: 'DESCRIPTION', - labelDetails: { - description: path.relative(FIXTURE_URI.DIRECTIVE.replace('file://', ''), FIXTURE_URI.FOO_INC.replace('file://', '')) - } - }) - ]) - ) - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - label: 'DESCRIPTION', - labelDetails: { - description: path.relative(FIXTURE_URI.DIRECTIVE.replace('file://', ''), FIXTURE_URI.BAZ_BBCLASS.replace('file://', '')) - } - }) - ]) - ) bitBakeDocScanner.parseBitbakeVariablesFile() bitBakeDocScanner.parseYoctoVariablesFile() @@ -698,7 +709,7 @@ describe('On Completion', () => { expect.objectContaining({ label: 'DESCRIPTION', labelDetails: { - description: path.relative(FIXTURE_URI.DIRECTIVE.replace('file://', ''), FIXTURE_URI.BAZ_BBCLASS.replace('file://', '')) + description: path.relative(FIXTURE_URI.DIRECTIVE.replace('file://', ''), FIXTURE_URI.FOO_INC.replace('file://', '')) }, documentation: { value: '```man\nDESCRIPTION (bitbake-language-server)\n\n\n```\n```bitbake\n\n```\n---\n The package description used by package managers. If not set,\n `DESCRIPTION` takes the value of the `SUMMARY`\n variable.\n\n\n[Reference](https://docs.yoctoproject.org/ref-manual/variables.html#term-DESCRIPTION)', diff --git a/server/src/__tests__/definition.test.ts b/server/src/__tests__/definition.test.ts index 49e63efb..8e5cbcc8 100644 --- a/server/src/__tests__/definition.test.ts +++ b/server/src/__tests__/definition.test.ts @@ -7,20 +7,8 @@ import { analyzer } from '../tree-sitter/analyzer' import { generateParser } from '../tree-sitter/parser' import { onDefinitionHandler } from '../connectionHandlers/onDefinition' import { FIXTURE_DOCUMENT, DUMMY_URI, FIXTURE_URI } from './fixtures/fixtures' -import { type Location } from 'vscode-languageserver' -import { definitionProvider } from '../DefinitionProvider' import path from 'path' import { bitBakeProjectScannerClient } from '../BitbakeProjectScannerClient' -// TODO: Current implementation of the definitionProvider needs to be improved, this test suite should be modified accordingly after -const mockDefinition = (path: string | undefined): void => { - if (path !== undefined) { - const location: Location = { uri: 'file://' + path, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } } - - jest.spyOn(definitionProvider, 'createDefinitionForKeyword').mockReturnValue(location) - } else { - jest.spyOn(definitionProvider, 'createDefinitionForKeyword').mockReturnValue([]) - } -} describe('on definition', () => { beforeAll(async () => { @@ -39,56 +27,74 @@ describe('on definition', () => { jest.resetAllMocks() }) - it('provides definition to directive statement', async () => { - await analyzer.analyze({ - uri: DUMMY_URI, - document: FIXTURE_DOCUMENT.DEFINITION - }) + it('provides go-to-definition to directive statement', async () => { + const parsedBarPath = path.parse(FIXTURE_DOCUMENT.BAR_INC.uri.replace('file://', '')) + const parsedFooPath = path.parse(FIXTURE_DOCUMENT.FOO_INC.uri.replace('file://', '')) + const parsedBazPath = path.parse(FIXTURE_DOCUMENT.BAZ_BBCLASS.uri.replace('file://', '')) - let position = { - line: 0, - character: 9 + bitBakeProjectScannerClient.bitbakeScanResult = { + _classes: [ + { + name: parsedBazPath.name, + path: parsedBazPath, + extraInfo: 'layer: core' + } + ], + _includes: [ + { + name: parsedBarPath.name, + path: parsedBarPath, + extraInfo: 'layer: core' + }, + { + name: parsedFooPath.name, + path: parsedFooPath, + extraInfo: 'layer: core' + } + ], + _layers: [], + _overrides: [], + _recipes: [] } - mockDefinition(analyzer.getDocumentTexts(DUMMY_URI)?.[position.line].split(' ')[1]) + await analyzer.analyze({ + uri: FIXTURE_URI.DIRECTIVE, + document: FIXTURE_DOCUMENT.DIRECTIVE + }) - const definition1 = onDefinitionHandler({ + const definition = onDefinitionHandler({ textDocument: { - uri: DUMMY_URI + uri: FIXTURE_URI.DIRECTIVE }, - position + position: { + line: 2, + character: 9 + } }) - expect(definition1).toEqual( - { - uri: 'file://dummy', - range: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 } + expect(definition).toEqual( + expect.arrayContaining([ + { + uri: FIXTURE_URI.BAZ_BBCLASS, + range: { + start: { + line: 0, + character: 0 + }, + end: { + line: 0, + character: 0 + } + } } - } + ]) ) - - position = { - line: 0, - character: 0 - } - - mockDefinition(analyzer.getDocumentTexts(DUMMY_URI)?.[position.line].split(' ')[1]) - - const definition2 = onDefinitionHandler({ - textDocument: { - uri: DUMMY_URI - }, - position - }) - - expect(definition2).toEqual([]) }) - it('provides go to definition for variables if the included files also contain the variable', async () => { + it('provides go to definition for variables found in current file and included files', async () => { const parsedBazPath = path.parse(FIXTURE_DOCUMENT.BAZ_BBCLASS.uri.replace('file://', '')) const parsedFooPath = path.parse(FIXTURE_DOCUMENT.FOO_INC.uri.replace('file://', '')) + const parsedBarPath = path.parse(FIXTURE_DOCUMENT.BAR_INC.uri.replace('file://', '')) bitBakeProjectScannerClient.bitbakeScanResult = { _layers: [], @@ -104,53 +110,51 @@ describe('on definition', () => { name: parsedFooPath.name, path: parsedFooPath, extraInfo: 'layer: core' + }, + { + name: parsedBarPath.name, + path: parsedBarPath, + extraInfo: 'layer: core' } ] } await analyzer.analyze({ uri: DUMMY_URI, - document: FIXTURE_DOCUMENT.DEFINITION + document: FIXTURE_DOCUMENT.DIRECTIVE }) - const result = onDefinitionHandler({ + const result1 = onDefinitionHandler({ textDocument: { uri: DUMMY_URI }, position: { - line: 10, + line: 4, character: 1 } }) + // Go to definition for symbols in variable expansion + const result2 = onDefinitionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 6, + character: 11 + } + }) + + if (result1 === null) { + fail('result1 is null') + } - expect(result).toEqual( + expect(result1).toEqual(result2) + expect(result1).toEqual( expect.arrayContaining([ - { - uri: FIXTURE_URI.BAZ_BBCLASS, - range: { - start: { - line: 0, - character: 0 - }, - end: { - line: 0, - character: 27 - } - } - }, - { - uri: FIXTURE_URI.FOO_INC, - range: { - start: { - line: 0, - character: 0 - }, - end: { - line: 0, - character: 23 - } - } - } + { uri: FIXTURE_URI.FOO_INC, range: { start: { line: 1, character: 0 }, end: { line: 1, character: 21 } } }, + { uri: FIXTURE_URI.FOO_INC, range: { start: { line: 2, character: 0 }, end: { line: 2, character: 21 } } }, { uri: FIXTURE_URI.BAR_INC, range: { start: { line: 2, character: 0 }, end: { line: 2, character: 21 } } }, + { uri: DUMMY_URI, range: { start: { line: 4, character: 0 }, end: { line: 4, character: 21 } } }, + { uri: DUMMY_URI, range: { start: { line: 5, character: 0 }, end: { line: 5, character: 28 } } } ]) ) }) diff --git a/server/src/__tests__/fixtures/completion.bb b/server/src/__tests__/fixtures/completion.bb index eda09e4d..5eb38a55 100644 --- a/server/src/__tests__/fixtures/completion.bb +++ b/server/src/__tests__/fixtures/completion.bb @@ -1,7 +1,7 @@ FOO = '123' MYVAR = 'F${F}' MYVAR:append = '123' -MYVAR[doc] 'this is docs' +MYVAR[doc] = 'this is docs' python (){ myvar = [1,2,3] myvar[0] = 4 diff --git a/server/src/__tests__/fixtures/definition.bb b/server/src/__tests__/fixtures/definition.bb deleted file mode 100644 index 5f015c90..00000000 --- a/server/src/__tests__/fixtures/definition.bb +++ /dev/null @@ -1,11 +0,0 @@ -inherit dummy - -require dummy.inc - -include dummy.inc - -inherit baz # This is a real file in fixture folder - -include inc/foo.inc - -DESCRIPTION = 'Go to definition for this variable should point to its included files' \ No newline at end of file diff --git a/server/src/__tests__/fixtures/directive.bb b/server/src/__tests__/fixtures/directive.bb index 7e61752f..6ba9073c 100644 --- a/server/src/__tests__/fixtures/directive.bb +++ b/server/src/__tests__/fixtures/directive.bb @@ -1,3 +1,7 @@ require inc/foo.inc include inc/bar.inc -inherit baz \ No newline at end of file +inherit baz + +APPEND = 'append bar' +APPEND:append = 'append bar' +FOO = '${APPEND} \ No newline at end of file diff --git a/server/src/__tests__/fixtures/fixtures.ts b/server/src/__tests__/fixtures/fixtures.ts index 32b0fea6..f85f4424 100644 --- a/server/src/__tests__/fixtures/fixtures.ts +++ b/server/src/__tests__/fixtures/fixtures.ts @@ -30,7 +30,6 @@ export const FIXTURE_URI = { DECLARATION: `file://${path.join(FIXTURE_FOLDER, 'declarations.bb')}`, COMPLETION: `file://${path.join(FIXTURE_FOLDER, 'completion.bb')}`, HOVER: `file://${path.join(FIXTURE_FOLDER, 'hover.bb')}`, - DEFINITION: `file://${path.join(FIXTURE_FOLDER, 'definition.bb')}`, EMBEDDED: `file://${path.join(FIXTURE_FOLDER, 'embedded.bb')}`, SEMANTIC_TOKENS: `file://${path.join(FIXTURE_FOLDER, 'semanticTokens.bb')}`, DIRECTIVE: `file://${path.join(FIXTURE_FOLDER, 'directive.bb')}`, diff --git a/server/src/__tests__/fixtures/inc/bar.inc b/server/src/__tests__/fixtures/inc/bar.inc index 5cf1753a..20a48f06 100644 --- a/server/src/__tests__/fixtures/inc/bar.inc +++ b/server/src/__tests__/fixtures/inc/bar.inc @@ -1,2 +1,3 @@ DESCRIPTION = 'bar.inc' -export PYTHON = 'python3' \ No newline at end of file +export PYTHON = 'python3' +APPEND = 'append bar' diff --git a/server/src/__tests__/fixtures/inc/foo.inc b/server/src/__tests__/fixtures/inc/foo.inc index ef685cce..379c7d6d 100644 --- a/server/src/__tests__/fixtures/inc/foo.inc +++ b/server/src/__tests__/fixtures/inc/foo.inc @@ -1 +1,3 @@ -DESCRIPTION = 'foo.inc' \ No newline at end of file +DESCRIPTION = 'foo.inc' +APPEND = 'append foo' +APPEND:remove = 'foo' \ No newline at end of file diff --git a/server/src/connectionHandlers/onCompletion.ts b/server/src/connectionHandlers/onCompletion.ts index e7fd4b83..f5a7f5f9 100644 --- a/server/src/connectionHandlers/onCompletion.ts +++ b/server/src/connectionHandlers/onCompletion.ts @@ -111,7 +111,14 @@ export function onCompletionHandler (textDocumentPositionParams: TextDocumentPos let symbolCompletionItems: CompletionItem[] = [] if (word !== null) { - const globalDeclarationSymbols = analyzer.getGlobalDeclarationSymbols(documentUri) + const uniqueSymbolSet = new Set() + const globalDeclarationSymbols = analyzer.getGlobalDeclarationSymbols(documentUri).filter(symbol => { + if (!uniqueSymbolSet.has(symbol.name)) { + uniqueSymbolSet.add(symbol.name) + return true + } + return false + }) // Filter out duplicate BITBAKE_VARIABLES as they will be included as global declaration after running analyzer.analyze() in documents.onDidChangeContent() in server.ts symbolCompletionItems = [ ...globalDeclarationSymbols.filter((symbol: SymbolInformation) => !(new Set(BITBAKE_VARIABLES).has(symbol.name))).map((symbol: SymbolInformation) => ( @@ -274,21 +281,21 @@ function getFilePath (elementInfo: ElementInfo, fileType: string): string | unde function convertExtraSymbolsToCompletionItems (uri: string): CompletionItem[] { logger.debug(`[onCompletion] convertSymbolsToCompletionItems: ${uri}`) - const completionItems: CompletionItem[] = [] + let completionItems: CompletionItem[] = [] analyzer.getExtraSymbolsForUri(uri).forEach((extraSymbols) => { Object.keys(extraSymbols).forEach((key) => { const variableInfo = [ ...bitBakeDocScanner.bitbakeVariableInfo.filter((bitbakeVariable) => !bitBakeDocScanner.yoctoVariableInfo.some(yoctoVariable => yoctoVariable.name === bitbakeVariable.name)), ...bitBakeDocScanner.yoctoVariableInfo ] - const foundInVariableInfo = variableInfo.find((variable) => variable.name === extraSymbols[key].name) + const foundInVariableInfo = variableInfo.find((variable) => variable.name === key) const completionItem: CompletionItem = { - label: extraSymbols[key].name, + label: key, labelDetails: { - description: path.relative(documentUri.replace('file://', ''), extraSymbols[key].location.uri.replace('file://', '')) + description: path.relative(documentUri.replace('file://', ''), extraSymbols[key][0].location.uri.replace('file://', '')) }, documentation: foundInVariableInfo?.definition ?? '', - kind: symbolKindToCompletionKind(extraSymbols[key].kind), + kind: symbolKindToCompletionKind(extraSymbols[key][0].kind), data: { referenceUrl: foundInVariableInfo?.referenceUrl }, @@ -297,6 +304,15 @@ function convertExtraSymbolsToCompletionItems (uri: string): CompletionItem[] { completionItems.push(completionItem) }) }) + // Filter duplicates from the included files, current goal is to show only one item for one symbol even though it occurs in multiple included files. The one that remains will still contain the path in its label details but it doesn't necessarily indicate the location of the very first occurance as this feature will require extra info from bitbake and it is not yet planned. + const uniqueItems = new Set() + completionItems = completionItems.filter(item => { + if (!uniqueItems.has(item.label)) { + uniqueItems.add(item.label) + return true + } + return false + }) return completionItems } diff --git a/server/src/connectionHandlers/onDefinition.ts b/server/src/connectionHandlers/onDefinition.ts index 178d49cd..c98e9ce5 100644 --- a/server/src/connectionHandlers/onDefinition.ts +++ b/server/src/connectionHandlers/onDefinition.ts @@ -39,19 +39,33 @@ export function onDefinitionHandler (textDocumentPositionParams: TextDocumentPos } if (word !== null) { - if (analyzer.isIdentifierOfVariableAssignment(textDocumentPositionParams)) { - const definitions: Definition = [] + const definitions: Definition = [] + const canProvideGoToDefinitionForSymbol = analyzer.isIdentifierOfVariableAssignment(textDocumentPositionParams) || + (analyzer.isVariableExpansion(documentUri, position.line, position.character) && analyzer.isIdentifier(textDocumentPositionParams)) + if (canProvideGoToDefinitionForSymbol) { analyzer.getExtraSymbolsForUri(documentUri).forEach((globalDeclaration) => { if (globalDeclaration[word] !== undefined) { - definitions.push({ - uri: globalDeclaration[word].location.uri, - range: globalDeclaration[word].location.range + globalDeclaration[word].forEach((symbol) => { + definitions.push({ + uri: symbol.location.uri, + range: symbol.location.range + }) }) } }) - return definitions + const ownSymbol = analyzer.getAnalyzedDocument(documentUri)?.globalDeclarations[word] + if (ownSymbol !== undefined) { + ownSymbol.forEach((symbol) => { + definitions.push({ + uri: symbol.location.uri, + range: symbol.location.range + }) + }) + } } + + return definitions } return getDefinition(textDocumentPositionParams, documentAsText) diff --git a/server/src/server.ts b/server/src/server.ts index 0ee7d9a5..18836c71 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -39,6 +39,13 @@ let parseOnSave = true const disposables: Disposable[] = [] +let currentActiveTextDocument: TextDocument = TextDocument.create( + 'file://dummy_uri', + 'bitbake', + 0, + '' +) + connection.onInitialize(async (params: InitializeParams): Promise => { logger.level = 'debug' logger.info('[onInitialize] Initializing connection') @@ -57,6 +64,11 @@ connection.onInitialize(async (params: InitializeParams): Promise { + logger.debug('[On scanReady] Analyzing the current document again...') + void analyzer.analyze({ document: currentActiveTextDocument, uri: currentActiveTextDocument.uri }) + }) + return { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, @@ -140,6 +152,8 @@ documents.onDidChangeContent(async (event) => { void connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) } + currentActiveTextDocument = textDocument + // Other language extensions might also associate .conf files with their langauge modes if (textDocument.uri.endsWith('.conf')) { logger.debug('verifyConfigurationFileAssociation') diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index 02b9a800..7244c58b 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -103,11 +103,13 @@ export default class Analyzer { } public getGlobalDeclarationSymbols (uri: string): SymbolInformation[] { - const symbols: SymbolInformation[] = [] + let symbols: SymbolInformation[] = [] const analyzedDocument = this.uriToAnalyzedDocument[uri] if (analyzedDocument !== undefined) { const { globalDeclarations } = analyzedDocument - Object.values(globalDeclarations).forEach((symbol) => symbols.push(symbol)) + Object.values(globalDeclarations).forEach((symbolArray) => { + symbols = symbols.concat(symbolArray) + }) return symbols } return [] diff --git a/server/src/tree-sitter/declarations.ts b/server/src/tree-sitter/declarations.ts index eca6835a..48bb6c70 100644 --- a/server/src/tree-sitter/declarations.ts +++ b/server/src/tree-sitter/declarations.ts @@ -23,7 +23,7 @@ const TREE_SITTER_TYPE_TO_LSP_KIND: Record = * An object that contains the symbol information of all the global declarations. * Referenced by the symbol name */ -export type GlobalDeclarations = Record +export type GlobalDeclarations = Record const GLOBAL_DECLARATION_NODE_TYPES = new Set([ 'function_definition', @@ -54,7 +54,10 @@ export function getGlobalDeclarations ({ if (symbol !== null) { const word = symbol.name // Note that this can include BITBAKE_VARIABLES (e.g DESCRIPTION = ''), it will be used for completion later. But BITBAKE_VARIABLES are also added as completion from doc scanner. The remove of duplicates will happen there. - globalDeclarations[word] = symbol + if (globalDeclarations[word] === undefined) { + globalDeclarations[word] = [] + } + globalDeclarations[word].push(symbol) } return followChildren