From b7008fa7670525e825c082e78ed3f6685e103dca Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Mon, 4 Dec 2023 14:18:33 -0500 Subject: [PATCH 1/3] Chore: Add functions to extract, store and get symbols in string content --- server/src/tree-sitter/analyzer.ts | 100 +++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index 6e5d6e7d..b4f027f6 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -8,11 +8,12 @@ * Reference: https://github.com/bash-lsp/bash-language-server/blob/8c42218c77a9451b308839f9a754abde901323d5/server/src/analyser.ts */ -import type { - TextDocumentPositionParams, - Diagnostic, - SymbolInformation, - Range +import { + type TextDocumentPositionParams, + type Diagnostic, + type SymbolInformation, + type Range, + SymbolKind } from 'vscode-languageserver' import type Parser from 'web-tree-sitter' import { TextDocument } from 'vscode-languageserver-textdocument' @@ -33,6 +34,7 @@ interface AnalyzedDocument { embeddedRegions: EmbeddedRegions tree: Parser.Tree extraSymbols?: GlobalDeclarations[] // symbols from the include files + symbolsInStringContent?: SymbolInformation[] } export default class Analyzer { @@ -72,6 +74,7 @@ export default class Analyzer { const tree = this.parser.parse(fileContent) const globalDeclarations = getGlobalDeclarations({ tree, uri }) + const symbolsInStringContent = this.getSymbolsInStringContent(tree, uri) const embeddedRegions = getEmbeddedRegionsFromNode(tree, uri) /* eslint-disable-next-line prefer-const */ let extraSymbols: GlobalDeclarations[] = [] @@ -82,7 +85,8 @@ export default class Analyzer { globalDeclarations, embeddedRegions, tree, - extraSymbols + extraSymbols, + symbolsInStringContent } let debouncedExecuteAnalyzation = this.debouncedExecuteAnalyzation @@ -481,6 +485,90 @@ export default class Analyzer { }) return fileUris } + + /** + * Extract symbols from the string content of the tree + */ + public getSymbolsInStringContent (tree: Parser.Tree, uri: string): SymbolInformation[] { + const symbolInformation: SymbolInformation[] = [] + const wholeWordRegex = /(? { + if (n.type === 'string_content') { + const splittedStringContent = n.text.split(/\n/g) + for (let i = 0; i < splittedStringContent.length; i++) { + const line = splittedStringContent[i] + for (const match of line.matchAll(wholeWordRegex)) { + if (match !== undefined && uri !== undefined) { + const start = { + line: n.startPosition.row + i, + character: match.index !== undefined ? match.index + n.startPosition.column : 0 + } + const end = { + line: n.startPosition.row + i, + character: match.index !== undefined ? match.index + n.startPosition.column + match[0].length : 0 + } + if (i > 0) { + start.character = match.index ?? 0 + end.character = (match.index ?? 0) + match[0].length + } + const foundRecipe = bitBakeProjectScannerClient.bitbakeScanResult._recipes.find((recipe) => { + return recipe.name === match[0] + }) + if (foundRecipe !== undefined) { + if (foundRecipe?.path !== undefined) { + symbolInformation.push({ + name: match[0], + kind: SymbolKind.Variable, + location: { + range: { + start, + end + }, + uri: 'file://' + foundRecipe.path.dir + '/' + foundRecipe.path.base + } + }) + } + if (foundRecipe?.appends !== undefined && foundRecipe.appends.length > 0) { + foundRecipe.appends.forEach((append) => { + symbolInformation.push({ + name: append.name, + kind: SymbolKind.Variable, + location: { + range: { + start, + end + }, + uri: 'file://' + append.dir + '/' + append.base + } + }) + }) + } + } + } + } + } + } + return true + }) + + return symbolInformation + } + + public getSymbolInStringContentForPosition (uri: string, line: number, column: number): SymbolInformation[] | undefined { + const analyzedDocument = this.uriToAnalyzedDocument[uri] + if (analyzedDocument?.symbolsInStringContent !== undefined) { + const { symbolsInStringContent } = analyzedDocument + const allSymbolsFoundAtPosition: SymbolInformation[] = [] // recipe + appends + for (const symbol of symbolsInStringContent) { + const { location: { range } } = symbol + if (line === range.start.line && column >= range.start.character && column <= range.end.character) { + allSymbolsFoundAtPosition.push(symbol) + } + } + return allSymbolsFoundAtPosition + } + return undefined + } } export const analyzer: Analyzer = new Analyzer() From 4a32ec63e07126e83922bf5b7081ddf835fe1249 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Mon, 4 Dec 2023 14:21:41 -0500 Subject: [PATCH 2/3] Feat: Implement go-to-definition for symbols in string content using tree-sitter --- server/src/connectionHandlers/onDefinition.ts | 117 +++--------------- 1 file changed, 18 insertions(+), 99 deletions(-) diff --git a/server/src/connectionHandlers/onDefinition.ts b/server/src/connectionHandlers/onDefinition.ts index ac95cc08..cde6753d 100644 --- a/server/src/connectionHandlers/onDefinition.ts +++ b/server/src/connectionHandlers/onDefinition.ts @@ -9,7 +9,7 @@ import { analyzer } from '../tree-sitter/analyzer' import { type DirectiveStatementKeyword } from '../lib/src/types/directiveKeywords' import { bitBakeProjectScannerClient } from '../BitbakeProjectScannerClient' import path, { type ParsedPath } from 'path' -import { type PathInfo, type ElementInfo } from '../lib/src/types/BitbakeScanResult' +import { type ElementInfo } from '../lib/src/types/BitbakeScanResult' export function onDefinitionHandler (textDocumentPositionParams: TextDocumentPositionParams): Definition | null { const { textDocument: { uri: documentUri }, position } = textDocumentPositionParams @@ -45,6 +45,7 @@ export function onDefinitionHandler (textDocumentPositionParams: TextDocumentPos const definitions: Definition = [] const canProvideGoToDefinitionForSymbol = analyzer.isIdentifierOfVariableAssignment(textDocumentPositionParams) || (analyzer.isVariableExpansion(documentUri, position.line, position.character) && analyzer.isIdentifier(textDocumentPositionParams)) + // Variables in declartion and variable expansion syntax if (canProvideGoToDefinitionForSymbol) { analyzer.getExtraSymbolsForUri(documentUri).forEach((globalDeclaration) => { if (globalDeclaration[word] !== undefined) { @@ -66,12 +67,25 @@ export function onDefinitionHandler (textDocumentPositionParams: TextDocumentPos }) }) } - } - return definitions + return definitions + } + // Symbols in string content + if (analyzer.isStringContent(documentUri, position.line, position.character)) { + const allSymbolsFoundAtPosition = analyzer.getSymbolInStringContentForPosition(documentUri, position.line, position.character) + if (allSymbolsFoundAtPosition !== undefined) { + allSymbolsFoundAtPosition.forEach((symbol) => { + definitions.push({ + uri: symbol.location.uri, + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } + }) + }) + return definitions + } + } } - return getDefinition(textDocumentPositionParams, documentAsText) + return [] } function getDefinitionForDirectives (directiveStatementKeyword: DirectiveStatementKeyword, symbol: string): Definition { @@ -119,98 +133,3 @@ function createDefinitionLocationForPathInfo (path: ParsedPath): Location { return location } - -function getDefinition (textDocumentPositionParams: TextDocumentPositionParams, documentAsText: string[]): Definition { - let definition: Definition = [] - - const currentLine = documentAsText[textDocumentPositionParams.position.line] - const symbol = extractSymbolFromLine(textDocumentPositionParams, currentLine) - - definition = createDefinitionForSymbol(symbol) - return definition -} - -function createDefinitionForSymbol (symbol: string): Definition { - return createDefinitionForSymbolRecipes(symbol) -} - -function createDefinitionForSymbolRecipes (symbol: string): Definition { - let definitions: Definition = [] - - const recipe: ElementInfo | undefined = bitBakeProjectScannerClient.bitbakeScanResult._recipes.find((obj: ElementInfo): boolean => { - return obj.name === symbol - }) - - if (recipe?.path !== undefined) { - let definitionsList: PathInfo[] = new Array < PathInfo >(recipe.path) - - if ((recipe.appends !== undefined) && (recipe.appends.length > 0)) { - definitionsList = definitionsList.concat(recipe.appends) - } - definitions = createDefinitionLocationForPathInfoList(definitionsList) - } - - return definitions -} - -function createDefinitionLocationForPathInfoList (pathInfoList: PathInfo[]): Definition { - let definition: Definition = [] - - if ((pathInfoList !== undefined) && (pathInfoList.length > 0)) { - if (pathInfoList.length > 1) { - definition = new Array < Location >() - - for (const pathInfo of pathInfoList) { - logger.debug(`definition ${JSON.stringify(pathInfo)}`) - const location: Location = createDefinitionLocationForPathInfo(pathInfo) - - definition.push(location) - } - } else { - definition = createDefinitionLocationForPathInfo(pathInfoList[0]) - } - } - - return definition -} - -function extractSymbolFromLine (textDocumentPositionParams: TextDocumentPositionParams, currentLine: string): string { - logger.debug(`getDefinitionForSymbol ${currentLine}`) - const linePosition: number = textDocumentPositionParams.position.character - let symbolEndPosition: number = currentLine.length - let symbolStartPosition: number = 0 - const rightBorderCharacter: string[] = [' ', '=', '/', '$', '+', '}', '\'', '\'', ']', '['] - const leftBorderCharacter: string[] = [' ', '=', '/', '+', '{', '\'', '\'', '[', ']'] - - for (const character of rightBorderCharacter) { - let temp: number = currentLine.indexOf(character, linePosition) - if (temp === -1) { - temp = currentLine.length - } - symbolEndPosition = Math.min(symbolEndPosition, temp) - } - - const symbolRightTrimed = currentLine.substring(0, symbolEndPosition) - logger.debug(`symbolRightTrimed ${symbolRightTrimed}`) - - for (const character of leftBorderCharacter) { - let temp: number = symbolRightTrimed.lastIndexOf(character, linePosition) - if (temp === -1) { - temp = 0 - } - symbolStartPosition = Math.max(symbolStartPosition, temp) - } - - let symbol: string = symbolRightTrimed.substring(symbolStartPosition) - - for (const character of leftBorderCharacter.concat('-')) { - if (symbol.startsWith(character)) { - symbol = symbol.substring(1) - break - } - } - - logger.debug(`symbol ${symbol}`) - - return symbol -} From 72dda9bf71671caecf1cf5e423358586b06f82f0 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Mon, 4 Dec 2023 14:22:26 -0500 Subject: [PATCH 3/3] Test: Add tests for go-to-definition for symbols in string content --- server/src/__tests__/definition.test.ts | 80 ++++++++++++++++++++++ server/src/__tests__/fixtures/directive.bb | 6 +- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/definition.test.ts b/server/src/__tests__/definition.test.ts index 8e5cbcc8..045ccfa6 100644 --- a/server/src/__tests__/definition.test.ts +++ b/server/src/__tests__/definition.test.ts @@ -158,4 +158,84 @@ describe('on definition', () => { ]) ) }) + + it('provides go to definition for symbols found in the string content', async () => { + const parsedHoverPath = path.parse(FIXTURE_DOCUMENT.HOVER.uri.replace('file://', '')) + + bitBakeProjectScannerClient.bitbakeScanResult._recipes = [ + { + name: parsedHoverPath.name, + path: parsedHoverPath, + appends: [ + { + root: parsedHoverPath.root, + dir: parsedHoverPath.dir, + base: 'hover-append.bbappend', + ext: 'bbappend', + name: 'hover-append' + } + ], + extraInfo: 'layer: core' + } + ] + + await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.DIRECTIVE + }) + + const shouldWork1 = onDefinitionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 7, + character: 21 + } + }) + + const shouldWork2 = onDefinitionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 8, + character: 22 + } + }) + + const shouldWork3 = onDefinitionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 9, + character: 14 + } + }) + + const shouldNotWork = onDefinitionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 9, + character: 31 + } + }) + + expect(shouldWork1).toEqual([ + { + uri: FIXTURE_URI.HOVER, + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } + }, + { range: { end: { character: 0, line: 0 }, start: { character: 0, line: 0 } }, uri: 'file://' + parsedHoverPath.dir + '/hover-append.bbappend' } + ]) + + expect(shouldWork2).toEqual(shouldWork1) + + expect(shouldWork3).toEqual(shouldWork1) + + expect(shouldNotWork).toEqual([]) + }) }) diff --git a/server/src/__tests__/fixtures/directive.bb b/server/src/__tests__/fixtures/directive.bb index 6ba9073c..ee555f6e 100644 --- a/server/src/__tests__/fixtures/directive.bb +++ b/server/src/__tests__/fixtures/directive.bb @@ -4,4 +4,8 @@ inherit baz APPEND = 'append bar' APPEND:append = 'append bar' -FOO = '${APPEND} \ No newline at end of file +FOO = '${APPEND}' +SYMBOL_IN_STRING = 'hover is a package ${FOO} \ + parentFolder/hover should also be seen as symbol \ + this hover too, other words should not. \ + ' \ No newline at end of file