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: Go to definition for symbols in string content using tree-sitter #20

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
80 changes: 80 additions & 0 deletions server/src/__tests__/definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
})
})
6 changes: 5 additions & 1 deletion server/src/__tests__/fixtures/directive.bb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ inherit baz

APPEND = 'append bar'
APPEND:append = 'append bar'
FOO = '${APPEND}
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. \
'
117 changes: 18 additions & 99 deletions server/src/connectionHandlers/onDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
100 changes: 94 additions & 6 deletions server/src/tree-sitter/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -33,6 +34,7 @@ interface AnalyzedDocument {
embeddedRegions: EmbeddedRegions
tree: Parser.Tree
extraSymbols?: GlobalDeclarations[] // symbols from the include files
symbolsInStringContent?: SymbolInformation[]
}

export default class Analyzer {
Expand Down Expand Up @@ -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[] = []
Expand All @@ -82,7 +85,8 @@ export default class Analyzer {
globalDeclarations,
embeddedRegions,
tree,
extraSymbols
extraSymbols,
symbolsInStringContent
}

let debouncedExecuteAnalyzation = this.debouncedExecuteAnalyzation
Expand Down Expand Up @@ -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 = /(?<![-.:])\b(\w+)\b(?![-.:])/g
TreeSitterUtils.forEach(tree.rootNode, (n) => {
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) => {
deribaucourt marked this conversation as resolved.
Show resolved Hide resolved
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()
Loading