-
Notifications
You must be signed in to change notification settings - Fork 11
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 uris in string content #23
Changes from all commits
3dbf117
76dd599
5bbe950
6453d5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import { type DirectiveStatementKeyword } from '../lib/src/types/directiveKeywor | |
import { bitBakeProjectScannerClient } from '../BitbakeProjectScannerClient' | ||
import path, { type ParsedPath } from 'path' | ||
import { type ElementInfo } from '../lib/src/types/BitbakeScanResult' | ||
import fs from 'fs' | ||
|
||
export function onDefinitionHandler (textDocumentPositionParams: TextDocumentPositionParams): Definition | null { | ||
const { textDocument: { uri: documentUri }, position } = textDocumentPositionParams | ||
|
@@ -72,16 +73,38 @@ export function onDefinitionHandler (textDocumentPositionParams: TextDocumentPos | |
} | ||
// 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 | ||
const wholeWordRegex = /(?<![-.:])\b(\w+)\b(?![-.:])/g | ||
const uriRegex = /(file:\/\/)(?<uri>.*)\b/g | ||
const [uriAtPosition] = analyzer.getSymbolsInStringContent(documentUri, position.line, position.character, uriRegex) | ||
if (uriAtPosition !== undefined) { | ||
const { workspaceFolders } = analyzer | ||
if (workspaceFolders !== undefined && workspaceFolders !== null) { | ||
for (const workspaceFolder of workspaceFolders) { | ||
const filePath = findFileInDirectory(workspaceFolder.uri.replace('file://', ''), uriAtPosition.name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user's workspace may contain several yocto layers and build directories which are not ignored here. Looking up the whole workspaceFolder will be inaccurate and slow. I suggest rather searching only the directory or immediate parent of the recipe file. |
||
if (filePath !== null) { | ||
definitions.push( | ||
{ | ||
uri: 'file://' + filePath, | ||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } | ||
} | ||
) | ||
break | ||
} | ||
} | ||
return definitions | ||
} | ||
} | ||
|
||
const allSymbolsAtPosition = analyzer.getSymbolsInStringContent(documentUri, position.line, position.character, wholeWordRegex) | ||
|
||
allSymbolsAtPosition.forEach((symbol) => { | ||
definitions.push({ | ||
uri: symbol.location.uri, | ||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } | ||
}) | ||
}) | ||
|
||
return definitions | ||
} | ||
} | ||
|
||
|
@@ -133,3 +156,22 @@ function createDefinitionLocationForPathInfo (path: ParsedPath): Location { | |
|
||
return location | ||
} | ||
|
||
function findFileInDirectory (dir: string, fileName: string): string | null { | ||
try { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a library for finding files, I don't think there is added value in defining this ourselves. |
||
const filePaths = fs.readdirSync(dir).map(name => path.join(dir, name)) | ||
for (const filePath of filePaths) { | ||
if (fs.statSync(filePath).isDirectory()) { | ||
const result = findFileInDirectory(filePath, fileName) | ||
if (result !== null) return result | ||
} else if (path.basename(filePath) === fileName) { | ||
return filePath | ||
} | ||
} | ||
} catch { | ||
logger.debug(`[findFileInDirectory] ${dir} not found`) | ||
return null | ||
} | ||
|
||
return null | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,8 @@ import { | |
type Diagnostic, | ||
type SymbolInformation, | ||
type Range, | ||
SymbolKind | ||
SymbolKind, | ||
type WorkspaceFolder | ||
} from 'vscode-languageserver' | ||
import type Parser from 'web-tree-sitter' | ||
import { TextDocument } from 'vscode-languageserver-textdocument' | ||
|
@@ -34,13 +35,13 @@ interface AnalyzedDocument { | |
embeddedRegions: EmbeddedRegions | ||
tree: Parser.Tree | ||
extraSymbols?: GlobalDeclarations[] // symbols from the include files | ||
symbolsInStringContent?: SymbolInformation[] | ||
} | ||
|
||
export default class Analyzer { | ||
private parser?: Parser | ||
private uriToAnalyzedDocument: Record<string, AnalyzedDocument | undefined> = {} | ||
private debouncedExecuteAnalyzation?: ReturnType<typeof debounce> | ||
public workspaceFolders: WorkspaceFolder[] | undefined | null = [] | ||
|
||
public getDocumentTexts (uri: string): string[] | undefined { | ||
return this.uriToAnalyzedDocument[uri]?.document.getText().split(/\r?\n/g) | ||
|
@@ -74,7 +75,6 @@ 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[] = [] | ||
|
@@ -85,8 +85,7 @@ export default class Analyzer { | |
globalDeclarations, | ||
embeddedRegions, | ||
tree, | ||
extraSymbols, | ||
symbolsInStringContent | ||
extraSymbols | ||
} | ||
|
||
let debouncedExecuteAnalyzation = this.debouncedExecuteAnalyzation | ||
|
@@ -516,85 +515,84 @@ export default class Analyzer { | |
/** | ||
* 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 | ||
public getSymbolsInStringContent (uri: string, line: number, character: number, regex: RegExp): SymbolInformation[] { | ||
const allSymbolsAtPosition: SymbolInformation[] = [] | ||
const n = this.nodeAtPoint(uri, line, character) | ||
if (n?.type === 'string_content') { | ||
const splittedStringContent = n.text.split(/\n/g) | ||
for (let i = 0; i < splittedStringContent.length; i++) { | ||
const lineText = splittedStringContent[i] | ||
for (const match of lineText.matchAll(regex)) { | ||
const matchedUri = match.groups?.uri | ||
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 | ||
} | ||
if (this.positionIsInRange(line, character, { start, end })) { | ||
if (matchedUri !== undefined) { | ||
return [{ | ||
name: matchedUri, | ||
kind: SymbolKind.String, | ||
location: { | ||
range: { | ||
start, | ||
end | ||
}, | ||
uri | ||
} | ||
}] | ||
} | ||
const foundRecipe = bitBakeProjectScannerClient.bitbakeScanResult._recipes.find((recipe) => { | ||
return recipe.name === match[0] | ||
}) | ||
if (foundRecipe !== undefined) { | ||
if (foundRecipe?.path !== undefined) { | ||
allSymbolsAtPosition.push({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure there is a need for go to definition for URIs. They are usually related only to the current file itself. Why could several recipe files be matched? Only the opened one should be suggested I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of providing "Go to definition", register a https://code.visualstudio.com/api/references/vscode-api#DocumentLinkProvider |
||
name: match[0], | ||
kind: SymbolKind.Variable, | ||
location: { | ||
range: { | ||
start, | ||
end | ||
}, | ||
uri: 'file://' + foundRecipe.path.dir + '/' + foundRecipe.path.base | ||
} | ||
}) | ||
} | ||
const foundRecipe = bitBakeProjectScannerClient.bitbakeScanResult._recipes.find((recipe) => { | ||
return recipe.name === match[0] | ||
}) | ||
if (foundRecipe !== undefined) { | ||
if (foundRecipe?.path !== undefined) { | ||
symbolInformation.push({ | ||
name: match[0], | ||
if (foundRecipe?.appends !== undefined && foundRecipe.appends.length > 0) { | ||
foundRecipe.appends.forEach((append) => { | ||
allSymbolsAtPosition.push({ | ||
name: append.name, | ||
kind: SymbolKind.Variable, | ||
location: { | ||
range: { | ||
start, | ||
end | ||
}, | ||
uri: 'file://' + foundRecipe.path.dir + '/' + foundRecipe.path.base | ||
uri: 'file://' + append.dir + '/' + append.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 | ||
return allSymbolsAtPosition | ||
} | ||
|
||
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 | ||
public positionIsInRange (line: number, character: number, range: Range): boolean { | ||
return line === range.start.line && character >= range.start.character && character <= range.end.character | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird for a bitbake file to provide the same URI twice. Not sure it would be compile through bitbake.