diff --git a/.vscode/launch.json b/.vscode/launch.json index 670d6e6..58df78a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,18 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: compile" + }, + { + "name": "Run Extension (watch)", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] }, { "name": "Extension Tests", @@ -28,7 +39,7 @@ "outFiles": [ "${workspaceFolder}/out/test/**/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: compile" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3b17e53..3946211 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,6 +3,14 @@ { "version": "2.0.0", "tasks": [ + { + "type": "npm", + "script": "compile", + "group": { + "kind": "build", + "isDefault": true + } + }, { "type": "npm", "script": "watch", @@ -11,10 +19,7 @@ "presentation": { "reveal": "never" }, - "group": { - "kind": "build", - "isDefault": true - } + "group": "build" } ] } diff --git a/.vscodeignore b/.vscodeignore index 6d17873..16e0171 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -7,3 +7,4 @@ vsc-extension-quickstart.md **/.eslintrc.json **/*.map **/*.ts +scripts/** diff --git a/LICENSE.txt b/LICENSE.txt index bd0820c..a9039f7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Intel Corporation +Copyright (c) 2025 Intel Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,5 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + +SOFTWARE. diff --git a/README.md b/README.md index 5358679..82831e8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Ubuntu installation example: `sudo apt install cscope` ## Documentation -Check full documentation including setup on [project wiki page](https://github.com/intel/Edk2Code/wiki) +Check full documentation including setup on [project home page](https://intel.github.io/Edk2Code/) ## Contributing diff --git a/languages/syntaxes/edk2_fdf.tmLanguage.json b/languages/syntaxes/edk2_fdf.tmLanguage.json index 3a090b0..520c0ae 100644 --- a/languages/syntaxes/edk2_fdf.tmLanguage.json +++ b/languages/syntaxes/edk2_fdf.tmLanguage.json @@ -12,6 +12,10 @@ { "//": "TO-DO: Too much specific keywords in FDF, should revisit for finding better way of color syntax." }, + { + "name": "variable.language.edk2_dsc", + "match": "\\$\\(.*?\\)" + }, { "name": "variable.language.edk2_fdf", "match": "((? = new Map(); let dscFiles:Set = new Set(); @@ -43,7 +43,7 @@ export class BuildFolder { for (const buildOption of l.split(",")) { let [val, data] = split(buildOption, ":", 2); - gDebugLog.verbose(`Define: ${val}: ${data}`); + gDebugLog.trace(`Define: ${val}: ${data}`); buildDefines.set(val.trim(), data.trim()); } } @@ -60,7 +60,7 @@ export class BuildFolder { const newBuildActivePlatform = path.normalize(buildActivePlatform).replace(oldWorkspacePath, gWorkspacePath); if (fs.existsSync(newBuildActivePlatform)) { buildActivePlatform = newBuildActivePlatform; - gDebugLog.verbose(`Corrected Active platform: ${buildActivePlatform}`); + gDebugLog.trace(`Corrected Active platform: ${buildActivePlatform}`); if(this.replaceWorkspacePath !== undefined && this.replaceWorkspacePath !== oldWorkspacePath){ gDebugLog.error(`Multiple original workspace paths found: ${this.replaceWorkspacePath} and ${oldWorkspacePath}`); } @@ -77,7 +77,7 @@ export class BuildFolder { buildActivePlatform = getRealPathRelative(buildActivePlatform); - gDebugLog.verbose(`Active platform: ${buildActivePlatform}`); + gDebugLog.trace(`Active platform: ${buildActivePlatform}`); dscFiles.add(buildActivePlatform); } } @@ -100,7 +100,7 @@ export class BuildFolder { } const part = wrongPathParts.shift(); oldPathWorkspace.push(part); - gDebugLog.verbose(`Removed part: ${part}`); + gDebugLog.trace(`Removed part: ${part}`); } return undefined; } @@ -168,7 +168,14 @@ export class BuildFolder { let filteredCscope = []; for (const value of cscopeMap.values()) { try { - filteredCscope.push(value.replace(/\n$/, "")); + let cleanValue = value.replace(/\n$/, ""); + filteredCscope.push(cleanValue); + // For .dec files, add the package directory to package paths + let unquoted = cleanValue.replace(/^"|"$/g, ""); + if(unquoted.toLowerCase().endsWith(".dec")){ + let decPackageDir = getRealPathRelative(path.dirname(unquoted)); + gConfigAgent.pushBuildPackagePaths(decPackageDir); + } } catch (error) { } diff --git a/src/Languages/completionProvider.ts b/src/Languages/completionProvider.ts index 3d5f770..f1e29fe 100644 --- a/src/Languages/completionProvider.ts +++ b/src/Languages/completionProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { ParserFactory } from '../edkParser/parserFactory'; +import { getParserForDocument } from '../edkParser/parserFactory'; import { gDebugLog } from '../extension'; @@ -10,13 +10,11 @@ export class EdkCompletionProvider implements vscode.CompletionItemProvider { } async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext) { - let factory = new ParserFactory(); - let parser = factory.getParser(document); + let parser = await getParserForDocument(document); if(parser){ - await parser.parseFile(); let selectedSymbol = parser.getSelectedSymbol(position); if (!selectedSymbol) { return []; } - gDebugLog.verbose(`Completion for: ${selectedSymbol.toString()}`); + gDebugLog.trace(`Completion for: ${selectedSymbol.toString()}`); if (selectedSymbol.onCompletion !== undefined) { let temp = await selectedSymbol.onCompletion(document, position, token, context); diff --git a/src/Languages/declarationProvider.ts b/src/Languages/declarationProvider.ts index ebec73b..21b1cf3 100644 --- a/src/Languages/declarationProvider.ts +++ b/src/Languages/declarationProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { ParserFactory } from '../edkParser/parserFactory'; +import { getParserForDocument } from '../edkParser/parserFactory'; import { gDebugLog } from '../extension'; @@ -10,13 +10,11 @@ export class EdkDeclarationProvider implements vscode.DeclarationProvider { } async provideDeclaration(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) { - let factory = new ParserFactory(); - let parser = factory.getParser(document); + let parser = await getParserForDocument(document); if(parser){ - await parser.parseFile(); let selectedSymbol = parser.getSelectedSymbol(position); if (!selectedSymbol) { return []; } - gDebugLog.verbose(`Definition for: ${selectedSymbol.toString()}`); + gDebugLog.trace(`Definition for: ${selectedSymbol.toString()}`); if (selectedSymbol.onDeclaration !== undefined) { let temp = await selectedSymbol.onDeclaration(); return temp; diff --git a/src/Languages/definitionProvider.ts b/src/Languages/definitionProvider.ts index 5d91bf4..0cccf50 100644 --- a/src/Languages/definitionProvider.ts +++ b/src/Languages/definitionProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { ParserFactory } from '../edkParser/parserFactory'; +import { getParserForDocument } from '../edkParser/parserFactory'; import { gDebugLog, gEdkWorkspaces } from '../extension'; import { REGEX_PCD, REGEX_VAR_USAGE } from '../edkParser/commonParser'; import { split } from '../utils'; @@ -57,16 +57,21 @@ export class EdkDefinitionProvider implements vscode.DefinitionProvider { - let factory = new ParserFactory(); - let parser = factory.getParser(document); + let parser = await getParserForDocument(document); if(parser){ - await parser.parseFile(); let selectedSymbol = parser.getSelectedSymbol(position); if (!selectedSymbol) { return []; } - gDebugLog.verbose(`Definition for: ${selectedSymbol.toString()}`); + gDebugLog.trace(`Definition for: ${selectedSymbol.toString()}`); if (selectedSymbol.onDefinition !== undefined) { - let temp = await selectedSymbol.onDefinition(parser); - return temp; + let locations: vscode.Location[] = await selectedSymbol.onDefinition(parser); + if (!locations || locations.length === 0) { return []; } + // Return LocationLink[] so VS Code highlights the full symbol range on Ctrl+hover + return locations.map(loc => ({ + originSelectionRange: selectedSymbol!.selectionRange, + targetUri: loc.uri, + targetRange: loc.range, + targetSelectionRange: loc.range, + } as vscode.LocationLink)); } } } diff --git a/src/Languages/symbolProvider.ts b/src/Languages/symbolProvider.ts index 3b4cd13..3c839c7 100644 --- a/src/Languages/symbolProvider.ts +++ b/src/Languages/symbolProvider.ts @@ -3,9 +3,10 @@ import * as fs from 'fs'; import { getStaticPath, itsPcdSelected } from '../utils'; import path = require('path'); import { CompletionItemKind } from 'vscode'; -import { ParserFactory } from '../edkParser/parserFactory'; -import { gConfigAgent, gEdkWorkspaces, gGrayOutController } from '../extension'; +import { getParserForDocument } from '../edkParser/parserFactory'; +import { gConfigAgent, gEdkWorkspaces } from '../extension'; import { Debouncer } from '../debouncer'; +import { DiagnosticManager } from '../diagnostics'; export class EdkSymbolProvider implements vscode.DocumentSymbolProvider { @@ -37,11 +38,8 @@ export class EdkSymbolProvider implements vscode.DocumentSymbolProvider { public async provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken) { // Create a parser for the document - let factory = new ParserFactory(); - let parser = factory.getParser(document); + let parser = await getParserForDocument(document); if (parser) { - - await parser.parseFile(); return parser.symbolsTree; } return []; diff --git a/src/TreeDataProvider.ts b/src/TreeDataProvider.ts deleted file mode 100644 index 077c59a..0000000 --- a/src/TreeDataProvider.ts +++ /dev/null @@ -1,599 +0,0 @@ -import path = require('path'); -import * as vscode from 'vscode'; -import { TreeItemLabel } from 'vscode'; -import { edkLensTreeDetailProvider, gCompileCommands, gEdkWorkspaces, gMapFileManager, gPathFind, gWorkspacePath } from './extension'; -import { InfParser } from './edkParser/infParser'; -import { getParser } from './edkParser/parserFactory'; -import { Edk2SymbolType } from './symbols/symbolsType'; -import { EdkSymbolInfLibrary, EdkSymbolInfSource } from './symbols/infSymbols'; -import { documentGetText, documentGetTextSync, getAllSymbols, getSymbolAtLocation, openTextDocument, openTextDocumentInRange } from './utils'; -import { EdkWorkspace } from './index/edkWorkspace'; -import { EdkSymbol } from './symbols/edkSymbols'; -import { DiagnosticManager, EdkDiagnosticCodes } from './diagnostics'; -import { CompileCommandsEntry } from './compileCommands'; -import { TreeItem } from './treeElements/TreeItem'; - - -export class TreeDetailsDataProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - - data: TreeItem[]; - private lastChildren:TreeItem|undefined = undefined; - - constructor() { - this.data = []; - } - - async expandAll(view:vscode.TreeView){ - await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: "Expanding Tree nodes...", - cancellable: true - }, async (progress, reject) => { - for (const item of this.data) { - await item.expand(); - for (const child of item.iterateChildren()) { - await child.expand(); - } - } - }); - } - - async expandAllNode(node:TreeItem, view:vscode.TreeView, reject:vscode.CancellationToken){ - if(reject.isCancellationRequested){ - return; - } - - if(node instanceof Edk2TreeItem){ - if((node as Edk2TreeItem).circularDependency === true){ - return; - } - } - - await node.expand(); - node.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; - - - for (const child of node.children) { - await this.expandAllNode(child, view, reject); - } - await view.reveal(node); - } - - getHierarchy(item: TreeItem, level: number = 0): string { - let result = ' '.repeat(level) +`- ${item.toString()}\n`;// Indentation based on level - for (const child of item.children) { - result += this.getHierarchy(child, level + 1); // Recurse for each child, increasing the level - } - return result; - } - - toString(){ - let result = ''; - for (const item of this.data) { - result += this.getHierarchy(item); - } - return result; - } - - getLastChildren(){ - return this.lastChildren; - } - - addChildren(item:TreeItem){ - if(!item.visible){return;} - this.lastChildren = item; - this.data.push(item); - this.refresh(); - } - - clear(){ - this.lastChildren = undefined; - this.data = []; - this.refresh(); - } - - refresh(): void { - this._onDidChangeTreeData.fire(); - } - - getTreeItem(element: TreeItem): vscode.TreeItem | Thenable { - return element; - } - - getChildren(element?: TreeItem | undefined): vscode.ProviderResult { - if (element === undefined) { - return this.data; - } - return element.children; - } - - getParent?(element: TreeItem): vscode.ProviderResult { - return element.getParent(); - } - - // resolveTreeItem?(item: vscode.TreeItem, element: TreeItem, token: vscode.CancellationToken): vscode.ProviderResult { - // throw new Error('Method not implemented.'); - // } - -} - - - - - -class Edk2TreeItem extends TreeItem { - uri:vscode.Uri; - loaded = false; - workspace:EdkWorkspace; - edkObject:EdkSymbol; - circularDependency = false; - contextModuleUri:vscode.Uri|undefined = undefined; - - constructor(uri:vscode.Uri, position:vscode.Position, wp:EdkWorkspace, edkObject:EdkSymbol){ - super(uri, vscode.TreeItemCollapsibleState.Collapsed); - this.edkObject = edkObject; - this.workspace = wp; - this.uri = uri; - this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; - - this.command = { - "command": "editor.action.peekLocations", - "title":"Open file", - "arguments": [this.uri, position, []] - }; - } - - addChildren(node: Edk2TreeItem|TreeItem) { - node.parent = this; - this.loaded = true; - this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; - this.children.push(node); - - } - - async onExpanded(){ - if(this.children.length > 0){return;} - - edkLensTreeDetailProvider.refresh(); - if(this.loaded === false){ - let contextModule = this.uri; - if(this.contextModuleUri){ - contextModule = this.contextModuleUri; - } - await openLibraryNode(this.uri, contextModule,this,this.workspace); - this.loaded = true; - } - - // If no children, set collapsible state to none to hide the expand arrow - if(this.children.length === 0){ - this.collapsibleState = vscode.TreeItemCollapsibleState.None; - } - - - } -} - - - - -function getIconForSymbolKind(kind: vscode.SymbolKind): string { - switch(kind) { - case vscode.SymbolKind.File: - return "symbol-file"; - case vscode.SymbolKind.Module: - return "symbol-module"; - case vscode.SymbolKind.Namespace: - return "symbol-namespace"; - case vscode.SymbolKind.Package: - return "symbol-package"; - case vscode.SymbolKind.Class: - return "symbol-class"; - case vscode.SymbolKind.Method: - return "symbol-method"; - case vscode.SymbolKind.Property: - return "symbol-property"; - case vscode.SymbolKind.Field: - return "symbol-field"; - case vscode.SymbolKind.Constructor: - return "symbol-constructor"; - case vscode.SymbolKind.Enum: - return "symbol-enum"; - case vscode.SymbolKind.Interface: - return "symbol-interface"; - case vscode.SymbolKind.Function: - return "symbol-function"; - case vscode.SymbolKind.Variable: - return "symbol-variable"; - case vscode.SymbolKind.Constant: - return "symbol-constant"; - case vscode.SymbolKind.String: - return "symbol-string"; - case vscode.SymbolKind.Number: - return "symbol-number"; - case vscode.SymbolKind.Boolean: - return "symbol-boolean"; - case vscode.SymbolKind.Array: - return "symbol-array"; - case vscode.SymbolKind.Object: - return "symbol-object"; - case vscode.SymbolKind.Key: - return "symbol-key"; - case vscode.SymbolKind.Null: - return "symbol-null"; - case vscode.SymbolKind.EnumMember: - return "symbol-enum-member"; - case vscode.SymbolKind.Struct: - return "symbol-struct"; - case vscode.SymbolKind.Event: - return "symbol-event"; - case vscode.SymbolKind.Operator: - return "symbol-operator"; - case vscode.SymbolKind.TypeParameter: - return "symbol-type-parameter"; - default: - return "symbol-misc"; - } -} - -export class SourceSymbolTreeItem extends TreeItem{ - uri:vscode.Uri; - range:vscode.Range; - constructor(uri:vscode.Uri, symbol:vscode.DocumentSymbol){ - super(uri, vscode.TreeItemCollapsibleState.Collapsed); - this.label = symbol.name; - this.description = symbol.detail.length?symbol.detail:vscode.SymbolKind[symbol.kind]; - this.iconPath = new vscode.ThemeIcon(getIconForSymbolKind(symbol.kind)); - this.collapsibleState = vscode.TreeItemCollapsibleState.None; - this.range = symbol.range; - this.uri = uri; - - // Fix typedef label rendering - if(symbol.kind === vscode.SymbolKind.Interface){ - let text = documentGetTextSync(this.uri, symbol.range); - let clearText = text.replace(symbol.detail,"").replace(/\s+/g, ' ').replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '').replaceAll("\n"," ").replaceAll("\r","").trim(); - if(!clearText.match(/^(struct|enum|union)/)){ - this.label = clearText; - } - } - - for (const child of symbol.children) { - let newChild = new SourceSymbolTreeItem(uri,child); - this.addChildren(newChild); - this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; - // Get the type parsing the definition - void documentGetText(this.uri, child.range).then((text) => { - const childType = text.trim().split(" ")[0]; - newChild.description = childType; - }); - } - - if(symbol.kind === vscode.SymbolKind.Field){ - this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; - } - - - this.command = { - "command": "editor.action.peekLocations", - "title":"Open file", - "arguments": [this.uri, symbol.range.start, []] - }; - - } - - async onExpanded() { - await openTextDocumentInRange(this.uri, this.range); - if(this.children.length > 0){return;} - - // Find definition for field - const locations = await vscode.commands.executeCommand('vscode.executeTypeDefinitionProvider', this.uri, this.range.start); - if(locations.length > 0){ - let symbol = await getSymbolAtLocation(locations[0].uri, locations[0]); - if(symbol){ - - if(symbol.kind === vscode.SymbolKind.Interface){ - const locationsType = await vscode.commands.executeCommand('vscode.executeTypeDefinitionProvider',locations[0].uri , locations[0].range.start); - if(locationsType.length > 0){ - symbol = await getSymbolAtLocation(locationsType[0].uri, locationsType[0]); - if(!symbol){return;} - } - } - - let symbolNode = new SourceSymbolTreeItem(locations[0].uri, symbol); - symbolNode.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; - this.addChildren(symbolNode); - }else{ - this.collapsibleState = vscode.TreeItemCollapsibleState.None; - } - } - } - -} - -export class SectionTreeItem extends TreeItem{ - uri: vscode.Uri; - constructor(uri:vscode.Uri, position:vscode.Position, sectionName:string, wp:EdkWorkspace){ - super(sectionName); - this.uri = uri; - this.label = sectionName; - this.iconPath = new vscode.ThemeIcon("array"); - } - - toString(): string { - return `[${this.label}]`; - } - - -} - -export class FileTreeItemLibraryTree extends Edk2TreeItem{ - constructor(uri:vscode.Uri, position:vscode.Position, wp:EdkWorkspace, edkObject:EdkSymbol){ - super(uri, position, wp, edkObject); - - - let name = vscode.workspace.asRelativePath(uri); - this.description = name; - this.label = path.basename(uri.fsPath); - this.iconPath = new vscode.ThemeIcon("file"); - - this.command = { - "command": "vscode.open", - "title":"Open file", - "arguments": [this.uri] - }; - - } -} - -export class FileTreeItem extends TreeItem{ - uri:vscode.Uri; - constructor(uri:vscode.Uri, position:vscode.Position,wp:EdkWorkspace){ - super(uri); - let name = vscode.workspace.asRelativePath(uri); - this.description = name; - this.label = path.basename(uri.fsPath); - this.iconPath = new vscode.ThemeIcon("file"); - this.uri = uri; - this.command = { - "command": "editor.action.peekLocations", - "title":"Open file", - "arguments": [this.uri, position, []] - }; - } -} - -export class HeaderFileTreeItemLibraryTree extends TreeItem{ - systemIncludes:string[]= []; - uri:vscode.Uri; - constructor(uri:vscode.Uri, systemIncludes:string[] = []){ - super(uri, vscode.TreeItemCollapsibleState.Collapsed); - this.uri = uri; - this.systemIncludes = systemIncludes; - - let name = vscode.workspace.asRelativePath(uri); - this.description = name; - this.label = path.basename(uri.fsPath); - this.iconPath = new vscode.ThemeIcon("file"); - - this.command = { - "command": "vscode.open", - "title":"Open file", - "arguments": [this.uri] - }; - } - - async loadIncludes(){ - - } - - async onExpanded(){ - if(this.children.length > 0){return;} - - const includeHeaders = await findHeaderIncludes(this.uri, this.systemIncludes); - const compileCommand = gCompileCommands.getCompileCommandForFile(this.uri.fsPath); - - for (const includeHeader of includeHeaders) { - this.addChildren(includeHeader); - if(compileCommand){ - updateCompileCommandsForHeaderFile(includeHeader, compileCommand); - } - } - - // add symbols - - const sourceSymbols:vscode.DocumentSymbol[] = await getAllSymbols(this.uri); - for (const symbol of sourceSymbols) { - - if(symbol.name.startsWith("__unnamed")){continue;} - const symbolNode = new SourceSymbolTreeItem(this.uri, symbol); - - this.addChildren(symbolNode); - } - - if(this.children.length === 0){ - this.collapsibleState = vscode.TreeItemCollapsibleState.None; - } - - } - -} - - - - -export async function findHeaderIncludes(fileUri:vscode.Uri, systemIncludes:string[]){ - let sourceDocument = await openTextDocument(fileUri); - let sourceText = sourceDocument.getText(); - let lineNo = 0; - let includeNodes:HeaderFileTreeItemLibraryTree[] = []; - for (const line of sourceText.split('\n')) { - let match = /#include\s+(["<])([^">]+)[">]/.exec(line); - if(match){ - let isRelative = false; - - if(match[1] === '"'){ - isRelative = true; - } - let includePath = match[2]; - - let includeUri:vscode.Uri = fileUri; - - if(isRelative){ - // Relative includes - const locations = await gPathFind.findRelativePath(includePath, path.dirname(fileUri.fsPath)); - if(locations){ - includeUri = locations.uri; - } - }else{ - // System includes - for (const compiledIncludePath of systemIncludes) { - const location = await gPathFind.findRelativePath(includePath, compiledIncludePath); - if(location){ - includeUri = location.uri; - break; - } - } - } - - let includeNode = new HeaderFileTreeItemLibraryTree(includeUri, systemIncludes); - includeNodes.push(includeNode); - lineNo++; - - } - } - return includeNodes; -} - - -function updateCompileCommandsForHeaderFile(headerItem:HeaderFileTreeItemLibraryTree, compileCommand:CompileCommandsEntry){ - let tempCompileCommand = new CompileCommandsEntry(compileCommand.command, "", headerItem.uri.fsPath); - gCompileCommands.addCompileCommandForFile(tempCompileCommand); -} - - -/** - * Populates a node with all the libraries found in fileUri. - * - * @param infUri - The URI of the file to be parsed. - * @param node - The library tree item node to which child nodes will be added. - * - * @returns A promise that resolves when the operation is complete. - * - * @throws Will throw an error if the parser cannot be obtained. - * @throws Will throw an error if workspace information cannot be retrieved. - * @throws Will throw an error if library declarations cannot be fetched. - * @throws Will throw an error if path information cannot be found. - */ -export async function openLibraryNode(infUri:vscode.Uri, moduleUri:vscode.Uri, node:FileTreeItemLibraryTree, wp:EdkWorkspace){ - - if(node.circularDependency){return;} // Dont process duplicated elements - - DiagnosticManager.clearProblems(infUri); - let parser = await getParser(infUri) as InfParser; - if(parser){ - // Add librarires - let libraries = parser.getSymbolsType(Edk2SymbolType.infLibrary) as EdkSymbolInfLibrary[]; - if(libraries.length > 0){ - let sectionLib = new SectionTreeItem(infUri,libraries[0].parent!.range.start, "Libraries", wp); - node.addChildren(sectionLib); - for (const library of libraries) { - let libDefinitions = await wp.getLibDeclarationModule(moduleUri, library.name); - if(libDefinitions.length === 0){ - let libNode = new FileTreeItemLibraryTree(infUri, library.location.range.start, wp, library); - libNode.description = "Library not found"; - libNode.label = library.name; - sectionLib.addChildren(libNode); - continue; - } - for (const libDefinition of libDefinitions) { - let filePaths = await gPathFind.findPath(libDefinition.path); - for (const path of filePaths) { - let pos = new vscode.Position(0,0); - - let libNode = new FileTreeItemLibraryTree(path.uri, pos, wp, library); - libNode.contextModuleUri = moduleUri; - sectionLib.addChildren(libNode); - // Check for circular dependencies - if(isCircularDependencies(libNode)){ - libNode.circularDependency = true; - } - } - } - } - } - - - // Add sources - let sources = parser.getSymbolsType(Edk2SymbolType.infSource) as EdkSymbolInfSource[]; - if(sources.length > 0){ - let sectionSource = new SectionTreeItem(infUri,sources[0].parent!.range.start, "Sources", wp, ); - - node.addChildren(sectionSource); - for (const source of sources) { - let filePath = await source.getValue(); - - if(!filePath.toLowerCase().endsWith(".c")){continue;} // Only C files - - let pos = new vscode.Position(0,0); - let fileUri = vscode.Uri.file(filePath); - - let sourceNode = new FileTreeItemLibraryTree(fileUri, pos, wp, source); - sectionSource.addChildren(sourceNode); - - // Find includes - const compileCommand = gCompileCommands.getCompileCommandForFile(fileUri.fsPath); - if(compileCommand){ - const compiledIncludePaths = compileCommand.getIncludePaths(); - const includeHeaders = await findHeaderIncludes(fileUri, compiledIncludePaths); - for (const includeHeader of includeHeaders) { - sourceNode.addChildren(includeHeader); - updateCompileCommandsForHeaderFile(includeHeader, compileCommand); - } - } - - - // Check if the symbol is used - // TODO: Experimental function - if(false){ - const sourceSymbols:vscode.DocumentSymbol[] = await getAllSymbols(fileUri); - for (const symbol of sourceSymbols) { - const symbolNode = new SourceSymbolTreeItem(fileUri, symbol); - if(symbol.kind === vscode.SymbolKind.Function){ - if(!gMapFileManager.isSymbolUsed(symbol.name)){ - DiagnosticManager.warning(fileUri,symbol.range,EdkDiagnosticCodes.unusedSymbol, `Unused function: ${symbol.name}`, [vscode.DiagnosticTag.Unnecessary]); - symbolNode.tooltip = "Unused function"; - symbolNode.description = `Unused ${symbolNode.description}`; - symbolNode.iconPath = new vscode.ThemeIcon("warning"); - } - } - sourceNode.addChildren(symbolNode); - } - } - - } - } - } - - - edkLensTreeDetailProvider.refresh(); -} - - -function isCircularDependencies(libNode: FileTreeItemLibraryTree) { - let tempNode = libNode.parent; - while (tempNode) { - if (tempNode.label === libNode.label) { - DiagnosticManager.warning( - libNode.uri, 0, EdkDiagnosticCodes.circularDependency, - `Library circular dependency detected`, [vscode.DiagnosticTag.Unnecessary] - ); - libNode.collapsibleState = vscode.TreeItemCollapsibleState.None; - libNode.iconPath = new vscode.ThemeIcon("extensions-refresh"); - libNode.tooltip = "Circular library dependency"; - return true; - } - tempNode = tempNode.parent; - } - return false; -} diff --git a/src/TreeWebview.ts b/src/TreeWebview.ts deleted file mode 100644 index e451528..0000000 --- a/src/TreeWebview.ts +++ /dev/null @@ -1,153 +0,0 @@ - -import * as vscode from 'vscode'; -import { TreeDetailsDataProvider } from './TreeDataProvider'; -import { copyTreeProviderToClipboard, gotoFile, openFileSide } from './utils'; -import { gExtensionContext } from './extension'; -import { getNonce } from './utilities/getNonce'; -import path = require('path'); -import { TreeItem } from './treeElements/TreeItem'; - -//https://bendera.github.io/vscode-webview-elements/ - -export class TreeWebview { - treeProvider: TreeDetailsDataProvider; - private count: number = 0; - contextModule: string; - targetModule: string; - - constructor(treeProvider: TreeDetailsDataProvider, targetModule:string, contextModule:string) { - this.treeProvider = treeProvider; - this.targetModule = targetModule; - this.contextModule = contextModule; - } - - private processTree(currentNode: TreeItem, htmlText: string) { - let children = currentNode.children; - - - let returnHtml =` - { - icons, - label: '${currentNode.label}', - value: '${currentNode.description?.toString().replaceAll("\\",path.sep)}', - open: true, - `; - - if (children.length > 0) { - returnHtml += `subItems: [`; - for (const r of children) { - returnHtml += this.processTree(r, htmlText); - } - returnHtml += `],`; - } - return returnHtml + "},"; - } - - render() { - let htmlTree = ""; - for (const items of this.treeProvider.getChildren()) { - htmlTree += this.processTree(items, ''); - } - - const webviewPanel = vscode.window.createWebviewPanel( - 'vscodeTest', - `Library tree: ${path.basename(this.targetModule)}`, - vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: true, - enableFindWidget: true - } - ); - webviewPanel.webview.onDidReceiveMessage( - async message => { - switch (message.command) { - case 'openFile': - await openFileSide(vscode.Uri.file(message.path), new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0))); - return; - case 'copy': - await copyTreeProviderToClipboard(this.treeProvider); - return; - } - }, - undefined, - gExtensionContext.subscriptions - ); - this.setHtmlContent(webviewPanel.webview, gExtensionContext, htmlTree); - } - - - setHtmlContent(webview: vscode.Webview, extensionContext: vscode.ExtensionContext, reportObject: any) { - const nonce = getNonce(); - - const fileUri = (fp: string) => { - const fragments = fp.split('/'); - return vscode.Uri.file( - path.join(extensionContext.asAbsolutePath(""),...fragments) - ); - }; - - const assetUri = (fp: string) => { - return webview.asWebviewUri(fileUri(fp)); - }; - - let htmlContent = /*html*/ - - ` - - - - - - - - - -
-

Library dependencies of ${this.targetModule} in context of ${this.contextModule} module. -
Recursive dependencies are ommited.

- Copy tree -
- - - - `; - webview.html = htmlContent; - } -} - - - diff --git a/src/buildEdk2.ts b/src/buildEdk2.ts new file mode 100644 index 0000000..cc0f5aa --- /dev/null +++ b/src/buildEdk2.ts @@ -0,0 +1,627 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { gConfigAgent, gDebugLog, gWorkspacePath } from './extension'; +import { readEdkCodeFolderFile, writeEdkCodeFolderFile, deleteEdkCodeFolderFile, getEdkCodeFolderFilePath } from './edk2CodeFolder'; + +let buildFormPanel: vscode.WebviewPanel | undefined; + +const BUILD_CONFIG_FILE = 'edk2_build_configuration.json'; +const GLOBAL_CONFIG_KEY = '__global__'; + +export interface BuildInvocation { + /** Absolute or workspace-relative path to the DSC to build. */ + dscPath?: string; + /** Absolute or workspace-relative path to an INF to build with `-m`. */ + modulePath?: string; +} + +/** + * Global build configuration stored in .edkCode/edk2_build_configuration.json. + * Shared across all module builds. Extendable for future global settings. + */ +interface GlobalBuildConfig { + /** EDK2 workspace root containing edksetup.bat/sh. Empty = use VS Code workspace path. */ + edkSetupPath: string; + /** NASM assembler path prefix. Exported as NASM_PREFIX. */ + nasmPrefix: string; + /** IASL compiler path prefix. Exported as IASL_PREFIX. */ + iaslPrefix: string; + /** Path to EDK2 BaseTools binaries. Exported as EDK_TOOLS_BIN. */ + edkToolsBin: string; + /** Optional WORKSPACE directory override. Empty = auto-compute from platform DSC location. */ + workspaceDir: string; +} + +const DEFAULT_GLOBAL_CONFIG: GlobalBuildConfig = { + edkSetupPath: '', + nasmPrefix: '', + iaslPrefix: '', + edkToolsBin: '', + workspaceDir: '' +}; + +interface DefineEntry { + name: string; + value: string; +} + +interface BuildFormState { + platform: string; + module: string; + arch: string; + target: string; + toolchain: string; + action: string; + skuid: string; + threads: string; + fdfFile: string; + romImage: string; + fvImage: string; + capsuleImage: string; + skipAutogen: boolean; + reParse: boolean; + caseInsensitive: boolean; + warningAsError: boolean; + logFile: string; + silent: boolean; + quiet: boolean; + verbose: boolean; + debug: string; + defines: DefineEntry[]; + reportFile: string; + reportType: string[]; + flag: string; + noCache: boolean; + confDir: string; + checkUsage: boolean; + ignoreSources: boolean; + pcds: string[]; + cmdLen: string; + hash: boolean; + binaryDestination: string; + binarySource: string; + genfdsMultiThread: boolean; + noGenfdsMultiThread: boolean; + disableIncludePathCheck: boolean; + extraArgs: string[]; + packagePaths: string[]; + /** DSC choices to populate the dropdown (not persisted). */ + dscPaths: string[]; +} + +/** Key used to store/load per-module config in the JSON file. */ +function configKey(state: { platform: string; module: string }): string { + return state.module || state.platform || '__default__'; +} + +function loadAllConfig(): Record { + const raw = readEdkCodeFolderFile(BUILD_CONFIG_FILE); + if (!raw) { return {}; } + try { return JSON.parse(raw); } catch { return {}; } +} + +function saveAllConfig(all: Record): void { + writeEdkCodeFolderFile(BUILD_CONFIG_FILE, JSON.stringify(all, null, 2)); +} + +function loadGlobalConfig(): GlobalBuildConfig { + const all = loadAllConfig(); + return { ...DEFAULT_GLOBAL_CONFIG, ...(all[GLOBAL_CONFIG_KEY] ?? {}) }; +} + +function saveGlobalConfig(global: GlobalBuildConfig): void { + const all = loadAllConfig(); + all[GLOBAL_CONFIG_KEY] = global; + saveAllConfig(all); +} + +function loadSavedConfig(key: string): Partial | undefined { + const all = loadAllConfig(); + return all[key] ?? undefined; +} + +function saveConfig(key: string, state: BuildFormState): void { + const all = loadAllConfig(); + // Don't persist the dscPaths list — it's runtime only + const { dscPaths, ...toSave } = state; + all[key] = toSave; + saveAllConfig(all); +} + +function deleteSavedConfig(key: string): void { + const all = loadAllConfig(); + delete all[key]; + // Keep at least global config + if (Object.keys(all).length === 0 || (Object.keys(all).length === 1 && all[GLOBAL_CONFIG_KEY])) { + if (Object.keys(all).length === 0) { + deleteEdkCodeFolderFile(BUILD_CONFIG_FILE); + } else { + saveAllConfig(all); + } + } else { + saveAllConfig(all); + } +} + +/** + * Public entry point for the EDK2 build command. + * Always opens the build configuration form (webview) first, then runs + * the build in a terminal when the user clicks "Build". + */ +export async function buildEdk2Workspace(options?: BuildInvocation) { + const isWindows = process.platform === 'win32'; + + // Load global config (edkSetupPath, nasmPrefix, etc.) + const globalConfig = loadGlobalConfig(); + + // Get DSC paths + const dscPaths = gConfigAgent.getBuildDscPaths(); + if (!dscPaths || dscPaths.length === 0) { + void vscode.window.showErrorMessage('No DSC paths configured. Please configure DSC paths first.'); + return; + } + + // Determine pre-selected DSC + let selectedDsc: string | undefined = options?.dscPath; + if (selectedDsc) { + // Normalize: if dscPath is absolute, find the matching relative entry in dscPaths + const match = dscPaths.find(d => + d === selectedDsc || + path.resolve(gWorkspacePath, d) === path.resolve(selectedDsc!) || + path.normalize(d) === path.normalize(selectedDsc!) + ); + if (match) { + selectedDsc = match; + } + } + if (!selectedDsc) { + if (dscPaths.length === 1) { + selectedDsc = dscPaths[0]; + } else { + const picked = await vscode.window.showQuickPick(dscPaths, { + placeHolder: 'Select DSC file to build', + title: 'EDK2 Build' + }); + if (!picked) { return; } + selectedDsc = picked; + } + } + + // Build defaults from settings + const defaultState: BuildFormState = { + platform: selectedDsc, + module: options?.modulePath ?? '', + arch: gConfigAgent.getBuildArch(), + target: gConfigAgent.getBuildTarget(), + toolchain: gConfigAgent.getBuildToolchain(), + action: '', + skuid: '', + threads: '', + fdfFile: '', + romImage: '', + fvImage: '', + capsuleImage: '', + skipAutogen: false, + reParse: false, + caseInsensitive: false, + warningAsError: false, + logFile: '', + silent: false, + quiet: false, + verbose: false, + debug: '', + defines: Array.from(gConfigAgent.getBuildDefines().entries()).map(([k, v]) => ({ name: k, value: v })), + reportFile: '', + reportType: [], + flag: '', + noCache: false, + confDir: '', + checkUsage: false, + ignoreSources: false, + pcds: [], + cmdLen: '', + hash: false, + binaryDestination: '', + binarySource: '', + genfdsMultiThread: false, + noGenfdsMultiThread: false, + disableIncludePathCheck: false, + extraArgs: gConfigAgent.getBuildExtraArgs().filter(a => a.trim()), + packagePaths: gConfigAgent.getBuildPackagePaths().filter(p => p.trim()), + dscPaths: dscPaths + }; + + // Load saved config if available + const key = configKey(defaultState); + const saved = loadSavedConfig(key); + const initial: BuildFormState = saved + ? { ...defaultState, ...saved, dscPaths } + : defaultState; + + // Open the form (stays open; handles build internally) + showBuildForm(initial, defaultState, globalConfig, isWindows); +} + +/** Validation error with associated field ID for highlighting in the form. */ +interface ValidationError { + field: string; + message: string; +} + +/** Resolve a config path: if relative, resolve against workspace root; if absolute, use as-is. */ +function resolveConfigPath(p: string): string { + if (!p) { return ''; } + if (path.isAbsolute(p)) { return p; } + return path.join(gWorkspacePath, p); +} + +/** Validate global config paths. Returns an array of validation errors (empty if all OK). */ +function validateGlobalConfig(config: GlobalBuildConfig): ValidationError[] { + const errors: ValidationError[] = []; + + // EDK Setup Path: must point to edksetup.bat or edksetup.sh + const rawSetup = config.edkSetupPath.trim(); + if (rawSetup) { + const setupPath = resolveConfigPath(rawSetup); + if (!fs.existsSync(setupPath)) { + errors.push({ field: 'edkSetupPath', message: `EDK2 setup script not found: ${setupPath}` }); + } else if (fs.statSync(setupPath).isDirectory()) { + errors.push({ field: 'edkSetupPath', message: `Expected a file (edksetup.bat/.sh), got a directory: ${setupPath}` }); + } + } else { + // Default: look for edksetup in workspace root + const isWindows = process.platform === 'win32'; + const defaultScript = path.join(gWorkspacePath, isWindows ? 'edksetup.bat' : 'edksetup.sh'); + if (!fs.existsSync(defaultScript)) { + errors.push({ field: 'edkSetupPath', message: `EDK2 setup script not found in workspace: ${defaultScript}` }); + } + } + + // NASM Prefix: must point to nasm executable + const rawNasm = config.nasmPrefix.trim(); + if (rawNasm) { + const nasmPath = resolveConfigPath(rawNasm); + if (!fs.existsSync(nasmPath)) { + errors.push({ field: 'nasmPrefix', message: `NASM executable not found: ${nasmPath}` }); + } else if (fs.statSync(nasmPath).isDirectory()) { + errors.push({ field: 'nasmPrefix', message: `Expected a file (nasm executable), got a directory: ${nasmPath}` }); + } + } + + // IASL Prefix: must point to iasl executable + const rawIasl = config.iaslPrefix.trim(); + if (rawIasl) { + const iaslPath = resolveConfigPath(rawIasl); + if (!fs.existsSync(iaslPath)) { + errors.push({ field: 'iaslPrefix', message: `IASL executable not found: ${iaslPath}` }); + } else if (fs.statSync(iaslPath).isDirectory()) { + errors.push({ field: 'iaslPrefix', message: `Expected a file (iasl executable), got a directory: ${iaslPath}` }); + } + } + + // EDK Tools Bin: must be an existing directory + const rawToolsBin = config.edkToolsBin.trim(); + if (rawToolsBin) { + const toolsBinPath = resolveConfigPath(rawToolsBin); + if (!fs.existsSync(toolsBinPath)) { + errors.push({ field: 'edkToolsBin', message: `EDK_TOOLS_BIN directory not found: ${toolsBinPath}` }); + } else if (!fs.statSync(toolsBinPath).isDirectory()) { + errors.push({ field: 'edkToolsBin', message: `Expected a directory, got a file: ${toolsBinPath}` }); + } + } + + return errors; +} + +/** Internal: assemble the command line from form state and execute it in a terminal. */ +async function runBuild(state: BuildFormState, edkRoot: string, isWindows: boolean, globalConfig: GlobalBuildConfig) { + // Derive NASM_PREFIX: directory of the nasm executable, ending with separator + const rawNasm = (globalConfig.nasmPrefix || '').trim(); + const nasmPrefix = rawNasm ? path.dirname(resolveConfigPath(rawNasm)) + path.sep : ''; + + // Derive IASL_PREFIX: directory of the iasl executable, ending with separator + const rawIasl = (globalConfig.iaslPrefix || '').trim(); + const iaslPrefix = rawIasl ? path.dirname(resolveConfigPath(rawIasl)) + path.sep : ''; + + // EDK_TOOLS_BIN: resolve as-is (already validated as a directory) + const edkToolsBin = (globalConfig.edkToolsBin || '').trim() + ? resolveConfigPath(globalConfig.edkToolsBin.trim()) : ''; + + // Derive setup script name and edkRoot directory from edkSetupPath + const rawSetup = (globalConfig.edkSetupPath || '').trim(); + let setupScript: string; + if (rawSetup) { + const resolvedSetup = resolveConfigPath(rawSetup); + edkRoot = path.dirname(resolvedSetup); + setupScript = path.basename(resolvedSetup); + } else { + setupScript = isWindows ? 'edksetup.bat' : 'edksetup.sh'; + } + + // Package paths + const packagePaths = (state.packagePaths || []).map(s => s.trim()).filter(s => s.length > 0); + + /** + * Convert an absolute INF/DSC path into a path that EDK2 `build` can resolve. + */ + const toEdkRelative = (p: string): string => { + if (!p) { return p; } + if (!path.isAbsolute(p)) { return p; } + const roots = [edkRoot, ...packagePaths]; + for (const root of roots) { + if (!root) { continue; } + const rel = path.relative(root, p); + if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) { + return rel; + } + } + return p; + }; + + const platformArg = toEdkRelative(state.platform); + const moduleArg = toEdkRelative(state.module); + + // Compute the effective workspace root (WORKSPACE env variable / CWD for edksetup). + // If the user explicitly set workspaceDir in global config, use that. + // Otherwise, auto-compute from platform DSC location. + let workspaceRoot = edkRoot; + if (globalConfig.workspaceDir) { + workspaceRoot = path.isAbsolute(globalConfig.workspaceDir) + ? globalConfig.workspaceDir + : path.resolve(gWorkspacePath, globalConfig.workspaceDir); + } else if (platformArg && !path.isAbsolute(platformArg)) { + const candidates = new Set(); + candidates.add(edkRoot); + for (const p of packagePaths) { + if (path.isAbsolute(p)) { + candidates.add(p); + candidates.add(path.dirname(p)); + } + } + for (const candidate of candidates) { + if (fs.existsSync(path.join(candidate, platformArg))) { + workspaceRoot = candidate; + break; + } + } + } + + // If workspaceRoot differs from edkRoot, use full path for the setup script + const setupScriptCall = (workspaceRoot !== edkRoot) + ? path.join(edkRoot, setupScript) + : setupScript; + + // Compose build args — use absolute paths for -p and -m + // Note: platformArg/moduleArg are relative to gWorkspacePath (VS Code workspace root), + // not to workspaceRoot (EDK2 WORKSPACE), so resolve against gWorkspacePath. + const args: string[] = []; + if (platformArg) { + const absPlat = path.isAbsolute(platformArg) ? platformArg : path.resolve(gWorkspacePath, platformArg); + args.push(`-p ${absPlat}`); + } + if (moduleArg) { + const absMod = path.isAbsolute(moduleArg) ? moduleArg : path.resolve(gWorkspacePath, moduleArg); + args.push(`-m ${absMod}`); + } + if (state.arch) { + // Support multiple architectures separated by space or comma (e.g. "IA32 X64") + const archs = state.arch.split(/[\s,]+/).filter(Boolean); + for (const a of archs) { args.push(`-a ${a}`); } + } + if (state.target) { args.push(`-b ${state.target}`); } + if (state.toolchain) { args.push(`-t ${state.toolchain}`); } + if (state.skuid) { args.push(`-x ${state.skuid}`); } + if (state.threads) { args.push(`-n ${state.threads}`); } + if (state.fdfFile) { args.push(`-f ${state.fdfFile}`); } + if (state.romImage) { args.push(`-r ${state.romImage}`); } + if (state.fvImage) { args.push(`-i ${state.fvImage}`); } + if (state.capsuleImage) { args.push(`-C ${state.capsuleImage}`); } + if (state.skipAutogen) { args.push(`-u`); } + if (state.reParse) { args.push(`-e`); } + if (state.caseInsensitive) { args.push(`-c`); } + if (state.warningAsError) { args.push(`-w`); } + if (state.logFile) { args.push(`-j ${state.logFile}`); } + if (state.silent) { args.push(`-s`); } + if (state.quiet) { args.push(`-q`); } + if (state.verbose) { args.push(`-v`); } + if (state.debug) { args.push(`-d ${state.debug}`); } + + // Defines: array of {name, value} + for (const def of (state.defines || [])) { + if (def.name) { + const val = def.value.includes(' ') ? `"${def.value}"` : def.value; + args.push(`-D ${def.name}=${val}`); + } + } + + if (state.reportFile) { args.push(`-y ${state.reportFile}`); } + for (const rt of state.reportType || []) { + if (rt) { args.push(`-Y ${rt}`); } + } + if (state.flag) { args.push(`-F ${state.flag}`); } + if (state.noCache) { args.push(`-N`); } + if (state.confDir) { args.push(`--conf=${state.confDir}`); } + if (state.checkUsage) { args.push(`--check-usage`); } + if (state.ignoreSources) { args.push(`--ignore-sources`); } + + // PCDs: array of strings + for (const pcd of (state.pcds || [])) { + const trimmed = pcd.trim(); + if (trimmed) { args.push(`--pcd=${trimmed}`); } + } + + if (state.cmdLen) { args.push(`-l ${state.cmdLen}`); } + if (state.hash) { args.push(`--hash`); } + if (state.binaryDestination) { args.push(`--binary-destination=${state.binaryDestination}`); } + if (state.binarySource) { args.push(`--binary-source=${state.binarySource}`); } + if (state.genfdsMultiThread) { args.push(`--genfds-multi-thread`); } + if (state.noGenfdsMultiThread) { args.push(`--no-genfds-multi-thread`); } + if (state.disableIncludePathCheck) { args.push(`--disable-include-path-check`); } + + // Extra args: array of strings + for (const a of (state.extraArgs || [])) { + const trimmed = a.trim(); + if (trimmed) { args.push(trimmed); } + } + + // Trailing positional action keyword + if (state.action) { args.push(state.action); } + + const buildArgsStr = args.join(' '); + + let cmd: string; + if (isWindows) { + // On Windows, `set "VAR=value"` already preserves spaces — no inner quoting needed + const pkgPath = packagePaths.join(';'); + const parts: string[] = []; + parts.push(`set "WORKSPACE=${workspaceRoot}"`); + parts.push(`set "PACKAGES_PATH=${pkgPath}"`); + if (nasmPrefix) { parts.push(`set "NASM_PREFIX=${nasmPrefix}"`); } + if (iaslPrefix) { parts.push(`set "IASL_PREFIX=${iaslPrefix}"`); } + if (edkToolsBin) { parts.push(`set "EDK_TOOLS_BIN=${edkToolsBin}"`); } + parts.push(`call ${setupScriptCall}`); + parts.push(`build ${buildArgsStr}`); + cmd = parts.join(' && '); + } else { + // On Linux, the outer double-quotes in export protect spaces + const pkgPath = packagePaths.join(':'); + const parts: string[] = []; + parts.push(`export WORKSPACE="${workspaceRoot}"`); + parts.push(`export PACKAGES_PATH="${pkgPath}"`); + if (nasmPrefix) { parts.push(`export NASM_PREFIX="${nasmPrefix}"`); } + if (iaslPrefix) { parts.push(`export IASL_PREFIX="${iaslPrefix}"`); } + if (edkToolsBin) { parts.push(`export EDK_TOOLS_BIN="${edkToolsBin}"`); } + parts.push(`. ${setupScriptCall}`); + parts.push(`build ${buildArgsStr}`); + cmd = parts.join(' && '); + } + + gDebugLog.info(`EDK2 Build command: ${cmd}`); + + // Use a VS Code Task so we get proper process lifecycle tracking + const shellExec = isWindows + ? new vscode.ShellExecution(cmd, { cwd: workspaceRoot }) + : new vscode.ShellExecution(cmd, { cwd: workspaceRoot }); + + const taskDef: vscode.TaskDefinition = { type: 'edk2build' }; + const task = new vscode.Task( + taskDef, + vscode.TaskScope.Workspace, + 'EDK2 Build', + 'edk2code', + shellExec + ); + task.presentationOptions = { + reveal: vscode.TaskRevealKind.Always, + panel: vscode.TaskPanelKind.Shared, + clear: true + }; + + const execution = await vscode.tasks.executeTask(task); + + // Wait for the task process to end + return new Promise((resolve) => { + const disposable = vscode.tasks.onDidEndTaskProcess((e) => { + if (e.execution === execution) { + disposable.dispose(); + resolve(); + } + }); + }); +} + +// ──────────────────────────────────────────────────────────────────────────── +// Build configuration form (webview) +// ──────────────────────────────────────────────────────────────────────────── + +function getFormHtml(extensionPath: string): string { + const htmlPath = path.join(extensionPath, 'static', 'buildForm.html'); + return fs.readFileSync(htmlPath, 'utf8'); +} + +function showBuildForm(initial: BuildFormState, defaults: BuildFormState, globalConfig: GlobalBuildConfig, isWindows: boolean): void { + if (buildFormPanel) { + // If already open, just reveal and re-init + buildFormPanel.reveal(); + buildFormPanel.webview.postMessage({ command: 'init', state: initial, globalConfig, workspacePath: gWorkspacePath }); + return; + } + + // Get extension path for loading HTML + const ext = vscode.extensions.getExtension('intel-corporation.edk2code'); + const extensionPath = ext?.extensionPath ?? path.join(__dirname, '..'); + + const panel = vscode.window.createWebviewPanel( + 'edk2code.buildForm', + 'EDK2 Build Configuration', + vscode.ViewColumn.Active, + { enableScripts: true, retainContextWhenHidden: true, enableFindWidget: true } + ); + buildFormPanel = panel; + + panel.onDidDispose(() => { + buildFormPanel = undefined; + }); + + panel.webview.onDidReceiveMessage(async (msg: any) => { + if (!msg) { return; } + if (msg.command === 'ready') { + // Send initial state + global config to webview + panel.webview.postMessage({ command: 'init', state: initial, globalConfig, workspacePath: gWorkspacePath }); + } else if (msg.command === 'build') { + const state = msg.state as BuildFormState; + const formGlobal = msg.globalConfig as GlobalBuildConfig; + + // Save global config + if (formGlobal) { + saveGlobalConfig(formGlobal); + } + + // Persist the submitted state + saveConfig(configKey(state), state); + + // Re-read global config + const updatedGlobal = loadGlobalConfig(); + + // Validate + const errors = validateGlobalConfig(updatedGlobal); + if (errors.length > 0) { + // Send validation errors to webview for highlighting + panel.webview.postMessage({ command: 'validationErrors', errors }); + return; + } + + // Disable form during build + panel.webview.postMessage({ command: 'buildStarted' }); + + // Derive edkRoot from setup script path (runBuild will also do this, but we need it for cwd) + const rawSetup = updatedGlobal.edkSetupPath.trim(); + let finalEdkRoot: string; + if (rawSetup) { + finalEdkRoot = path.dirname(resolveConfigPath(rawSetup)); + } else { + finalEdkRoot = process.env['WORKSPACE'] || gWorkspacePath; + } + + await runBuild(state, finalEdkRoot, isWindows, updatedGlobal); + + // Re-enable form after build command is sent + panel.webview.postMessage({ command: 'buildFinished' }); + } else if (msg.command === 'cancel') { + panel.dispose(); + } else if (msg.command === 'openConfig') { + const filePath = getEdkCodeFolderFilePath(BUILD_CONFIG_FILE); + const uri = vscode.Uri.file(filePath); + vscode.commands.executeCommand('vscode.open', uri); + } else if (msg.command === 'reset') { + // Delete saved config and re-initialize with defaults + deleteSavedConfig(configKey(initial)); + panel.webview.postMessage({ command: 'init', state: defaults, globalConfig: DEFAULT_GLOBAL_CONFIG }); + } + }); + + panel.webview.html = getFormHtml(extensionPath); +} + + diff --git a/src/compileCommands.ts b/src/compileCommands.ts index f57e6d0..ee0cb9e 100644 --- a/src/compileCommands.ts +++ b/src/compileCommands.ts @@ -1,7 +1,6 @@ import path = require("path"); -import { gWorkspacePath } from "./extension"; import * as fs from 'fs'; import { readEdkCodeFolderFile, writeEdkCodeFolderFile } from "./edk2CodeFolder"; import { normalizePath } from "./utils"; @@ -41,8 +40,8 @@ export class CompileCommandsEntry{ getDefines(): string[]{ const defines: string[] = []; - // Combine patterns for gcc (-D) and msbuild (/D) - const match = this.command.match(/(-D\s*[^ ]+)|(\/D\s*[^ ]+)/g); + // Match -D or /D flags preceded by whitespace (to avoid matching inside paths) + const match = this.command.match(/(?<=\s)-D\s*[^ ]+|(?<=\s)\/D\s*[^ ]+/g); if (match) { match.forEach((define) => { // Remove leading -D or /D and trim whitespace diff --git a/src/compileFile.ts b/src/compileFile.ts new file mode 100644 index 0000000..2c3e5d1 --- /dev/null +++ b/src/compileFile.ts @@ -0,0 +1,97 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import { gCompileCommands } from './extension'; +import { CompileCommandsEntry } from './compileCommands'; + +function getCompilerName(command: string): string { + const match = command.match(/^"?([^"\s]+)"?/); + return match ? path.basename(match[1]) : 'unknown'; +} + +function getOutputFile(command: string): string { + const match = command.match(/-o\s+(\S+)/); + return match ? match[1] : 'N/A'; +} + +function getFlags(command: string): string[] { + const flags: string[] = []; + // Match flags that are not -D, -I, -o, or the compiler/source file + const match = command.match(/\s(-[^DIo]\S*)/g); + if (match) { + match.forEach(f => flags.push(f.trim())); + } + return flags; +} + +function buildReport(entry: CompileCommandsEntry): string { + const fileName = path.basename(entry.file); + const compiler = getCompilerName(entry.command); + const output = getOutputFile(entry.command); + const defines = entry.getDefines(); + const includes = entry.getIncludePaths(); + + const lines: string[] = []; + lines.push('---------------------------------------------------------------'); + lines.push(`| EDK2 Compile: ${fileName}`); + lines.push('---------------------------------------------------------------'); + lines.push(`| Compiler: ${compiler}`); + lines.push(`| Source: ${entry.file}`); + lines.push(`| Output: ${output}`); + lines.push(`| Directory: ${entry.directory}`); + lines.push('---------------------------------------------------------------'); + lines.push(`| Defines (${defines.length}):`); + defines.forEach(d => lines.push(`| -D ${d}`)); + lines.push('---------------------------------------------------------------'); + lines.push(`| Include Paths (${includes.length}):`); + includes.forEach(i => lines.push(`| ${i}`)); + lines.push('---------------------------------------------------------------'); + return lines.join('\n'); +} + +export async function compileCFile(fileUri?: vscode.Uri) { + let filePath: string; + if (fileUri) { + filePath = fileUri.fsPath; + } else { + const editor = vscode.window.activeTextEditor; + if (!editor) { + void vscode.window.showErrorMessage('No active editor found.'); + return; + } + filePath = editor.document.uri.fsPath; + } + + gCompileCommands.load(); + const entry = gCompileCommands.getCompileCommandForFile(filePath); + if (!entry) { + void vscode.window.showErrorMessage(`No compile command found for ${path.basename(filePath)}`); + return; + } + + const report = buildReport(entry); + + // Write report to a separate text file to avoid shell escaping issues + const isWindows = os.platform() === 'win32'; + const reportPath = path.join(os.tmpdir(), 'edk2_compile_report.txt'); + fs.writeFileSync(reportPath, report); + + // Write command to a temp script to avoid terminal line-length limits + const scriptExt = isWindows ? '.bat' : '.sh'; + const scriptPath = path.join(os.tmpdir(), `edk2_compile${scriptExt}`); + const fileName = path.basename(filePath); + + let scriptContent: string; + if (isWindows) { + scriptContent = `@echo off\ntype "${reportPath}"\necho.\ncd /d "${entry.directory}"\n${entry.command}\nif %ERRORLEVEL% EQU 0 (echo Compilation successful: ${fileName}) else (echo Compilation FAILED: ${fileName})`; + } else { + scriptContent = `#!/bin/bash\ncat "${reportPath}"\necho ""\ncd "${entry.directory}"\n${entry.command}\nif [ $? -eq 0 ]; then echo "Compilation successful: ${fileName}"; else echo "Compilation FAILED: ${fileName}"; fi`; + } + fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 }); + + const terminal = vscode.window.createTerminal({ name: `Compile: ${fileName}`, cwd: entry.directory }); + terminal.show(); + const runCmd = isWindows ? `"${scriptPath}"` : `bash "${scriptPath}"`; + terminal.sendText(runCmd); +} diff --git a/src/configuration.ts b/src/configuration.ts index c7c59d6..365436c 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -7,12 +7,14 @@ import { askReloadFiles } from './ui/messages'; import { readFile } from './utils'; import { SettingsPanel } from './settings/settingsPanel'; import { getEdkCodeFolderFilePath, existsEdkCodeFolderFile, writeEdkCodeFolderFile } from './edk2CodeFolder'; +import { TernarySearchTree } from './ternarySearchTree'; export interface WorkspaceConfig { packagePaths:string[]; dscPaths:string[]; buildDefines:string[]; + workspaceTreeFilters?: number[]; } export interface WorkspaceConfigErrors{ @@ -39,6 +41,21 @@ export class ConfigAgent { private workspaceConfig:WorkspaceConfig; private settingsFileName: string = "edk2_workspace_properties.json"; + /** + * Temporary T-tree file index built when the workspace is being processed. + * Used by PathFind as a fast lookup while packagePaths are not yet populated. + * Set to `null` when the processing is finished. + */ + private _fileIndex: TernarySearchTree | null = null; + + /** + * Tracks whether the last workspace processing completed successfully. + * When `false`, the next `loadConfig` / `proccessWorkspace` will build + * a temporary T-tree so PathFind can resolve files without relying on + * the (still-empty) packagePaths. + */ + private _workspaceProcessComplete: boolean = false; + public constructor() { this.vscodeSettings = vscode.workspace.getConfiguration('edk2code'); this.workspaceConfig = this.readWpConfig(); @@ -46,7 +63,7 @@ export class ConfigAgent { } - reloadVscodeSettings(){ + reloadVscodeSettings(event?: vscode.ConfigurationChangeEvent){ this.vscodeSettings = vscode.workspace.getConfiguration('edk2code'); } @@ -72,7 +89,7 @@ export class ConfigAgent { private readWpConfig(){ let settingsPath = getEdkCodeFolderFilePath(this.settingsFileName); - gDebugLog.verbose(`Loading configuration from ${settingsPath}`); + gDebugLog.trace(`Loading configuration from ${settingsPath}`); if(existsEdkCodeFolderFile(this.settingsFileName)){ try { @@ -100,6 +117,50 @@ export class ConfigAgent { clearWpConfiguration(){ this.workspaceConfig = this.getCleanWpConfig(); + // Mark workspace as not-yet-processed so the next loadConfig + // will build a temporary T-tree for fast file lookups. + this._workspaceProcessComplete = false; + } + + /** + * Returns `true` if the previous workspace processing completed + * successfully (i.e. packagePaths are fully populated). + */ + isWorkspaceProcessComplete(): boolean { + return this._workspaceProcessComplete; + } + + /** + * Mark the workspace processing as complete and dispose of the + * temporary file index so that normal `findFiles` is used again. + */ + setWorkspaceProcessComplete(): void { + this._workspaceProcessComplete = true; + if (this._fileIndex) { + this._fileIndex.dispose(); + this._fileIndex = null; + } + } + + /** + * Build the temporary T-tree file index if the workspace has not + * yet been fully processed (packagePaths not populated). + * This should be called at the beginning of workspace processing. + */ + async buildFileIndexIfNeeded(): Promise { + if (!this._workspaceProcessComplete) { + this._fileIndex = new TernarySearchTree(); + await this._fileIndex.buildFromWorkspace(); + } + } + + /** + * Returns the temporary file index if one is active, or `null` + * when the workspace is fully processed and findFiles should be + * used instead. + */ + getFileIndex(): TernarySearchTree | null { + return this._fileIndex; } initConfigWatcher(){ @@ -191,7 +252,8 @@ export class ConfigAgent { let toSave: string[] = []; for (const [key, value] of defines.entries()) { - toSave.push(`${key.trim()}=${value.trim()}`); + // Normalize double backslashes to single to avoid over-escaping in JSON + toSave.push(`${key.trim()}=${value.trim().replace(/\\\\/g, '\\')}`); } this.workspaceConfig.buildDefines = toSave; this.writeWorkspaceConfig(this.workspaceConfig); @@ -206,7 +268,8 @@ export class ConfigAgent { toSave.push(def); } } - toSave.push(`${name}=${value.trim()}`); + // Normalize double backslashes to single to avoid over-escaping in JSON + toSave.push(`${name}=${value.trim().replace(/\\\\/g, '\\')}`); this.workspaceConfig.buildDefines = toSave; this.writeWorkspaceConfig(this.workspaceConfig); } @@ -233,7 +296,11 @@ export class ConfigAgent { let paths = this.workspaceConfig.packagePaths; let retPaths = []; for (const p of paths) { - retPaths.push(path.join(gWorkspacePath, p)); + if (path.isAbsolute(p)) { + retPaths.push(p); + } else { + retPaths.push(path.join(gWorkspacePath, p)); + } } return retPaths; } @@ -243,6 +310,10 @@ export class ConfigAgent { if(this.workspaceConfig.packagePaths.includes(path)){ return; } + // EDK2 build system does not allow spaces in PACKAGES_PATH entries + if(path.includes(' ')){ + return; + } this.workspaceConfig.packagePaths.push(path); this.writeWorkspaceConfig(this.workspaceConfig); } @@ -257,6 +328,15 @@ export class ConfigAgent { return this.workspaceConfig.dscPaths; } + getWorkspaceTreeFilters(): number[] | undefined { + return this.workspaceConfig.workspaceTreeFilters; + } + + setWorkspaceTreeFilters(filters: number[]): void { + this.workspaceConfig.workspaceTreeFilters = filters; + this.writeWorkspaceConfig(this.workspaceConfig); + } + getIsGenIgnoreFile() { return this.get("generateIgnoreFile"); } @@ -277,10 +357,30 @@ export class ConfigAgent { return this.get("extraIgnorePatterns"); } + getUseCscope() { + return this.get("useCscope"); + } + getCscopeOverwritePath() { return (this.get("cscopeOverwritePath")).trim(); } + getBuildToolchain(): string { + return process.platform === 'win32' ? "VS2019" : "GCC"; + } + + getBuildArch(): string { + return this.get("buildArch") || "X64"; + } + + getBuildTarget(): string { + return this.get("buildTarget") || "DEBUG"; + } + + getBuildExtraArgs(): string[] { + return this.get("buildExtraArgs") || []; + } + getIsGenGuidXrefFile() { return this.get("generateGuidXref"); } diff --git a/src/contextState/cmds.ts b/src/contextState/cmds.ts index 86c3f4f..74923e4 100644 --- a/src/contextState/cmds.ts +++ b/src/contextState/cmds.ts @@ -4,136 +4,324 @@ import { rgSearch } from "../rg"; import { delay, getCurrentWord, gotoFile, isWorkspacePath, listFilesRecursive, openTextDocument, pathCompare, profileEnd, profileStart, readLines, toPosix } from "../utils"; import path = require("path"); import * as fs from 'fs'; -import { edkLensTreeDetailProvider, edkLensTreeDetailView, gConfigAgent, gCscope, gDebugLog, gEdkWorkspaces, gExtensionContext, gMapFileManager, gPathFind, gWorkspacePath } from "../extension"; +import { edkWorkspaceTreeProvider, edkWorkspaceTreeView, gConfigAgent, gCscope, gDebugLog, gEdkWorkspaces, gExtensionContext, gMapFileManager, gPathFind, gWorkspacePath } from "../extension"; import { glob } from "fast-glob"; import { BuildFolder } from "../Languages/buildFolder"; import { EdkWorkspace, InfDsc } from "../index/edkWorkspace"; -import { FileTreeItem, FileTreeItemLibraryTree, openLibraryNode, SectionTreeItem } from "../TreeDataProvider"; -import { ParserFactory, getParser } from "../edkParser/parserFactory"; +import { getParser, getParserForDocument } from "../edkParser/parserFactory"; import { Edk2SymbolType } from "../symbols/symbolsType"; import * as edkStatusBar from '../statusBar'; import { SettingsPanel } from "../settings/settingsPanel"; -import { InfParser } from "../edkParser/infParser"; -import { EdkSymbolInfLibrary } from "../symbols/infSymbols"; -import { debuglog } from "util"; import { deleteEdkCodeFolder, existsEdkCodeFolderFile } from "../edk2CodeFolder"; -import { EdkInfNode, EdkInfNodeLibrary } from "../treeElements/Library"; -import { TreeItem } from "../treeElements/TreeItem"; -import { EdkModule, ModuleReport } from "../moduleReport"; import { infoMissingCompileInfo } from "../ui/messages"; import { checkCppConfiguration } from "../cppProviders/cppUtils"; +import { buildEdk2Workspace as buildEdk2WorkspaceImpl } from "../buildEdk2"; +import { DocumentSymbolItem, WorkspaceRootItem, WorkspaceTreeNode } from "../workspaceTree/WorkspaceTreeProvider"; - export async function rebuildIndexDatabase() { + export async function buildEdk2Workspace() { + await buildEdk2WorkspaceImpl(); + } + + /** + * Build command launched from the workspace tree. + * - On a WorkspaceRootItem: builds the whole DSC. + * - On a DocumentSymbolItem (library/module): builds just that module via `-m`. + */ + export async function buildFromTree(node: WorkspaceTreeNode | undefined) { + if (!node) { + await buildEdk2WorkspaceImpl(); + return; + } + // Keep the node selected so the user knows which module will be built + edkWorkspaceTreeView.reveal(node, { select: true, focus: false }); + if (node instanceof WorkspaceRootItem) { + await buildEdk2WorkspaceImpl({ dscPath: node.workspace.mainDsc.fsPath }); + return; + } + if (node instanceof DocumentSymbolItem) { + // Walk up parent chain to find the owning WorkspaceRootItem (DSC). + let cur: WorkspaceRootItem | DocumentSymbolItem | undefined = node.parent; + while (cur && !(cur instanceof WorkspaceRootItem)) { + cur = (cur as DocumentSymbolItem).parent; + } + const dscPath = cur instanceof WorkspaceRootItem ? cur.workspace.mainDsc.fsPath : undefined; + + // Extract the INF path directly from the DSC text line. + // textLine already resolves parser-level defines; we also call + // gEdkWorkspaces.replaceDefines() for workspace-level variables + // like $(SOME_VARIABLE) that might still be present. + let modulePath: string | undefined; + try { + const sym: any = node.symbol; + + if (sym.type === Edk2SymbolType.dscLibraryDefinition) { + void vscode.window.showWarningMessage( + 'Libraries cannot be built standalone with `build -m`. Only components (modules) listed in [Components] can be built individually.' + ); + return; + } + + const textLine: string = sym.textLine || ''; + if (sym.type === Edk2SymbolType.dscModuleDefinition) { + // Format: "Path/To/Module.inf" possibly followed by " { ... }" + const rawPath = textLine.replace(/\s*\{.*/, '').trim(); + if (rawPath) { + modulePath = await gEdkWorkspaces.replaceDefines(node.fileUri, rawPath); + } + } + } catch { + // ignore + } + if (!modulePath) { + void vscode.window.showErrorMessage('Could not resolve INF path for selected node.'); + return; + } + await buildEdk2WorkspaceImpl({ dscPath, modulePath }); + return; + } + // Unknown node type – fall back to generic build. + await buildEdk2WorkspaceImpl(); + } - gDebugLog.verbose("rebuildIndexDatabase()"); + let discoveredBuildFolders: string[] = []; + let buildFolderScanTimer: NodeJS.Timeout | undefined; + let scanInProgressPromise: Promise | undefined; + + /** + * Searches for BuildOptions files in the given search path. + * Returns an array of directory paths that contain BuildOptions files. + */ + export async function findBuildOptionsFolders(searchPath: string): Promise { + let lookPath = toPosix(path.join(searchPath, "**", "BuildOptions")); + let buildInfoFolders = await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Looking for compile info", + cancellable: true + }, async (progress, reject) => { + return glob(lookPath); + }); - // Pick build folder - let buildPath = await vscode.window.showOpenDialog({ - defaultUri: vscode.Uri.file(gWorkspacePath), canSelectFiles: false, canSelectFolders: true, canSelectMany: false, - filters: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'BuildFolder': ['Build'], - }, - title: "Select EDK build folder" + return buildInfoFolders.map((x) => { + return path.parse(String(x)).dir; }); + } - // If build folder picked - if (buildPath) { - // Check if its part of workspace - if (isWorkspacePath(buildPath[0].fsPath)) { - let buildInfoFolders: any[] = []; - let configPath = buildPath[0].fsPath; - - // Look for BuildOptions file in selected buildPath - if (configPath !== undefined) { - let lookPath = toPosix(path.join(configPath, "**", "BuildOptions")); - buildInfoFolders = await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: "Looking for compile info", - cancellable: true - }, async (progress, reject) => { - return glob(lookPath); - }); - } + /** + * Periodically scans the workspace for BuildOptions folders. + * When found, sets a context key so the welcome view can show the discovery action. + */ + export function startBuildFolderScan(intervalMs: number = 600000) { + stopBuildFolderScan(); + // Run immediately once + void scanWorkspaceForBuildFolders(); + buildFolderScanTimer = setInterval(() => { + void scanWorkspaceForBuildFolders(); + }, intervalMs); + } - // Build Options not found this is not a build folder - if (buildInfoFolders.length === 0) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - vscode.window.showErrorMessage(`Build data was not found in selected folder`); - return; - } + export function stopBuildFolderScan() { + if (buildFolderScanTimer) { + clearInterval(buildFolderScanTimer); + buildFolderScanTimer = undefined; + } + } - buildInfoFolders = buildInfoFolders.map((x) => { - return path.parse(String(x)).dir; - }); + async function scanWorkspaceForBuildFolders() { + if (scanInProgressPromise) { return scanInProgressPromise; } + scanInProgressPromise = doScanWorkspace(); + try { + await scanInProgressPromise; + } finally { + scanInProgressPromise = undefined; + } + } - // Show options for builds - var options: vscode.QuickPickItem[] = []; - for (const foundPath of buildInfoFolders) { - let splitPath = foundPath.split(path.posix.sep); + async function doScanWorkspace() { + try { + let lookPath = toPosix(path.join(gWorkspacePath, "**", "BuildOptions")); + let results = await glob(lookPath); + let folders = results.map((x) => path.parse(String(x)).dir); - options.push({ - label: splitPath[splitPath.length - 2], - description: "", - detail: foundPath, - }); - } + if (folders.length > 0) { + discoveredBuildFolders = folders; + await vscode.commands.executeCommand('setContext', 'edk2code.buildFoldersFound', true); + } else { + discoveredBuildFolders = []; + await vscode.commands.executeCommand('setContext', 'edk2code.buildFoldersFound', false); + } + } catch (error) { + gDebugLog.error(`scanWorkspaceForBuildFolders: ${error}`); + } + } + + /** + * Manually triggers discovery of build folders in the workspace. + * Shows a discovering state in the view while scanning. + */ + export async function discoverBuildFolders() { + await vscode.commands.executeCommand('setContext', 'edk2code.isDiscovering', true); + try { + await scanWorkspaceForBuildFolders(); + } finally { + await vscode.commands.executeCommand('setContext', 'edk2code.isDiscovering', false); + } + } + /** + * Called when user clicks "Use discovered folders" in the welcome view. + * Shows the discovered build folders and lets the user pick which ones to use. + */ + export async function useDiscoveredBuildFolders() { + if (discoveredBuildFolders.length === 0) { + void vscode.window.showInformationMessage("No build folders discovered yet."); + return; + } - let selectedOptions = await vscode.window.showQuickPick(options, { title: "Select build", matchOnDescription: true, matchOnDetail: true , canPickMany:true}); - if (selectedOptions === undefined) { return; } + var options: vscode.QuickPickItem[] = []; + for (const foundPath of discoveredBuildFolders) { + let splitPath = foundPath.split(path.posix.sep); + options.push({ + label: splitPath[splitPath.length - 2], + description: "", + detail: foundPath, + }); + } - let selectedFolders:string[] = []; + let selectedOptions = await vscode.window.showQuickPick(options, { title: "Select build folders to use", matchOnDescription: true, matchOnDetail: true, canPickMany: true }); + if (selectedOptions === undefined) { return; } - for (const op of selectedOptions) { - if(op.detail){ - selectedFolders.push(op.detail); - } - } + let selectedFolders: string[] = []; + for (const op of selectedOptions) { + if (op.detail) { + selectedFolders.push(op.detail); + } + } + if (selectedFolders.length > 0) { + await vscode.commands.executeCommand('setContext', 'edk2code.isLoading', true); + await rebuildIndexDatabase(selectedFolders); + await vscode.commands.executeCommand('setContext', 'edk2code.isLoading', false); + } + } - gDebugLog.verbose("Loading from build"); - - let buildFolder = new BuildFolder(selectedFolders); - let buildData = await buildFolder.getBuildOptions(); - if (buildData) { - gDebugLog.verbose("Delete workspace files"); - // await gEdkDatabase.clearWorkspace(); - deleteEdkCodeFolder(); - gConfigAgent.clearWpConfiguration(); - await gConfigAgent.setBuildDefines(buildData.buildDefines); - await gConfigAgent.setBuildDscPaths(buildData.dscFiles); - buildFolder.copyCompileInfoToRoot(); - - // If cscope.file is not generated, then calculate files based on dsc parsing - if(existsEdkCodeFolderFile(".missing")){ - infoMissingCompileInfo(); - await reloadSymbols(); - }else{ - await checkCppConfiguration(); - await gCscope.reload(); - } + export async function rebuildIndexDatabase(preselectedFolders?: string[]){ + gDebugLog.trace("Rebuilding index database"); + + let selectedFolders: string[] = []; + + if (preselectedFolders && preselectedFolders.length > 0) { + // Use pre-discovered folders directly + selectedFolders = preselectedFolders; + } else { + // Pick build folder interactively + let buildPath = await vscode.window.showOpenDialog({ + defaultUri: vscode.Uri.file(gWorkspacePath), canSelectFiles: false, canSelectFolders: true, canSelectMany: false, + filters: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'BuildFolder': ['Build'], + }, + title: "Select EDK build folder" + }); - // Generate .ignore if setting is set and .ignore doesnt exists - if (gConfigAgent.getIsGenIgnoreFile()) { - await genIgnoreFile(); + // If build folder picked + if (buildPath) { + // Check if its part of workspace + if (isWorkspacePath(buildPath[0].fsPath)) { + let configPath = buildPath[0].fsPath; + + let buildInfoFolders = await findBuildOptionsFolders(configPath); + + // Build Options not found this is not a build folder + if (buildInfoFolders.length === 0) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + vscode.window.showErrorMessage(`Build data was not found in selected folder`); + return; } - await buildFolder.copyMapFilesList(); - gMapFileManager.load(); + // Show options for builds + var options: vscode.QuickPickItem[] = []; + for (const foundPath of buildInfoFolders) { + let splitPath = foundPath.split(path.posix.sep); + + options.push({ + label: splitPath[splitPath.length - 2], + description: "", + detail: foundPath, + }); + } - void vscode.window.showInformationMessage("Build data loaded"); + let selectedOptions = await vscode.window.showQuickPick(options, { title: "Select build", matchOnDescription: true, matchOnDetail: true, canPickMany: true }); + if (selectedOptions === undefined) { return; } - await gEdkWorkspaces.loadConfig(); + for (const op of selectedOptions) { + if (op.detail) { + selectedFolders.push(op.detail); + } + } + } else { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + vscode.window.showErrorMessage(`${buildPath[0].fsPath} its outside workspace`); + return; } - } else { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - vscode.window.showErrorMessage(`${buildPath[0].fsPath} its outside workspace`); + return; + } + } + + if (selectedFolders.length === 0) { return; } + + gDebugLog.trace("Loading from build"); + + let buildFolder = new BuildFolder(selectedFolders); + let buildData = await buildFolder.getBuildOptions(); + if (buildData) { + gDebugLog.trace("Delete workspace files"); + // await gEdkDatabase.clearWorkspace(); + deleteEdkCodeFolder(); + gConfigAgent.clearWpConfiguration(); + await gConfigAgent.setBuildDefines(buildData.buildDefines); + await gConfigAgent.setBuildDscPaths(buildData.dscFiles); + buildFolder.copyCompileInfoToRoot(); + + // If cscope.file is not generated, then calculate files based on dsc parsing + if(existsEdkCodeFolderFile(".missing")){ + infoMissingCompileInfo(); + await reloadSymbols(); + }else{ + await checkCppConfiguration(); + await gCscope.reload(); } + + // Generate .ignore if setting is set and .ignore doesnt exists + if (gConfigAgent.getIsGenIgnoreFile()) { + await genIgnoreFile(); + } + + await buildFolder.copyMapFilesList(); + gMapFileManager.load(); + + void vscode.window.showInformationMessage("Build data loaded"); + + await gEdkWorkspaces.loadConfig(); } } + export async function unloadWorkspace() { + const confirm = await vscode.window.showWarningMessage( + "Are you sure you want to unload the workspace? This will remove all indexed data.", + { modal: true }, + "Unload" + ); + if (confirm !== "Unload") { return; } + + gDebugLog.trace("Unloading workspace"); + deleteEdkCodeFolder(); + gConfigAgent.clearWpConfiguration(); + gEdkWorkspaces.workspaces = []; + edkWorkspaceTreeProvider.refresh(); + void vscode.window.showInformationMessage("Workspace unloaded"); + } + export async function rescanIndex() { gConfigAgent.reloadConfigFile(); await reloadSymbols(); @@ -256,6 +444,7 @@ import { checkCppConfiguration } from "../cppProviders/cppUtils"; locations = await wp.getInfReference(fileUri); if(locations.length){ await vscode.commands.executeCommand('editor.action.goToLocations', vscode.window.activeTextEditor?.document.uri, vscode.window.activeTextEditor?.selection.active, locations, "gotoAndPeek", "Not found"); + await edkWorkspaceTreeProvider.revealInfInTree(locations[0].uri, edkWorkspaceTreeView); } } }else{ @@ -340,189 +529,6 @@ import { checkCppConfiguration } from "../cppProviders/cppUtils"; - } - export async function showLibUsage(fileUri:vscode.Uri) { - throw new Error("Method not implemented."); - } - - - export function showReferences(): void { - throw new Error("Method not implemented."); - } - - - - export async function showEdkMap(moduleUri:vscode.Uri) { - let parser = await getParser(moduleUri); - let contextModule = moduleUri; - let contextSelected = false; - if(parser && (parser instanceof InfParser) ){ - // Check if INF file is a library - if(parser.isLibrary()){ - let modulesUsage: EdkModule[] = []; - let modules = ModuleReport.getInstance().getModuleList(); - for (const module of modules) { - if(!module.isLibrary){ - for (const lib of module.libraries) { - if(pathCompare(lib.path, moduleUri.fsPath)){ - modulesUsage.push(module); - } - } - } - } - if(modulesUsage.length){ - const options = modulesUsage.map(mod => ({ - label: mod.name, - description: mod.path, - module: mod - })); - const selectedOption = await vscode.window.showQuickPick(options, { - title: "Select a Module for context", - canPickMany: false - }); - if (!selectedOption) { - return; - } - contextModule = vscode.Uri.file(selectedOption.module.path); - contextSelected = true; - - }else{ - - // list all modules and ask for context - void vscode.window.showWarningMessage("This INF is a library. This command only works with EDK Modules for now"); - return; - } - } - - // Initialize tree view - edkLensTreeDetailProvider.clear(); - edkLensTreeDetailProvider.refresh(); - edkLensTreeDetailView.title = "EDK2 Module Map"; - let description = path.basename(moduleUri.fsPath); - if(moduleUri.fsPath !== contextModule.fsPath){ - description = `${path.basename(moduleUri.fsPath)} - ${path.basename(contextModule.fsPath)}`; - } - edkLensTreeDetailView.description = description; - - - let wps = await gEdkWorkspaces.getWorkspace(contextModule); - let libraries = parser.getSymbolsType(Edk2SymbolType.infLibrary) as EdkSymbolInfLibrary[]; - if(libraries.length === 0){ - void vscode.window.showWarningMessage("No libraries found in file"); - return; - } - - for (const wp of wps) { - // let dscDeclarations = await wp.getDscDeclaration(fileUri); - const sectionRange = libraries[0].parent?.range.start; - if(sectionRange===undefined){continue;} - let librarySet = new Set(); - let moduleNode; - if(parser.isLibrary()){ - moduleNode = new EdkInfNodeLibrary(moduleUri, contextModule, sectionRange, wp, libraries[0].parent!, librarySet); - }else{ - - moduleNode = new EdkInfNode(moduleUri, contextModule, sectionRange, wp, libraries[0].parent!, librarySet); - } - - edkLensTreeDetailProvider.addChildren(moduleNode); - - } - } - - await edkLensTreeDetailView.reveal(edkLensTreeDetailProvider.data[0]); - - } - - - export async function showReferenceStack(fileUri: Uri){ - return await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: "Looking for references...", - cancellable: false - }, async (progress, reject) => { - let document = await openTextDocument(fileUri); - return await updateInclussionTree(document); - }); - - } - - let _maxDscRec = 10; - export async function updateInclussionTree(document: vscode.TextDocument){ - edkLensTreeDetailProvider.clear(); - let wps = await gEdkWorkspaces.getWorkspace(document.uri); - for (const wp of wps) { - let currentDocument = document; - let infReferences:vscode.Location[] = []; - var dscReferences:vscode.Location[] = []; - let rootNode = new TreeItem(`[${wp.platformName}]` || "Undefined Platform"); - let cNode = new TreeItem(""); - edkLensTreeDetailProvider.addChildren(rootNode); - - switch (currentDocument.languageId) { - case "c": - cNode = new FileTreeItem(document.uri, new vscode.Position(0,0),wp); - rootNode.addChildren(cNode); - infReferences = await wp.getInfReference(document.uri); - // intentional with no break - case "edk2_inf": - let targetNode = cNode; - if(infReferences.length===0){ - infReferences = [new vscode.Location(document.uri, document.positionAt(0))]; - targetNode = rootNode; - } - - for (const infRef of infReferences) { - let infNode = new FileTreeItem(infRef.uri, infRef.range.start,wp); - targetNode.addChildren(infNode); - dscReferences = (await wp.getDscDeclaration(infRef.uri)).map(x=>{return x.location;}); - for (const dscRef of dscReferences) { - _maxDscRec = 10; - let targetNode = new FileTreeItem(dscRef.uri, dscRef.range.start,wp); - await _dscIncRefs(targetNode,wp, infNode); - } - } - break; - case "edk2_fdf": - case "edk2_dsc" : - - dscReferences = [new vscode.Location(document.uri, document.positionAt(0))]; - - - for (const dscRef of dscReferences) { - let refNode = new FileTreeItem(dscRef.uri, dscRef.range.start,wp); - _maxDscRec = 10; - await _dscIncRefs(refNode,wp,rootNode); - } - break; - default: - edkLensTreeDetailProvider.clear(); - - } - } - - - edkLensTreeDetailProvider.refresh(); - edkLensTreeDetailView.title = "EDK2 References"; - edkLensTreeDetailView.description = path.basename(document.uri.fsPath); - await edkLensTreeDetailView.reveal(edkLensTreeDetailProvider.data[0]); - } - - - export async function _dscIncRefs(referenceNode:FileTreeItem, wp:EdkWorkspace, targetNode:TreeItem){ - if(_maxDscRec === 0){return;} - _maxDscRec --; - - - let dscInclude = await wp.getIncludeReference(referenceNode.uri); - if(dscInclude.length === 0){ - referenceNode.collapsibleState = vscode.TreeItemCollapsibleState.None; - } - targetNode.addChildren(referenceNode); - for (const i of dscInclude) { - let newNode = new FileTreeItem(i.uri, i.range.start,wp); - await _dscIncRefs(newNode, wp, referenceNode); - } } @@ -535,6 +541,7 @@ import { checkCppConfiguration } from "../cppProviders/cppUtils"; cancellable: true }, async (progress, reject) => { await gEdkWorkspaces.loadConfig(); + edkWorkspaceTreeProvider.refresh(); let filesList:string[] = []; let decList:string[] = []; let hFiles:string[] = []; @@ -543,8 +550,6 @@ import { checkCppConfiguration } from "../cppProviders/cppUtils"; edkStatusBar.setWorking(); proccesedInfFiles = new Set(); - let factory = new ParserFactory(); - let wpInfFiles:InfDsc[] = []; // Grab all inf files in all workspaces for (const wp of gEdkWorkspaces.workspaces) { @@ -571,15 +576,18 @@ import { checkCppConfiguration } from "../cppProviders/cppUtils"; proccesedInfFiles.add(p[0].uri.fsPath); } let document = await openTextDocument(p[0].uri); - let parser = factory.getParser(document); + let parser = await getParserForDocument(document); if(parser){ - await parser.parseFile(); + + // Parse source files let sources = parser.getSymbolsType(Edk2SymbolType.infSource); filesList.push(parser.document.fileName); for (const source of sources) { if(reject.isCancellationRequested){break;} filesList.push(await source.getValue()); } + + // Parse DEC files let decs = parser.getSymbolsType(Edk2SymbolType.infPackage); for (const dec of decs) { if(reject.isCancellationRequested){break;} @@ -589,9 +597,8 @@ import { checkCppConfiguration } from "../cppProviders/cppUtils"; let decPath = await gPathFind.findPath(decValue); if(decPath.length){ let decDoc = await openTextDocument(decPath[0].uri); - let decParser = factory.getParser(decDoc); + let decParser = await getParserForDocument(decDoc); if(decParser){ - await decParser.parseFile(); let decIncludes = decParser.getSymbolsType(Edk2SymbolType.decInclude); for (const decInclude of decIncludes) { if(reject.isCancellationRequested){break;} @@ -649,45 +656,49 @@ import { checkCppConfiguration } from "../cppProviders/cppUtils"; cancellable: true }, async (progress, reject) => { - return new Promise((resolve, token) => { - gDebugLog.info("Generating .ignore file"); - let cscopeFilesList = gCscope.readCscopeFile(); - let filesSet = new Set(); - let filesExtensions = new Set(); - for (const cscopeFile of cscopeFilesList) { - let progFileUpper = cscopeFile.toUpperCase(); - filesSet.add(progFileUpper); - filesExtensions.add(path.extname(progFileUpper)); - } - - // Glob library needs posix path - let lookPath = toPosix(path.join(gWorkspacePath, "**")); - let globFilesList = glob.sync(lookPath); - let ignoreList = []; - - // Add extra ignore patterns - let extraIgnores = gConfigAgent.getExtraIgnorePatterns(); - for (const extraIgnore of extraIgnores) { - ignoreList.push(extraIgnore.trim()); - } + gDebugLog.info("Generating .ignore file"); + let cscopeFilesList = gCscope.readCscopeFile(); + gDebugLog.info(`genIgnoreFile: cscope list has ${cscopeFilesList.length} entries`); + let filesSet = new Set(); + let filesExtensions = new Set(); + for (const cscopeFile of cscopeFilesList) { + let progFileUpper = cscopeFile.toUpperCase(); + filesSet.add(progFileUpper); + filesExtensions.add(path.extname(progFileUpper)); + } + gDebugLog.info(`genIgnoreFile: tracking ${filesExtensions.size} extensions: ${[...filesExtensions].join(", ")}`); + + gDebugLog.info(`genIgnoreFile: listing workspace files under ${gWorkspacePath}`); + let globFilesList = (await listFilesRecursive(gWorkspacePath)).map(f => path.join(gWorkspacePath, f)); + gDebugLog.info(`genIgnoreFile: workspace file scan returned ${globFilesList.length} files`); + let ignoreList = []; + + // Add extra ignore patterns + let extraIgnores = gConfigAgent.getExtraIgnorePatterns(); + gDebugLog.info(`genIgnoreFile: ${extraIgnores.length} extra ignore patterns from config`); + for (const extraIgnore of extraIgnores) { + ignoreList.push(extraIgnore.trim()); + } - let posixWorkspacePath = toPosix(gWorkspacePath); - gDebugLog.info(`Cscope files found: ${filesSet.size}`); + let posixWorkspacePath = toPosix(gWorkspacePath); + gDebugLog.info(`Cscope files found: ${filesSet.size}`); - for (const globFile of globFilesList) { - // Just ignore EDK files - let globFileUpperCase = path.resolve(globFile.toUpperCase()); - let extension = path.extname(globFileUpperCase); - if (filesExtensions.has(extension) && !filesSet.has(globFileUpperCase)) { + let reportCount = 0; + for (const globFile of globFilesList) { + // Just ignore EDK files + const globFileUpperCase = globFile.toUpperCase(); + const extension = path.extname(globFileUpperCase); + if (filesExtensions.has(extension) && !filesSet.has(globFileUpperCase)) { + if (++reportCount % 100 === 0) { progress.report({ message: globFile }); - - ignoreList.push(toPosix(path.relative(posixWorkspacePath, globFile))); } + ignoreList.push(toPosix(path.relative(posixWorkspacePath, globFile))); } - fs.writeFileSync(path.join(gWorkspacePath, ".ignore"), ignoreList.join("\n")); - resolve(); - }); - + } + gDebugLog.info(`genIgnoreFile: ${ignoreList.length} entries written to .ignore (${reportCount} files ignored)`); + const ignoreFilePath = path.join(gWorkspacePath, ".ignore"); + fs.writeFileSync(ignoreFilePath, ignoreList.join("\n")); + gDebugLog.info(`genIgnoreFile: .ignore written to ${ignoreFilePath}`); }); @@ -698,34 +709,4 @@ export async function openWpConfigGui() { } export async function openWpConfigJson() { await gotoFile(gConfigAgent.getConfigFileUri()); -} - - - - - -var nodeStack = new Array(); - -export async function focusOnNode(node:TreeItem) { - nodeStack.push(edkLensTreeDetailProvider.data); - await vscode.commands.executeCommand('setContext', 'edk2code.isNodeFocusBackStack', true); - - edkLensTreeDetailProvider.clear(); - edkLensTreeDetailProvider.addChildren(node); - await edkLensTreeDetailView.reveal(edkLensTreeDetailProvider.data[0]); - edkLensTreeDetailProvider.refresh(); - edkLensTreeDetailView.title = "EDK2 Module Map"; -} - -export async function nodeFocusBack(){ - if(nodeStack.length){ - edkLensTreeDetailProvider.clear(); - edkLensTreeDetailProvider.data = nodeStack.pop()!; - await edkLensTreeDetailView.reveal(edkLensTreeDetailProvider.data[0]); - edkLensTreeDetailProvider.refresh(); - } - - if(nodeStack.length === 0){ - await vscode.commands.executeCommand('setContext', 'edk2code.isNodeFocusBackStack', false); - } } \ No newline at end of file diff --git a/src/cppProviders/cppUtils.ts b/src/cppProviders/cppUtils.ts index cb2ebe9..80673cd 100644 --- a/src/cppProviders/cppUtils.ts +++ b/src/cppProviders/cppUtils.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { gDebugLog, gWorkspacePath } from '../extension'; +import { gDebugLog } from '../extension'; import * as fs from 'fs'; import { infoMissingCppExtension } from '../ui/messages'; import { closeFileIfOpened, delay } from '../utils'; diff --git a/src/cppProviders/msCpp.ts b/src/cppProviders/msCpp.ts index 97fcda4..978379e 100644 --- a/src/cppProviders/msCpp.ts +++ b/src/cppProviders/msCpp.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { CppProvider } from './cppProvider'; import * as fs from 'fs'; -import { closeFileIfOpened, delay } from '../utils'; +import {closeFileIfOpened, delay } from '../utils'; import { gDebugLog, gWorkspacePath } from '../extension'; import path = require("path"); import { updateCompilesCommandCpp } from '../ui/messages'; @@ -24,8 +24,13 @@ export class MsCppProvider extends CppProvider { async validateConfiguration(): Promise { + if (vscode.workspace.workspaceFile !== undefined){ + // Don't try to fix CPP if the workspace is not a folder + return true; + } + if(!this.isCppPropertiesFile()){ - if(! await this.isFixFile()){ + if(!await this.isFixFile()){ return true; } await this.touchCppPropertiesFile(); // Asks CPP extension to create the file diff --git a/src/cscope.ts b/src/cscope.ts index bbd9653..a42f6ef 100644 --- a/src/cscope.ts +++ b/src/cscope.ts @@ -120,7 +120,7 @@ export class Cscope { void vscode.window.showErrorMessage("Cscope not available in the system. Check help for install", "Help").then(async selection => { if (selection === "Help"){ await vscode.env.openExternal(vscode.Uri.parse( - 'https://github.com/intel/Edk2Code/wiki#cscope')); + 'https://intel.github.io/Edk2Code/getting_started/#cscope-installation-windowslinux')); } }); } @@ -132,6 +132,7 @@ export class Cscope { } writeCscopeFile(fileList:string[]){ + if(!gConfigAgent.getUseCscope()){ return; } if(fileList.length === 0){ return; } @@ -164,6 +165,10 @@ export class Cscope { async reload(progressWindow=false){ gDebugLog.info("CSCOPE reload database"); + if(!gConfigAgent.getUseCscope()){ + gDebugLog.info("CSCOPE is disabled by setting"); + return; + } if(!this.cscopeInstalled){ this.showCscopeErrorMessage(); return; @@ -182,17 +187,20 @@ export class Cscope { } async getCaller(text:string){ + if(!gConfigAgent.getUseCscope()){ return []; } let result = await this.cscopeCommandWindow(text, CscopeCmd.findCallers, "Looking callers"); let temp = this.parseResult(result, text); return temp; } async getCallee(text:string){ + if(!gConfigAgent.getUseCscope()){ return []; } let result = await this.cscopeCommandWindow(text, CscopeCmd.findCallee, "Looking callees"); return this.parseResult(result, text); } async search(text:string){ + if(!gConfigAgent.getUseCscope()){ return []; } let result = await this.cscopeCommandWindow(text, CscopeCmd.findEgrep, "Searching"); let searchResult = this.parseResult(result, text); @@ -209,6 +217,7 @@ export class Cscope { } async getDefinitionPositions(text:string, showWindows:boolean=true){ + if(!gConfigAgent.getUseCscope()){ return []; } let windDescription = "Looking for definition"; if(!showWindows){ @@ -295,6 +304,7 @@ export class CscopeAgent { } async writeCscopeFile(fileList:string[]){ + if(!gConfigAgent.getUseCscope()){ return; } if(fileList.length === 0){ return; } @@ -328,6 +338,7 @@ export class CscopeAgent { /** * Updates Cscope database based on cscope.files elements */ + if(!gConfigAgent.getUseCscope()){ return; } const debouncer = Debouncer.getInstance(); debouncer.debounce("updateCscopeDb", async () => { let cscopeFilesPath = getEdkCodeFolderFilePath("cscope.files"); diff --git a/src/debugLog.ts b/src/debugLog.ts index 846a366..fda777b 100644 --- a/src/debugLog.ts +++ b/src/debugLog.ts @@ -11,11 +11,11 @@ export enum LogLevel { } export class DebugLog { - outConsole: vscode.OutputChannel; + outConsole: vscode.LogOutputChannel; public constructor() { - this.outConsole = vscode.window.createOutputChannel("EDK2Code"); + this.outConsole = vscode.window.createOutputChannel("EDK2Code", {log:true}); } public show(){ @@ -27,51 +27,28 @@ export class DebugLog { } public error(text:string){ - this.out(`[Edk2Code Error] ${text}`, LogLevel.error); + this.outConsole.error("[EDK2Code] " + text); let callStack = (new Error()).stack || ''; - this.out(`[Edk2Code Stack]\n${callStack}`, LogLevel.error); + this.outConsole.error(`[Stack]\n${callStack}`); } public info(text:string){ - this.out(`[Edk2Code Info] ${text}`, LogLevel.info); + this.outConsole.info("[EDK2Code] " + text); } - public verbose(text:string){ - this.out(`[Edk2Code Verb] ${text}`, LogLevel.verbose); + public trace(text:string){ + this.outConsole.trace("[EDK2Code] " + text); } public warning(text:string){ - this.out(`[Edk2Code Warn] ${text}`, LogLevel.warning); + this.outConsole.warn("[EDK2Code] " + text); } public debug(text:string){ - this.out(`[Edk2Code Debug] ${text}`, LogLevel.debug); + this.outConsole.trace("[EDK2Code] " + text); } - public out(text:string, level:LogLevel|undefined = undefined){ - if(level === undefined){ - this.outConsole.appendLine(text); - return; - } - - if(gConfigAgent === undefined){ - this.outConsole.appendLine(text); - console.log(text); - return; - } - - if (level <= gConfigAgent.getLogLevel()){ - this.outConsole.appendLine(text); - if(level === LogLevel.error){ - console.error(text); - }else if(level===LogLevel.warning){ - console.warn(text); - }else{ - console.info(text); - } - } - } } \ No newline at end of file diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 45856cb..ce1626d 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -29,6 +29,9 @@ emptyFile, circularDependency, inactiveCode, edk2CodeUnsuported, +errorMessage, + undefinedVariable, + duplicateDefine } export const edkErrorDescriptions: Map = new Map([ @@ -58,6 +61,9 @@ export const edkErrorDescriptions: Map = new Map([ [EdkDiagnosticCodes.circularDependency, "Circular dependency"], [EdkDiagnosticCodes.inactiveCode, "Inactive code"], [EdkDiagnosticCodes.edk2CodeUnsuported, "Edk2Code unsupported"], + [EdkDiagnosticCodes.errorMessage, "Error message"], + [EdkDiagnosticCodes.undefinedVariable, "Undefined variable"], + [EdkDiagnosticCodes.duplicateDefine, "Duplicate DEFINE overrides previous value"], ]); export class DiagnosticManager { @@ -122,6 +128,16 @@ export class DiagnosticManager { return DiagnosticManager.diagnostics.get(documentUri.fsPath) || []; } + /** + * Find a diagnostic matching one of the given codes at a specific line for a URI. + * Returns the first matching diagnostic, or undefined. + */ + public static findDiagnosticAt(documentUri: vscode.Uri, line: number, codes: EdkDiagnosticCodes[]): vscode.Diagnostic | undefined { + const diags = DiagnosticManager.diagnostics.get(documentUri.fsPath); + if (!diags) { return undefined; } + return diags.find(d => d.range.start.line === line && codes.includes(d.code as number)); + } + private static clearDiagnostic(documentUri: vscode.Uri) { DiagnosticManager.diagnostics.delete(documentUri.fsPath); } @@ -140,6 +156,8 @@ export class DiagnosticManager { relatedInformation?: vscode.DiagnosticRelatedInformation[]|undefined) { + + if(gConfigAgent.isDiagnostics() === false){ this.clearAllProblems(); return undefined; @@ -152,6 +170,18 @@ export class DiagnosticManager { }else{ range = new vscode.Range(line as number, 0, line as number, Number.MAX_VALUE); } + + // Check if the diagnostic already exists + const existingDiagnostics = DiagnosticManager.getDiagnostic(documentUri); + const isDuplicate = existingDiagnostics.some(diagnostic => + diagnostic.range.isEqual(range) && + diagnostic.message === message && + diagnostic.severity === severity + ); + + if (isDuplicate) { + return undefined; // Skip adding duplicate diagnostic + } const diagnostic = new vscode.Diagnostic(range, message, severity); diff --git a/src/driverInfoTree/DriverInfoTreeProvider.ts b/src/driverInfoTree/DriverInfoTreeProvider.ts new file mode 100644 index 0000000..b7d00a5 --- /dev/null +++ b/src/driverInfoTree/DriverInfoTreeProvider.ts @@ -0,0 +1,424 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { gEdkWorkspaces, gPathFind, gCompileCommands } from '../extension'; +import { getParser } from '../edkParser/parserFactory'; +import { EdkSymbol } from '../symbols/edkSymbols'; +import { Edk2SymbolType } from '../symbols/symbolsType'; +import { DocumentParser } from '../edkParser/languageParser'; +import { rgSearch } from '../rg'; + +// ─── Tree item types ────────────────────────────────────────────────────────── + +export class DriverInfoCategoryItem extends vscode.TreeItem { + children: DriverInfoLeafItem[] = []; + constructor(label: string, icon: string) { + super(label, vscode.TreeItemCollapsibleState.Expanded); + this.iconPath = new vscode.ThemeIcon(icon); + this.contextValue = 'driverInfoCategory'; + } +} + +export class DriverInfoLeafItem extends vscode.TreeItem { + constructor( + label: string, + description: string, + icon: vscode.ThemeIcon, + command?: vscode.Command, + fileUri?: vscode.Uri + ) { + super(label, vscode.TreeItemCollapsibleState.None); + this.description = description; + this.iconPath = icon; + if (fileUri) { + this.resourceUri = fileUri; + this.iconPath = vscode.ThemeIcon.File; + } + if (command) { + this.command = command; + } + } +} + +type DriverInfoNode = DriverInfoCategoryItem | DriverInfoLeafItem; + +// ─── Provider ───────────────────────────────────────────────────────────────── + +export class DriverInfoTreeProvider implements vscode.TreeDataProvider { + + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private categories: DriverInfoCategoryItem[] = []; + private _infUri: vscode.Uri | undefined; + private get infUri() { return this._infUri; } + private set infUri(v: vscode.Uri | undefined) { this._infUri = v; } + get currentInfUri(): vscode.Uri | undefined { return this._infUri; } + private parser: DocumentParser | undefined; + private _treeView: vscode.TreeView | undefined; + + /** Set the tree view reference so the provider can update its title. */ + setTreeView(treeView: vscode.TreeView) { + this._treeView = treeView; + } + private disposables: vscode.Disposable[] = []; + private _suppressNextUpdate = false; + private _lastEditorFsPath: string | undefined; + private _lastSuppressTime = 0; + + /** Call before programmatically opening a file to prevent the driver info from refreshing. + * Returns true if suppression was set (single click), false if double-click was detected. */ + suppressNextUpdate(): boolean { + const now = Date.now(); + if (now - this._lastSuppressTime < 500) { + // Double-click detected: cancel suppression so driver info updates + this._suppressNextUpdate = false; + this._lastSuppressTime = 0; + return false; + } else { + this._suppressNextUpdate = true; + this._lastSuppressTime = now; + return true; + } + } + + constructor() { + this.disposables.push( + vscode.window.onDidChangeActiveTextEditor(async (editor) => { + if (editor) { + if (!this._suppressNextUpdate) { + this._lastEditorFsPath = editor.document.uri.fsPath; + } + await this.onEditorChanged(editor); + } + }), + vscode.window.onDidChangeTextEditorSelection(async (e) => { + // Fires when the user clicks into an already-active editor (focus-back). + // Only process if the document differs from what is currently displayed. + if (e.textEditor.document.uri.fsPath !== this._lastEditorFsPath) { + this._lastEditorFsPath = e.textEditor.document.uri.fsPath; + await this.onEditorChanged(e.textEditor); + } + }) + ); + } + + dispose() { + for (const d of this.disposables) { d.dispose(); } + } + + getTreeItem(element: DriverInfoNode): vscode.TreeItem { + return element; + } + + getChildren(element?: DriverInfoNode): DriverInfoNode[] { + if (!element) { + return this.categories; + } + if (element instanceof DriverInfoCategoryItem) { + return element.children; + } + return []; + } + + // ─── Editor change handler ──────────────────────────────────────────────── + + async onEditorChanged(editor: vscode.TextEditor) { + if (this._suppressNextUpdate) { + this._suppressNextUpdate = false; + return; + } + + const document = editor.document; + const langId = document.languageId; + + // Only process EDK-relevant files + if (!['c', 'cpp', 'edk2_dsc', 'edk2_inf', 'edk2_dec', 'asl', 'edk2_vfr', 'edk2_fdf', 'edk2_uni'].includes(langId)) { + this.clear(); + return; + } + + // Check if file is in use + if (gEdkWorkspaces.isConfigured()) { + const isUsed = await gEdkWorkspaces.isFileInUse(document.uri); + if (isUsed !== true) { + this.clear(); + return; + } + } + + // Find the INF file + const infUri = await this.findInfForFile(document.uri); + if (!infUri) { + this.clear(); + return; + } + + // If same INF, no need to rebuild + if (this.infUri && this.infUri.fsPath === infUri.fsPath) { + return; + } + + this.infUri = infUri; + await this.buildTree(); + } + + // ─── Locate INF file ────────────────────────────────────────────────────── + + private async findInfForFile(fileUri: vscode.Uri): Promise { + // If the file itself is an INF + if (fileUri.fsPath.match(/\.inf$/i)) { + return fileUri; + } + + const wps = await gEdkWorkspaces.getWorkspace(fileUri); + if (wps.length) { + for (const wp of wps) { + const locations = await wp.getInfReference(fileUri); + if (locations.length) { + return locations[0].uri; + } + } + } else { + // Fallback: search for INF file referencing this source + const fileName = path.basename(fileUri.fsPath); + const folderPath = path.dirname(fileUri.fsPath); + const tempLocations = await rgSearch(`\\b${fileName}\\b`, ['*.inf'], [], true); + for (const l of tempLocations) { + if (!path.relative(path.dirname(l.uri.fsPath), folderPath).includes('..')) { + return l.uri; + } + } + } + return undefined; + } + + // ─── Build tree from INF parser ─────────────────────────────────────────── + + private async buildTree() { + this.categories = []; + + if (!this.infUri) { + this.refresh(); + return; + } + + const parser = await getParser(this.infUri); + if (!parser) { + this.refresh(); + return; + } + this.parser = parser; + + await vscode.commands.executeCommand('setContext', 'edk2code.driverInfoAvailable', true); + + // Set view title to BASE_NAME + const defines = parser.getSymbolsType(Edk2SymbolType.infDefine); + let baseName = ''; + for (const def of defines) { + const key = await def.getKey(); + if (key.toLowerCase() === 'base_name') { + baseName = await def.getValue(); + break; + } + } + if (this._treeView) { + this._treeView.title = baseName || path.basename(this.infUri.fsPath); + } + + // Defines + if (defines.length) { + const cat = new DriverInfoCategoryItem('Defines', 'symbol-constant'); + for (const def of defines) { + const key = await def.getKey(); + const value = await def.getValue(); + const item = new DriverInfoLeafItem( + key, + value, + new vscode.ThemeIcon('symbol-property'), + { + command: 'edk2code.driverInfoOpenFile', + title: 'Open', + arguments: [def.location.uri, { selection: def.location.range }] + } + ); + cat.children.push(item); + } + this.categories.push(cat); + } + + // Sources + const sources = parser.getSymbolsType(Edk2SymbolType.infSource); + if (sources.length) { + const cat = new DriverInfoCategoryItem('Sources', 'files'); + gCompileCommands.load(); + for (const src of sources) { + const filePath = src.textLine.replace(/\s*\|.*/, ''); + const relPath = path.dirname(this.infUri.fsPath); + const locations = await gPathFind.findPath(filePath, relPath); + const resolvedUri = locations.length ? locations[0].uri : vscode.Uri.file(path.join(relPath, filePath)); + const hasCompileCommand = gCompileCommands.getCompileCommandForFile(resolvedUri.fsPath) !== undefined; + const item = new DriverInfoLeafItem( + filePath, + '', + vscode.ThemeIcon.File, + locations.length ? { + command: 'edk2code.driverInfoOpenFile', + title: 'Open file', + arguments: [locations[0].uri] + } : undefined, + resolvedUri + ); + if (hasCompileCommand) { + item.contextValue = 'driverInfoCompilableSource'; + } + cat.children.push(item); + } + this.categories.push(cat); + } + + // Libraries + const libraries = parser.getSymbolsType(Edk2SymbolType.infLibrary); + if (libraries.length) { + const cat = new DriverInfoCategoryItem('Libraries', 'library'); + for (const lib of libraries) { + const libName = lib.textLine.replace(/\s*\|.*/, ''); + const item = new DriverInfoLeafItem( + libName, + '', + new vscode.ThemeIcon('symbol-module'), + { + command: 'edk2code.driverInfoGoToDefinition', + title: 'Go to definition', + arguments: [lib] + } + ); + cat.children.push(item); + } + this.categories.push(cat); + } + + // Packages + const packages = parser.getSymbolsType(Edk2SymbolType.infPackage); + if (packages.length) { + const cat = new DriverInfoCategoryItem('Packages', 'package'); + for (const pkg of packages) { + const pkgPath = pkg.textLine.replace(/\s*\|.*/, ''); + const item = new DriverInfoLeafItem( + pkgPath, + '', + new vscode.ThemeIcon('symbol-package'), + { + command: 'edk2code.driverInfoGoToDefinition', + title: 'Go to definition', + arguments: [pkg] + } + ); + cat.children.push(item); + } + this.categories.push(cat); + } + + // Protocols + const protocols = parser.getSymbolsType(Edk2SymbolType.infProtocol); + if (protocols.length) { + const cat = new DriverInfoCategoryItem('Protocols', 'plug'); + for (const proto of protocols) { + const protoName = proto.textLine.replace(/\s*\|.*/, ''); + const item = new DriverInfoLeafItem( + protoName, + '', + new vscode.ThemeIcon('symbol-interface'), + { + command: 'edk2code.driverInfoGoToDefinition', + title: 'Go to definition', + arguments: [proto] + } + ); + cat.children.push(item); + } + this.categories.push(cat); + } + + // PPIs + const ppis = parser.getSymbolsType(Edk2SymbolType.infPpi); + if (ppis.length) { + const cat = new DriverInfoCategoryItem('PPIs', 'plug'); + for (const ppi of ppis) { + const ppiName = ppi.textLine.replace(/\s*\|.*/, ''); + const item = new DriverInfoLeafItem( + ppiName, + '', + new vscode.ThemeIcon('symbol-event'), + { + command: 'edk2code.driverInfoGoToDefinition', + title: 'Go to definition', + arguments: [ppi] + } + ); + cat.children.push(item); + } + this.categories.push(cat); + } + + // GUIDs + const guids = parser.getSymbolsType(Edk2SymbolType.infGuid); + if (guids.length) { + const cat = new DriverInfoCategoryItem('GUIDs', 'key'); + for (const guid of guids) { + const guidName = guid.textLine.replace(/\s*\|.*/, ''); + const item = new DriverInfoLeafItem( + guidName, + '', + new vscode.ThemeIcon('symbol-number'), + { + command: 'edk2code.driverInfoGoToDefinition', + title: 'Go to definition', + arguments: [guid] + } + ); + cat.children.push(item); + } + this.categories.push(cat); + } + + // PCDs + const pcds = parser.getSymbolsType(Edk2SymbolType.infPcd); + if (pcds.length) { + const cat = new DriverInfoCategoryItem('PCDs', 'settings'); + for (const pcd of pcds) { + const pcdName = pcd.textLine.replace(/\s*\|.*/, ''); + const item = new DriverInfoLeafItem( + pcdName, + '', + new vscode.ThemeIcon('symbol-string'), + { + command: 'edk2code.driverInfoGoToDefinition', + title: 'Go to definition', + arguments: [pcd] + } + ); + cat.children.push(item); + } + this.categories.push(cat); + } + + this.refresh(); + } + + // ─── Helpers ────────────────────────────────────────────────────────────── + + private clear() { + this.categories = []; + this.infUri = undefined; + this.parser = undefined; + if (this._treeView) { + this._treeView.title = 'Module Info'; + } + void vscode.commands.executeCommand('setContext', 'edk2code.driverInfoAvailable', false); + this.refresh(); + } + + private refresh() { + this._onDidChangeTreeData.fire(); + } +} diff --git a/src/edkParser/README.md b/src/edkParser/README.md new file mode 100644 index 0000000..86e33a8 --- /dev/null +++ b/src/edkParser/README.md @@ -0,0 +1,297 @@ +# EDK II Language Parser + +This folder implements a recursive-descent, line-oriented parser that transforms EDK II source files (`.dsc`, `.inf`, `.dec`, `.fdf`, `.vfr`, `.asl`) into a tree of `EdkSymbol` objects used for navigation, outline views, and diagnostics throughout the extension. + +## Architecture Overview + +``` + ParserFactory + | + selects by languageId + | + ┌─────────┬──────┴──────┬──────────┐ + DscParser InfParser DecParser FdfParser ... + │ + (extends DocumentParser) + │ + blockParsers: BlockParser[] ← top-level parsers + │ + └─ each BlockParser has + tag / start / end ← regex rules + context: BlockParser[] ← nested parsers +``` + +### Key classes + +| Class | File | Role | +|---|---|---| +| `BlockParser` | `languageParser.ts` | Abstract block parser — matches, opens, and closes blocks using regex rules | +| `DocumentParser` | `languageParser.ts` | Base class for all language parsers — drives the line-by-line loop and manages the symbol tree/stack | +| `ParserFactory` | `parserFactory.ts` | Instantiates the correct `DocumentParser` subclass by VS Code `languageId` | +| `DscParser`, `InfParser`, etc. | `dscParser.ts`, `infParser.ts`, etc. | Concrete parsers that define `blockParsers`, comment syntax, and language-specific state | + +--- + +## Parsing Lifecycle + +### 1. Entry point — `parseFile()` + +``` +DocumentParser.parseFile() + ┌─────────────────────────────────────┐ + │ for each line in the document: │ + │ skip comment lines │ + │ call parseLine() │ + │ increment lineIndex │ + └─────────────────────────────────────┘ +``` + +`parseFile()` iterates from line 1 to the end of the document. For every non-comment line it calls `parseLine()`. + +### 2. Top-level dispatch — `parseLine()` + +```typescript +async parseLine() { + for (parseIndex = 0 .. blockParsers.length) { + decrementLineIndex(); // rewind so block.parse() can re-read + result = blockParsers[parseIndex].parse(this); + if (result) { + // If the block consumed multiple lines (has start/end), reset + // parseIndex to -1 so parsing continues from the first parser + // on the new current line. + // If the block is root-only or single-line, just continue + // trying the next parser on the same line. + } + } +} +``` + +Each `BlockParser` in `blockParsers` is tried in order. When a parser matches and it is a multi-line block (has `start`/`end`), `parseIndex` resets to `-1` so the next line is evaluated again from the first parser. Single-line or root-only parsers let the loop continue without resetting. + +--- + +## BlockParser — Start, Content, and End + +The core parsing logic lives in `BlockParser.parse()`. A block parser is configured by four regex properties and a list of child parsers: + +| Property | Required | Purpose | +|---|---|---| +| `tag` | **yes** | Regex the current line must match for this block to activate | +| `excludeTag` | no | If the line matches this regex, the block is skipped even if `tag` matches | +| `start` | no | Regex marking the beginning of the block's body (e.g. `{`) | +| `end` | no | Regex marking the end of the block (e.g. `}` or `[`) | +| `context` | no | Child `BlockParser[]` that will be tried on every line inside the block body | + +### Three block shapes + +The combination of `start` and `end` determines how a block behaves: + +#### Single-line block (`start = undefined`, `end = undefined`) + +``` + tag matches → create symbol → pop immediately → return true +``` + +The block matches exactly one line. The symbol is created and immediately finalized. Examples: `BlockDefinition`, `BlocklibraryDef`, `BlockPcd`, `BlockBuildOption`. + +#### Section block (`start = undefined`, `end = /regex/`) + +``` + tag matches → create symbol → push onto stack + for each subsequent line: + try context[] child parsers + if line matches end → pop symbol → return true + EOF → pop symbol → return true +``` + +The block begins at the `tag` line and continues until a line matches `end`. Every line inside the block is tested against `context[]` child parsers. Examples: `BlockComponentsSection` (ends at `^\[`), `BlockComponentSubLibraryClasses` (ends at `^<` or `^\}`). + +#### Delimited block (`start = /regex/`, `end = /regex/`) + +``` + tag matches → create symbol → push onto stack + scan forward until a line matches start (e.g. "{") + (if end is hit first, pop and return early) + then for each subsequent line: + try context[] child parsers + if line matches end → pop symbol → return true + EOF → pop symbol → return true +``` + +After the tag line matches, the parser scans forward looking for the `start` delimiter before entering the content loop. This handles constructs where the opening brace may appear on the same line as the tag or on a subsequent line. Example: `BlockComponentInf` (tag matches `*.inf`, start matches `{`, end matches `}` or `[`). + +### Flowchart + +``` +parse(docParser) + │ + ├─ read line, check tag/excludeTag + │ └─ no match → return false + │ + ├─ check isRoot constraint + │ + ├─ create EdkSymbol via SymbolFactory + │ └─ addSymbol() + pushSymbolStack() + │ + ├─ if start is defined: + │ │ scan lines until start matches + │ │ (if end matches first → popSymbolStack, return true) + │ │ + │ └─ fall through to content loop ↓ + │ + ├─ else if end is undefined: + │ └─ single-line: popSymbolStack → return true + │ + ├─ else: fall through to content loop ↓ + │ + ├─ CONTENT LOOP: while hasPendingLines() + │ │ + │ ├─ read next line (skip empty) + │ │ + │ ├─ for each child in context[]: + │ │ │ decrement lineIndex (rewind) + │ │ │ child.parse(docParser) ← recursive + │ │ │ if matched and exclusive → break + │ │ │ + │ │ + │ └─ if line matches end → popSymbolStack → return true + │ + └─ EOF reached → popSymbolStack → return true +``` + +--- + +## How Symbols Are Added + +### Symbol creation + +When a `BlockParser`'s `tag` matches, a symbol is created through this sequence: + +1. **Range** — An initial range is created from the matched line's start to the end of the document (a placeholder that gets narrowed later). +2. **SymbolFactory** — `symbolFactory.produceSymbol(type, textLine, location, parser)` instantiates the specific `EdkSymbol` subclass based on the `Edk2SymbolType` enum. +3. **addSymbol()** — The symbol is placed into the tree: + - If `symbolStack` is empty → pushed to `symbolsTree` (root level). + - Otherwise → added as a `child` of the current top-of-stack symbol, and `parent` is set. + - In all cases, also appended to the flat `symbolsList`. +4. **pushSymbolStack()** — The symbol is pushed onto the stack to become the current parent for any nested symbols. + +### Symbol finalization + +When the block ends (either by matching `end`, reaching EOF, or being a single-line block), `popSymbolStack()` is called: + +- The symbol is popped from the stack. +- Its **range is narrowed**: the end position is adjusted from the initial "rest of document" placeholder to the actual last line the block covered. + - If the symbol has children → end is set to `lineIndex - 2` (the line before the end delimiter). + - If no children → end is set to `lineIndex - 1` (the tag line itself for single-line blocks). + +### Tree structure + +After parsing, `DocumentParser` holds two views of the symbols: + +| Collection | Type | Description | +|---|---|---| +| `symbolsTree` | `EdkSymbol[]` | Hierarchical tree — only root-level symbols; children are nested via `symbol.children` | +| `symbolsList` | `EdkSymbol[]` | Flat list — every symbol in document order, for fast iteration and filtering | + +The tree structure corresponds directly to block nesting. For example, parsing a DSC `[Components]` section produces: + +``` +EdkSymbolDscSection "Components.X64" + ├─ EdkSymbolDscModuleDefinition "Module.inf" + │ ├─ EdkSymbolDscComponentSubSection "" + │ │ └─ EdkSymbolDscLibraryDefinition "Lib|Path.inf" + │ ├─ EdkSymbolDscComponentSubSection "" + │ │ └─ EdkSymbolDscPcdDefinition "gPkg.PcdName|value" + │ └─ EdkSymbolDscComponentSubSection "" + │ └─ EdkSymbolDscBuildOption "MSFT:*_*_*_CC_FLAGS = /D FLAG" + └─ EdkSymbolDscModuleDefinition "Other.inf" +``` + +--- + +## Root-only parsers (`isRoot = true`) + +Some parsers are registered twice in `blockParsers`: + +```typescript +blockParsers: BlockParser[] = [ + new BlockComponentsSection(), // normal: matches inside any context + // ... + new BlockComponentInf(true), // isRoot: only matches at the document root +]; +``` + +When `isRoot` is `true`, the parser only activates when `symbolStack` is empty (i.e. no parent block is open). This allows a block to act as a "catch-all" for lines that appear outside any section, ensuring they still get a symbol without interfering with the normal section hierarchy. + +--- + +## `startContext` / `endContext` — alternate end delimiter + +Some EDK II constructs use a delimiter that appears *on the same line as the tag* to enter a special sub-block, and that sub-block requires a different `end` pattern than the surrounding block. `startContext` and `endContext` handle this case without needing a separate `BlockParser`. + +| Property | Type | Purpose | +|---|---|---| +| `startContext` | `RegExp \| undefined` | If this pattern matches the tag line (or any line scanned while looking for `start`), the block switches to `endContext` as its terminator | +| `endContext` | `RegExp \| undefined` | Alternative `end` regex used only when `startContext` was matched | + +### How it works + +1. After the `tag` matches and the symbol is created, the parser checks whether the tag line also matches `startContext`. If so, `inContext = true`. +2. While scanning forward looking for `start`, each scanned line is also tested against `startContext`. +3. Once the content loop begins, `activeEnd` is resolved: + - `inContext && endContext` → `activeEnd = endContext` + - otherwise → `activeEnd = end` +4. Additionally, after a child context parser consumes a line, the **last consumed line** is peeked and tested against `endContext`. This handles the case where the child parser itself consumed the closing delimiter. + +### Example — `BlockComponentInf` + +`BlockComponentInf` matches `*.inf` lines. A component entry can optionally have an opening brace `{` on the same (or next) line, introducing a sub-block with scoped overrides. Without `startContext`/`endContext`, both `{...}` and bare entries would need separate parsers. + +```typescript +class BlockComponentInf extends BlockParser { + tag = /^[\s\.\w\$\(\)_\-\\\/]*\.inf/gi; + start = /.*?{/; // look for the opening brace + end = /(^\})|(^\[)|(\.inf)|(^\!include)/gi; // bare-entry terminators + startContext = /\{/; // brace on the tag/start line → enter context mode + endContext = /^\s*\}/gi; // context mode ends only on closing brace + // ... +} +``` + +When `{` is found, `inContext` becomes `true` and `endContext` (`^\s*\}`) takes over from `end`, so only a closing brace terminates the sub-block. When `{` is absent, `end` applies as usual and the entry is treated as a single-line construct. + +--- + +## `exclusive` flag + +When a child parser in `context[]` matches a line and `exclusive` is `true` (the default), no further child parsers are tried for that line. Setting `exclusive = false` would allow multiple child parsers to process the same line. + +--- + +## Comment handling + +Each `DocumentParser` subclass defines its comment syntax: + +```typescript +commentStart: string = "/*"; // block comment open +commentEnd: string = "*/"; // block comment close +commentLine: string[] = ["#"]; // line comment prefixes +``` + +`removeComment()` strips line comments and tracks block comment state via the `inComment` flag. Lines that are entirely comments are skipped by `parseFile()` before calling `parseLine()`. + +--- + +## File Overview + +| File | Purpose | +|---|---| +| `languageParser.ts` | `BlockParser` + `DocumentParser` base classes | +| `parserFactory.ts` | Factory that instantiates the correct parser by `languageId` | +| `commonParser.ts` | Shared regex constants used across language parsers | +| `dscParser.ts` | DSC file parser (sections, components, library classes, PCDs, build options) | +| `infParser.ts` | INF module file parser | +| `decParser.ts` | DEC package declaration parser | +| `fdfParser.ts` | FDF flash description parser | +| `vfrParser.ts` | VFR visual forms parser | +| `aslParser.ts` | ASL/ACPI source parser | diff --git a/src/edkParser/commonParser.ts b/src/edkParser/commonParser.ts index 2d1158d..b4cbeb7 100644 --- a/src/edkParser/commonParser.ts +++ b/src/edkParser/commonParser.ts @@ -1,6 +1,6 @@ -export const REGEX_DEFINE = /^(?:(?![=\!]).)*=.*/gi; +export const REGEX_DEFINE = /^\s*(?:DEFINE\s+\w+|)\s*=.*/gi; export const REGEX_INCLUDE = /\s*\!include/gi; - +export const REGEX_EQUAL = /\s*\w+\s*=\s*?.*/gi; export const REGEX_PATH_FILE = /(?<=^.*?\|\s*)[a-zA-Z\/\\0-9\._]+\.([a-z\d])/gi; export const REGEX_PATH_FOLDER = /^\s*[a-zA-Z\/\\0-9\._]+/gi; diff --git a/src/edkParser/dscParser.ts b/src/edkParser/dscParser.ts index f758fe4..395ad71 100644 --- a/src/edkParser/dscParser.ts +++ b/src/edkParser/dscParser.ts @@ -19,17 +19,70 @@ class BlockIncludes extends BlockParser { +class BlockBuildOption extends BlockParser { + name = "BuildOption"; + tag = /^[\w\*\:\|]+\s*=\s*.*/gi; + start = undefined; + end = undefined; + type = Edk2SymbolType.dscBuildOption; + visible: boolean = true; +} + +class BlockComponentSubLibraryClasses extends BlockParser { + name = "ComponentLibraryClasses"; + tag = /^<\s*LibraryClasses\s*>/gi; + start = undefined; + end = /(^<)|(^\})/gi; + type = Edk2SymbolType.dscComponentSubSection; + visible: boolean = true; + context: BlockParser[] = [ + new BlocklibraryDef(), + new BlockIncludes(), + ]; +} + +class BlockComponentSubPcds extends BlockParser { + name = "ComponentPcds"; + tag = /^<\s*Pcd[^>]*>/gi; + start = undefined; + end = /(^<)|(^\})/gi; + type = Edk2SymbolType.dscComponentSubSection; + visible: boolean = true; + context: BlockParser[] = [ + new BlockPcd(), + new BlockIncludes(), + ]; +} + +class BlockComponentSubBuildOptions extends BlockParser { + name = "ComponentBuildOptions"; + tag = /^<\s*BuildOptions\s*>/gi; + start = undefined; + end = /(^<)|(^\})/gi; + type = Edk2SymbolType.dscComponentSubSection; + visible: boolean = true; + context: BlockParser[] = [ + new BlockBuildOption(), + new BlockIncludes(), + ]; +} + class BlockComponentInf extends BlockParser { name= "ComponentInf"; tag= /^[\s\.\w\$\(\)_\-\\\/]*\.inf/gi; start= /.*?{/; end= /(^\})|(^\[)|(^[\s\.\w\$\(\)_\-\\\/]*\.inf)|(^\!include)/gi; type= Edk2SymbolType.dscModuleDefinition; + startContext= /\{/; + endContext= /^\s*\}/gi; visible:boolean = true; context: BlockParser[] = [ - new BlocklibraryDef(), - new BlockPcd(), + new BlockComponentSubLibraryClasses(), + new BlockComponentSubPcds(), + new BlockComponentSubBuildOptions(), + // new BlocklibraryDef(), + // new BlockPcd(), ]; } @@ -108,9 +161,22 @@ class BlockSimpleLine extends BlockParser { // Main sections // +class BlockBuildOptionsSection extends BlockParser { + name = "BuildOptions"; + tag = /\[\s*buildoptions.*?\]/gi; + start = undefined; + end = /^\[/gi; + type = Edk2SymbolType.dscBuildOptionsSection; + visible: boolean = true; + context: BlockParser[] = [ + new BlockBuildOption(), + new BlockIncludes(), + ]; +} + class BlockDefines extends BlockParser { name= "Defines"; - tag= /\[\s*(defines|buildoptions)\s*\]/gi; + tag= /\[\s*defines\s*\]/gi; start= undefined; end= /^\[/gi; type= Edk2SymbolType.dscSection; @@ -211,6 +277,7 @@ export class DscParser extends DocumentParser { blockParsers: BlockParser[] = [ new BlockDefines(), + new BlockBuildOptionsSection(), new BlockComponentsSection(), new BlockLibraryClasses(), new BlockSkuIds(), @@ -222,6 +289,7 @@ export class DscParser extends DocumentParser { new BlockComponentInf(true), new BlocklibraryDef(true), new BlockPcd(true), + new BlockBuildOption(true), ]; diff --git a/src/edkParser/infParser.ts b/src/edkParser/infParser.ts index b5bd3eb..9b00b53 100644 --- a/src/edkParser/infParser.ts +++ b/src/edkParser/infParser.ts @@ -1,8 +1,12 @@ -import { Edk2SymbolType } from "../symbols/symbolsType"; +import { debuglog } from "util"; +import { Edk2SymbolType, typeToStr } from "../symbols/symbolsType"; import { createRange, split } from "../utils"; import { REGEX_ANY_BUT_SECTION, REGEX_DEFINE } from "./commonParser"; import { BlockParser, DocumentParser } from "./languageParser"; +import { EdkSymbol } from "../symbols/edkSymbols"; +import * as vscode from 'vscode'; +import { DiagnosticManager, EdkDiagnosticCodes } from "../diagnostics"; class BlockModuleType extends BlockParser { name= "ModuleType"; @@ -163,6 +167,11 @@ class BlockGuid extends BlockParser { end = undefined; type = Edk2SymbolType.infGuid; visible:boolean = true; + diagnostic = async (docParser:DocumentParser, symbol:EdkSymbol)=>{ + if(!await symbol.isInDec(Edk2SymbolType.decGuid)){ + DiagnosticManager.error(docParser.document.uri, symbol.range, EdkDiagnosticCodes.statementNoKey, `No ${typeToStr.get(this.type)} found for ${symbol.name} in .dec files`); + } + }; } class BlockDepex extends BlockParser { @@ -196,6 +205,11 @@ class BlockProtocol extends BlockParser { end = undefined; type = Edk2SymbolType.infProtocol; visible:boolean = true; + diagnostic = async (docParser:DocumentParser, symbol:EdkSymbol)=>{ + if(!await symbol.isInDec(Edk2SymbolType.decProtocol)){ + DiagnosticManager.error(docParser.document.uri, symbol.range, EdkDiagnosticCodes.statementNoKey, `No ${typeToStr.get(this.type)} found for ${symbol.name} in .dec files`); + } + }; } class BlockProtocolsSection extends BlockParser { @@ -218,6 +232,11 @@ class BlockPpi extends BlockParser { end = undefined; type = Edk2SymbolType.infPpi; visible:boolean = true; + diagnostic = async (docParser:DocumentParser, symbol:EdkSymbol)=>{ + if(!await symbol.isInDec(Edk2SymbolType.decPpi)){ + DiagnosticManager.error(docParser.document.uri, symbol.range, EdkDiagnosticCodes.statementNoKey, `No ${typeToStr.get(this.type)} found for ${symbol.name} in .dec files`); + } + }; } class BlockPpisSection extends BlockParser { @@ -239,6 +258,11 @@ class BlockPcd extends BlockParser { end = undefined; type = Edk2SymbolType.infPcd; visible:boolean = true; + diagnostic = async (docParser:DocumentParser, symbol:EdkSymbol)=>{ + if(!await symbol.isInDec(Edk2SymbolType.decPcd)){ + DiagnosticManager.error(docParser.document.uri, symbol.range, EdkDiagnosticCodes.statementNoKey, `No ${typeToStr.get(this.type)} found for ${symbol.name} in .dec files`); + } + }; } class BlockPcdSection extends BlockParser { diff --git a/src/edkParser/languageParser.ts b/src/edkParser/languageParser.ts index 29c0ab5..3a70c84 100644 --- a/src/edkParser/languageParser.ts +++ b/src/edkParser/languageParser.ts @@ -20,6 +20,9 @@ export abstract class BlockParser { abstract visible: boolean; // Indicates if the block is visible or hidden exclusive: boolean = true; // Indicates if other blocks should parse this line isRoot: boolean = false; // Indicates if this block is in the root block of the document + diagnostic: undefined | ((docParser:DocumentParser, symbol:EdkSymbol) => Promise) = undefined; + startContext: RegExp | undefined; // When matched during start phase, endContext replaces end for block termination + endContext: RegExp | undefined; // End pattern used when startContext was matched constructor(isRoot: boolean = false) { this.isRoot = isRoot; @@ -44,8 +47,10 @@ export abstract class BlockParser { return false; } + + if (textLine.match(this.tag)) { - gDebugLog.verbose(`New block ${this.name} (${this.tag.toString()} -> ${textLine})`); + gDebugLog.trace(`New block ${this.name} (${this.tag.toString()} -> ${textLine})`); // Check if symbol is root definition if (this.isRoot && !docParser.isInRoot()) { @@ -71,6 +76,19 @@ export abstract class BlockParser { docParser.addSymbol(symbol); docParser.pushSymbolStack(symbol); + + if (this.diagnostic) { + setTimeout(async () => { + await this.diagnostic!(docParser, symbol); + }, 1); // Adjust the delay (in milliseconds) as needed + } + + // Check if context mode is active (startContext matched on the tag line) + let inContext = false; + if (this.startContext && textLine.match(this.startContext)) { + inContext = true; + } + // look for block start if (this.start) { // read lines until start tag is found @@ -86,6 +104,10 @@ export abstract class BlockParser { docParser.popSymbolStack(); return true; } + // Check startContext on lines scanned for block start + if (!inContext && this.startContext && textLine.match(this.startContext)) { + inContext = true; + } } } else { if (this.end === undefined) { @@ -94,6 +116,9 @@ export abstract class BlockParser { } } + // Use endContext instead of end when context mode is active + const activeEnd = inContext && this.endContext ? this.endContext : this.end; + // Parse block content while (docParser.hasPendingLines()) { // create a symbol @@ -108,23 +133,40 @@ export abstract class BlockParser { } // parse block + let contextMatched = false; const contextLength = this.context.length; for (let i = 0; i < contextLength; i++) { const blockContext = this.context[i]; - gDebugLog.verbose(`Parse block: ${blockContext.name}`); + gDebugLog.trace(`Parse block: ${blockContext.name}`); // decrement line index as parser will get next line by default docParser.decrementLineIndex(); const isSymbolAdded = blockContext.parse(docParser); if (isSymbolAdded) { - gDebugLog.verbose("Added symbol"); + gDebugLog.trace("Added symbol"); + contextMatched = true; if (blockContext.exclusive) { break; } } } + // If a context parser consumed lines, peek at the last consumed line + // to check if it also matches our endContext (child may have consumed the delimiter) + if (contextMatched && inContext && this.endContext) { + const peekIdx = docParser.lineIndex - 1; + if (peekIdx >= 0 && peekIdx < docParser.document.lineCount) { + const peekText = docParser.removeComment( + docParser.document.lineAt(peekIdx).text + ); + if (peekText.match(this.endContext)) { + docParser.popSymbolStack(); + return true; + } + } + } + // Check the end tag - if (this.end && textLine.match(this.end)) { + if (activeEnd && textLine.match(activeEnd)) { docParser.popSymbolStack(); return true; } @@ -445,7 +487,7 @@ addSymbol(symbol: EdkSymbol) { this.decrementLineIndex(); let parserResult = block.parse(this); if (parserResult) { - if (block.isRoot) { continue; } + if (block.isRoot || (block.start === undefined && block.end === undefined)) { continue; } parseIndex = -1; // reset index if (this.lineIndex >= this.document.lineCount) { return; diff --git a/src/edkParser/parserFactory.ts b/src/edkParser/parserFactory.ts index 89d1596..8592184 100644 --- a/src/edkParser/parserFactory.ts +++ b/src/edkParser/parserFactory.ts @@ -7,46 +7,89 @@ import { DecParser } from './decParser'; import { VfrParser } from './vfrParser'; import { AslParser } from './aslParser'; import { openTextDocument } from '../utils'; +import { DiagnosticManager } from '../diagnostics'; +import { Debouncer } from '../debouncer'; +import { DocumentParser } from './languageParser'; -export async function getParser(uri:vscode.Uri){ - let infDocument = await openTextDocument(uri); - let factory = new ParserFactory(); - let parser = factory.getParser(infDocument); +interface ParserCacheEntry { + parser: DocumentParser; + mtime: number; +} + +const parserCache = new Map(); + +export function invalidateParserCache(uri: vscode.Uri): void { + parserCache.delete(uri.toString()); +} + +export function clearParserCache(): void { + parserCache.clear(); +} + +/** + * Instantiates the appropriate parser for the given document based on its language ID. + * Returns `undefined` for unsupported language types. + */ +function createParserInstance(document: vscode.TextDocument): DocumentParser | undefined { + switch (document.languageId) { + case "asl": return new AslParser(document); + case "edk2_dsc": return new DscParser(document); + case "edk2_dec": return new DecParser(document); + case "edk2_vfr": return new VfrParser(document); + case "edk2_fdf": return new FdfParser(document); + case "edk2_inf": return new InfParser(document); + case "edk2_uni": return undefined; + default: + gDebugLog.error(`Document parser not supported: ${document.fileName}`); + return undefined; + } +} + +/** + * Returns a parsed `DocumentParser` for the given `TextDocument`. + * Results are cached by URI + mtime; if the file has not changed since the last + * parse the cached instance is returned immediately without re-parsing. + */ +export async function getParserForDocument(document: vscode.TextDocument): Promise { + const uri = document.uri; + const key = uri.toString(); + + // Retrieve the file modification time + let mtime: number | undefined; + try { + const stat = await vscode.workspace.fs.stat(uri); + mtime = stat.mtime; + } catch { + // File may not exist on disk (e.g. untitled); skip caching + } + + // Return cached parser when the file has not been modified + if (mtime !== undefined) { + const cached = parserCache.get(key); + if (cached && cached.mtime === mtime) { + gDebugLog.trace(`Parser cache hit: ${uri.fsPath}`); + return cached.parser; + } + } + + const parser = createParserInstance(document); if (parser) { await parser.parseFile(); + + // Store in cache only when we have a valid mtime + if (mtime !== undefined) { + parserCache.set(key, { parser, mtime }); + } } return parser; } -export class ParserFactory { - - getParser(document: vscode.TextDocument) { - let languageId = document.languageId; - switch (languageId) { - case "asl": - return new AslParser(document); - break; - case "edk2_dsc": - return new DscParser(document); - break; - case "edk2_dec": - return new DecParser(document); - break; - case "edk2_vfr": - return new VfrParser(document); - break; - case "edk2_fdf": - return new FdfParser(document); - break; - case "edk2_inf": - return new InfParser(document); - break; - case "edk2_uni": - break; - default: - gDebugLog.error(`Document parser not supported: ${document.fileName}`); - return undefined; - } - } +/** + * Convenience wrapper: opens the file at `uri` and returns its cached/parsed + * `DocumentParser`. Delegates to `getParserForDocument` after opening the document. + */ +export async function getParser(uri: vscode.Uri): Promise { + const document = await openTextDocument(uri); + return getParserForDocument(document); } diff --git a/src/edkParser/vfrParser.ts b/src/edkParser/vfrParser.ts index 3754537..9fc6155 100644 --- a/src/edkParser/vfrParser.ts +++ b/src/edkParser/vfrParser.ts @@ -117,8 +117,8 @@ class BlockNumericSection extends BlockParser { class BlockGotoSection extends BlockParser { name = "Goto"; tag = /\bgoto\b/gi; - start = undefined; - end = undefined; + start = undefined; + end = undefined; type = Edk2SymbolType.vfrGoto; visible:boolean = true; context: BlockParser[] = [ @@ -141,7 +141,7 @@ export class VfrParser extends DocumentParser { new BlockStringSection(), new BlockPasswordSection(), new BlockNumericSection(), - new BlockGotoSection(), + new BlockGotoSection(true), ]; } diff --git a/src/extension.ts b/src/extension.ts index 0eaf912..0d09eb6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; +import * as path from 'path'; import { ConfigAgent } from './configuration'; import { Cscope, CscopeAgent } from './cscope'; @@ -8,20 +9,24 @@ import { DebugLog } from './debugLog'; import * as edkStatusBar from './statusBar'; import { FileUseWarning } from './usedFileTracker'; import * as cmds from "./contextState/cmds"; -import { GrayoutController } from './grayout'; + import { initLanguages } from './Languages/languages'; import { ModuleReport } from './moduleReport'; import { GuidProvider } from './Languages/guidProvider'; import { PathFind } from './pathfind'; import { EdkWorkspaces } from './index/edkWorkspace'; import { Edk2CallHierarchyProvider } from './callHiearchy'; -import { copyToClipboard, getCurrentDocument, gotoFile, showVirtualFile } from './utils'; -import { ParserFactory } from './edkParser/parserFactory'; -import { TreeDetailsDataProvider } from './TreeDataProvider'; +import { copyToClipboard, findClosestCommonDirectory, getCurrentDocument, getDocsUrl, gotoFile, showVirtualFile } from './utils'; +import { getParserForDocument } from './edkParser/parserFactory'; import { DiagnosticManager } from './diagnostics'; +import { WorkspaceTreeProvider, WorkspaceRootItem, IncludeTreeItem, DocumentSymbolItem, WorkspaceTreeNode, isFileInWorkspaceTree, isInfInWorkspaces } from './workspaceTree/WorkspaceTreeProvider'; +import { InfDsc } from './index/edkWorkspace'; import { MapFilesManager } from './mapParser'; import { CompileCommands } from './compileCommands'; -import { TreeItem } from './treeElements/TreeItem'; +import { compileCFile } from './compileFile'; +import { showReleaseNotes } from './newVersionPage/newVersionMessage'; +import { startMcpServer, stopMcpServer } from './mcp/mcpServer'; +import { DriverInfoTreeProvider } from './driverInfoTree/DriverInfoTreeProvider'; // Global variables @@ -39,13 +44,15 @@ var gEdk2CallHierarchyProvider: Edk2CallHierarchyProvider; export var gDebugLog: DebugLog; export var gFileUseWarning: FileUseWarning; -export var gGrayOutController: GrayoutController; + export var gModuleReport: ModuleReport; export var gGuidProvider:GuidProvider; export var gDiagnosticManager:DiagnosticManager; -export var edkLensTreeDetailProvider: TreeDetailsDataProvider; -export var edkLensTreeDetailView: vscode.TreeView; +export var edkWorkspaceTreeProvider: WorkspaceTreeProvider; +export var edkWorkspaceTreeView: vscode.TreeView; + +export var gDriverInfoTreeProvider: DriverInfoTreeProvider; export var gMapFileManager: MapFilesManager; @@ -54,11 +61,10 @@ export var gMapFileManager: MapFilesManager; // your extension is activated the very first time the command is executed export async function activate(context: vscode.ExtensionContext) { - - - if (vscode.workspace.workspaceFolders !== undefined) { - gWorkspacePath = vscode.workspace.workspaceFolders[0].uri.fsPath; + let workspacePaths = vscode.workspace.workspaceFolders.map(folder => folder.uri.fsPath); + gWorkspacePath = findClosestCommonDirectory(workspacePaths); + console.log(gWorkspacePath); }else{ return; } @@ -74,42 +80,81 @@ export async function activate(context: vscode.ExtensionContext) { var commands = [ vscode.commands.registerCommand('edk2code.rebuildIndex', async ()=>{await cmds.rebuildIndexDatabase();}), + vscode.commands.registerCommand('edk2code.useDiscoveredBuildFolders', async ()=>{await cmds.useDiscoveredBuildFolders();}), + vscode.commands.registerCommand('edk2code.discoverBuildFolders', async ()=>{await cmds.discoverBuildFolders();}), vscode.commands.registerCommand('edk2code.openConfigurationUi', async ()=>{await cmds.openWpConfigGui();}), vscode.commands.registerCommand('edk2code.openConfigurationJson', async ()=>{await cmds.openWpConfigJson();}), vscode.commands.registerCommand('edk2code.rescanIndex', async ()=>{await cmds.rescanIndex();}), + vscode.commands.registerCommand('edk2code.unloadWorkspace', async ()=>{await cmds.unloadWorkspace();}), + vscode.commands.registerCommand('edk2code.help', async ()=>{ + const docUrl = getDocsUrl(); + await vscode.env.openExternal(vscode.Uri.parse(docUrl)); + }), vscode.commands.registerCommand('edk2code.openFile', async ()=>{await cmds.openFile();}), vscode.commands.registerCommand('edk2code.openLib', async()=>{await cmds.openLib();}), vscode.commands.registerCommand('edk2code.openModule', async ()=>{await cmds.openModule();}), vscode.commands.registerCommand('edk2code.gotoDefinition', async (fileUri)=>{ await cmds.gotoDefinitionCscope(fileUri);}), vscode.commands.registerCommand('edk2code.gotoDefinitionInput', ()=>{cmds.gotoDefinitionInput();}), - // wrong - // vscode.commands.registerCommand('edk2code.searchCcode', ()=>{throw new Error("ContextState not initialized");}), - - vscode.commands.registerCommand('edk2code.gotoInf',async (fileUri)=>{await cmds.gotoInf(fileUri);}), vscode.commands.registerCommand('edk2code.dscUsage', async (fileUri)=>{await cmds.gotoDscDeclaration(fileUri);}), vscode.commands.registerCommand('edk2code.dscInclusion', async (fileUri)=>{await cmds.gotoDscInclusion(fileUri);}), - vscode.commands.registerCommand('edk2code.references', async (fileUri)=>{await cmds.showReferenceStack(fileUri);}), - - vscode.commands.registerCommand('edk2code.libUsage', async (editor)=>{await cmds.showLibUsage(editor.document.uri);}), - vscode.commands.registerCommand('edk2code.showReferences', ()=>{cmds.showReferences();}), - vscode.commands.registerTextEditorCommand('edk2code.showLibraryTree', async (editor)=>{await cmds.showEdkMap(editor.document.uri);}), + vscode.commands.registerCommand('edk2code.buildEdk2Workspace', async ()=>{await cmds.buildEdk2Workspace();}), + vscode.commands.registerCommand('edk2code.buildFromTree', async (node)=>{await cmds.buildFromTree(node);}), // Internal vscode.commands.registerCommand('edk2code.searchDefinition', ()=>{}), vscode.commands.registerCommand("edk2code.gotoFile", async (fileUri, selRange)=>{await gotoFile(fileUri,selRange);}), + + vscode.commands.registerCommand('edk2code.driverInfoGoToDefinition', async (symbol: any) => { + if (symbol && symbol.onDefinition) { + const locations = await symbol.onDefinition(symbol.parser); + if (locations) { + const locArray = Array.isArray(locations) ? locations : [locations]; + if (locArray.length > 0) { + const loc = locArray[0]; + const suppressed = gDriverInfoTreeProvider.suppressNextUpdate(); + if (loc.uri && loc.range) { + await gotoFile(loc.uri, loc.range); + } else if (loc.uri) { + await vscode.commands.executeCommand('vscode.open', loc.uri, { preview: suppressed }); + } + if (!suppressed) { + // Double-click: force update + const editor = vscode.window.activeTextEditor; + if (editor) { await gDriverInfoTreeProvider.onEditorChanged(editor); } + } + } + } + } + }), + + vscode.commands.registerCommand('edk2code.driverInfoOpenFile', async (fileUri: vscode.Uri, options?: any) => { + const suppressed = gDriverInfoTreeProvider.suppressNextUpdate(); + const openOptions = suppressed ? { ...options, preview: true } : { ...options, preview: false }; + await vscode.commands.executeCommand('vscode.open', fileUri, openOptions); + if (!suppressed) { + // Double-click: force update + const editor = vscode.window.activeTextEditor; + if (editor) { await gDriverInfoTreeProvider.onEditorChanged(editor); } + } + }), + + vscode.commands.registerCommand('edk2code.driverInfoGotoDsc', async () => { + const infUri = gDriverInfoTreeProvider.currentInfUri; + if (infUri) { + await cmds.gotoDscDeclaration(infUri); + } + }), // vscode.commands.registerCommand("edk2code.viewWarnings", async ()=>{await gErrorReportAgent.reportErrors();}), // Debug vscode.commands.registerCommand('edk2code.debugCommand', async ()=>{ - let factory = new ParserFactory(); let doc = getCurrentDocument()!; - let parser = factory.getParser(doc); - await parser?.parseFile(); + let parser = await getParserForDocument(doc); let content = ""; for (const symb of parser?.symbolsList!) { content += `${symb.toString()}\n`; @@ -117,50 +162,187 @@ export async function activate(context: vscode.ExtensionContext) { await showVirtualFile(doc.fileName,content); }), - vscode.commands.registerCommand('edk2code.copyTreeData', () => { - // Your export logic here - let strTree = edkLensTreeDetailProvider.toString(); - void vscode.env.clipboard.writeText(strTree); - void vscode.window.showInformationMessage('Details copied to clipboard.'); - - }), - vscode.commands.registerCommand('edk2code.expandAllTree', () => { - // Your export logic here - let strTree = edkLensTreeDetailProvider.expandAll(edkLensTreeDetailView); - }), - vscode.commands.registerCommand('edk2code.focusOnNode', async (node:TreeItem) => { - await cmds.focusOnNode(node); - }), + vscode.commands.registerCommand('edk2code.copyWorkspaceTree', async () => { + const text = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Serializing workspace tree', + cancellable: false + }, + async progress => { + progress.report({ message: 'Building tree text for the clipboard...' }); + return await edkWorkspaceTreeProvider.serializeTree(); + } + ); + await vscode.env.clipboard.writeText(text); + void vscode.window.showInformationMessage('Workspace tree copied to clipboard.'); + }), + + vscode.commands.registerCommand('edk2code.copyWorkspaceNodePath', async (node: WorkspaceRootItem | IncludeTreeItem | DocumentSymbolItem) => { + if (!('nodePath' in node)) { + return; + } + await vscode.env.clipboard.writeText(node.nodePath); + void vscode.window.showInformationMessage(`Workspace path copied to clipboard`); + }), + + vscode.commands.registerCommand('edk2code.filterWorkspaceSymbols', async () => { + await edkWorkspaceTreeProvider.showFilterPicker(); + }), - vscode.commands.registerCommand('edk2code.NodeFocusBack', async () => { - await cmds.nodeFocusBack(); - }), + vscode.commands.registerCommand('edk2code.gotoOverwrite', async (node: DocumentSymbolItem) => { + if (node?.overwrittenBy) { + await gotoFile(node.overwrittenBy.uri, node.overwrittenBy.range); + await edkWorkspaceTreeProvider.revealLocation( + node.overwrittenBy.uri, + node.overwrittenBy.range.start, + edkWorkspaceTreeView + ); + } + }), + vscode.commands.registerCommand('edk2code.refreshWorkspaceConfig', async () => { + await gEdkWorkspaces.loadConfig(); + }), - vscode.commands.registerCommand('edk2code.getItemTreePath', (node:TreeItem) => { - // Your export logic here - let itemParent = node.getParent(); - let pathStack = [node]; - while(itemParent){ - pathStack.push(itemParent); - itemParent = itemParent.getParent(); + vscode.commands.registerCommand('edk2code.selectWorkspaceView', async () => { + const workspaces = gEdkWorkspaces.workspaces; + if (workspaces.length === 0) { + void vscode.window.showInformationMessage('No EDK2 workspaces loaded yet.'); + return; } - let path = ""; - let count = 0; - for (const pathItem of pathStack.reverse()) { - path += " ".repeat(count) + pathItem.toString() + "\n"; - count++; + const items = workspaces.map((ws, i) => ({ + label: ws.platformName ?? path.basename(ws.mainDsc.fsPath), + description: vscode.workspace.asRelativePath(ws.mainDsc, false), + index: i + })); + const picked = await vscode.window.showQuickPick(items, { + placeHolder: 'Select workspace to display', + title: 'EDK2: Select Workspace' + }); + if (picked !== undefined) { + edkWorkspaceTreeProvider.selectWorkspace(picked.index); } - void copyToClipboard(path, "Path copied to clipboard"); - }), + }), + + vscode.commands.registerCommand('edk2code.revealEditorInWorkspaceTree', async () => { + await edkWorkspaceTreeProvider.revealActiveEditor(edkWorkspaceTreeView); + }), + + vscode.commands.registerCommand('edk2code.searchWorkspaceTree', async () => { + await edkWorkspaceTreeProvider.searchTree(edkWorkspaceTreeView); + }), + + vscode.commands.registerCommand('edk2code.clearWorkspaceTreeSearch', () => { + edkWorkspaceTreeProvider.clearSearchFilter(); + }), + vscode.commands.registerCommand('edk2code.focusEditorInWorkspaceView', async () => { + const editor = vscode.window.activeTextEditor; + if (!editor) { return; } + + const langId = editor.document.languageId; + + // Helper: switch to the workspace that contains the given URI if it's + // not the currently displayed one. Returns false if user cancelled. + async function ensureWorkspaceForUri(uri: vscode.Uri): Promise { + const allWs = gEdkWorkspaces.workspaces; + const currentIdx = edkWorkspaceTreeProvider.activeIndex; + // Already showing the right workspace? + if (currentIdx < allWs.length && isFileInWorkspaceTree(uri, [allWs[currentIdx]])) { + return true; + } + // Search other workspaces + for (let i = 0; i < allWs.length; i++) { + if (i === currentIdx) { continue; } + if (isFileInWorkspaceTree(uri, [allWs[i]])) { + const wsName = allWs[i].platformName ?? path.basename(allWs[i].mainDsc.fsPath); + const answer = await vscode.window.showInformationMessage( + `File not found in the current workspace tree. Switch to "${wsName}"?`, + { modal: false }, + 'Switch' + ); + if (answer !== 'Switch') { return false; } + edkWorkspaceTreeProvider.selectWorkspace(i); + return true; + } + } + return true; + } + + // DSC / DSC-include: reveal directly by cursor position + if (langId === 'edk2_dsc') { + if (!await ensureWorkspaceForUri(editor.document.uri)) { return; } + await edkWorkspaceTreeProvider.revealActiveEditor(edkWorkspaceTreeView); + return; + } + // INF: first find DSC declaration(s), then reveal that location + if (langId === 'edk2_inf') { + const fileUri = editor.document.uri; + const wps = await gEdkWorkspaces.getWorkspace(fileUri); + + let declarations: InfDsc[] = []; + for (const wp of wps) { + declarations = declarations.concat(await wp.getDscDeclaration(fileUri)); + } + + if (declarations.length === 0) { + void vscode.window.showInformationMessage('This INF file has no DSC declaration in the loaded workspaces.'); + return; + } + + let chosen: InfDsc; + if (declarations.length === 1) { + chosen = declarations[0]; + } else { + const items = declarations.map(d => ({ + label: vscode.workspace.asRelativePath(d.location.uri, false), + description: `line ${d.location.range.start.line + 1}`, + detail: d.text.trim(), + decl: d + })); + const picked = await vscode.window.showQuickPick(items, { + placeHolder: 'Multiple DSC declarations found – select one to reveal', + title: 'EDK2: Focus on workspace view' + }); + if (!picked) { return; } + chosen = picked.decl; + } + + if (!await ensureWorkspaceForUri(chosen.location.uri)) { return; } + await edkWorkspaceTreeProvider.revealLocation( + chosen.location.uri, + chosen.location.range.start, + edkWorkspaceTreeView + ); + } + }), + + vscode.commands.registerCommand('edk2code.compileCFile', async (fileUriOrItem?: vscode.Uri | vscode.TreeItem) => { + let uri: vscode.Uri | undefined; + if (fileUriOrItem instanceof vscode.Uri) { + uri = fileUriOrItem; + } else if (fileUriOrItem && 'resourceUri' in fileUriOrItem && fileUriOrItem.resourceUri) { + uri = fileUriOrItem.resourceUri; + } + await compileCFile(uri); + }), + vscode.commands.registerCommand('edk2code.startMcpServer', async () => { + const portStr = vscode.workspace.getConfiguration('edk2code').get('mcpServerPort', 3100); + await startMcpServer(portStr); + }), + vscode.commands.registerCommand('edk2code.stopMcpServer', () => { + stopMcpServer(); + }) ]; - context.subscriptions.concat(commands); + // We need to concat custom commands, because they are not in the list of commands + // because they are added after the extension is activated + context.subscriptions.push(...commands); gExtensionContext = context; edkStatusBar.init(context); @@ -170,38 +352,59 @@ export async function activate(context: vscode.ExtensionContext) { gMapFileManager = new MapFilesManager(); gCompileCommands = new CompileCommands(); + edkWorkspaceTreeProvider = new WorkspaceTreeProvider(); + edkWorkspaceTreeView = vscode.window.createTreeView('workspaceView', { treeDataProvider: edkWorkspaceTreeProvider, showCollapseAll: true, dragAndDropController: edkWorkspaceTreeProvider }); + + gDriverInfoTreeProvider = new DriverInfoTreeProvider(); + const driverInfoTreeView = vscode.window.createTreeView('driverInfoView', { treeDataProvider: gDriverInfoTreeProvider, showCollapseAll: true }); + gDriverInfoTreeProvider.setTreeView(driverInfoTreeView); + await gEdkWorkspaces.loadConfig(); gFileUseWarning = new FileUseWarning(); + // Start periodic scan for build folders in workspace + cmds.startBuildFolderScan(); + gCscope = new Cscope(); gCscopeAgent = new CscopeAgent(); - if(gCscope.existCscopeFile()){ - // eslint-disable-next-line @typescript-eslint/no-floating-promises - gCscope.reload().then(()=>{ + if(gConfigAgent.getUseCscope() && gCscope.existCscopeFile()){ + void gCscope.reload().then(()=>{ if(gConfigAgent.getUseEdkCallHiearchy()){ gEdk2CallHierarchyProvider = new Edk2CallHierarchyProvider(); } }); } - - edkLensTreeDetailProvider = new TreeDetailsDataProvider(); - edkLensTreeDetailView = vscode.window.createTreeView('detailsView', { treeDataProvider: edkLensTreeDetailProvider, showCollapseAll:true }); - - edkLensTreeDetailProvider.refresh(); - edkLensTreeDetailView.onDidExpandElement(async event => { - let node = event.element as TreeItem; - await node.expand(); - edkLensTreeDetailProvider.refresh(); - }); - - await vscode.commands.executeCommand('setContext', 'edk2code.isNodeFocusBackStack', false); + + // ─── Track whether the active editor belongs to the workspace tree ───────── + async function updateEditorInWorkspaceContext(editor: vscode.TextEditor | undefined): Promise { + const uri = editor?.document.uri; + const langId = editor?.document.languageId; + + const inDscTree = + uri !== undefined && + isFileInWorkspaceTree(uri, gEdkWorkspaces.workspaces); + + const inInfWorkspace = + uri !== undefined && + langId === 'edk2_inf' && + isInfInWorkspaces(uri, gEdkWorkspaces.workspaces); + + void vscode.commands.executeCommand('setContext', 'edk2code.editorFileInWorkspaceTree', inDscTree); + void vscode.commands.executeCommand('setContext', 'edk2code.infFileInWorkspaceTree', inInfWorkspace); + } + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(editor => { void updateEditorInWorkspaceContext(editor); }) + ); + void updateEditorInWorkspaceContext(vscode.window.activeTextEditor); + + void showReleaseNotes(context); } @@ -211,7 +414,8 @@ export async function activate(context: vscode.ExtensionContext) { // this method is called when your extension is deactivated export async function deactivate() { - + stopMcpServer(); + cmds.stopBuildFolderScan(); } diff --git a/src/grayout.ts b/src/grayout.ts index 8c24de1..de08244 100644 --- a/src/grayout.ts +++ b/src/grayout.ts @@ -1,8 +1,121 @@ import * as vscode from 'vscode'; -import { getCurrentDocument } from './utils'; import { gDebugLog } from './extension'; +/** + * Manages grayout decorations for inactive code regions across all documents. + * + * Uses a single shared TextEditorDecorationType and a single set of event + * listeners instead of per-file controllers. This avoids: + * - Creating/disposing decoration types on every tab switch (main perf issue) + * - N event listeners for N files + * - Reference equality checks on TextDocument that silently fail + */ +export class GrayoutManager { + /** Single shared decoration type — created once, reused for all files */ + private decoration: vscode.TextEditorDecorationType; + + /** Map from fsPath -> grayout ranges for that file */ + private rangeMap: Map = new Map(); + + private disposables: vscode.Disposable[] = []; + + constructor() { + this.decoration = vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + light: { opacity: "0.3" }, + dark: { opacity: "0.3" }, + }); + + // Single listener: re-apply decorations when the active editor changes + this.disposables.push( + vscode.window.onDidChangeActiveTextEditor((editor) => { + if (editor) { + this.applyToEditor(editor); + } + }) + ); + + // Handle split editors and editor reuse + this.disposables.push( + vscode.window.onDidChangeVisibleTextEditors((editors) => { + for (const editor of editors) { + this.applyToEditor(editor); + } + }) + ); + } + + /** + * Store grayout ranges for a document and immediately apply decorations + * to all visible editors showing that document. + */ + setRanges(uri: vscode.Uri, ranges: vscode.Range[]) { + gDebugLog.trace(`GrayoutManager.setRanges(): ${uri.fsPath} (${ranges.length} ranges)`); + this.rangeMap.set(uri.fsPath, ranges); + this.applyToUri(uri.fsPath); + } + + /** + * Remove grayout ranges for a document and clear its decorations. + */ + clearRanges(uri: vscode.Uri) { + gDebugLog.trace(`GrayoutManager.clearRanges(): ${uri.fsPath}`); + this.rangeMap.delete(uri.fsPath); + this.applyToUri(uri.fsPath); + } + + /** + * Remove all grayout ranges and clear decorations from all visible editors. + */ + clearAll() { + gDebugLog.trace(`GrayoutManager.clearAll()`); + this.rangeMap.clear(); + // Clear decorations on all visible editors + for (const editor of vscode.window.visibleTextEditors) { + editor.setDecorations(this.decoration, []); + } + } + + /** + * Apply stored decorations to a specific editor. + */ + private applyToEditor(editor: vscode.TextEditor) { + const fsPath = editor.document.uri.fsPath; + const ranges = this.rangeMap.get(fsPath); + if (ranges && ranges.length > 0) { + gDebugLog.trace(`GrayoutManager: Applying ${ranges.length} ranges to ${fsPath}`); + editor.setDecorations(this.decoration, ranges); + } else { + // Clear any stale decorations for files not in the map + editor.setDecorations(this.decoration, []); + } + } + + /** + * Apply decorations to all visible editors showing the given file. + */ + private applyToUri(fsPath: string) { + for (const editor of vscode.window.visibleTextEditors) { + if (editor.document.uri.fsPath === fsPath) { + this.applyToEditor(editor); + } + } + } + + dispose() { + this.decoration.dispose(); + for (const d of this.disposables) { + d.dispose(); + } + this.disposables = []; + this.rangeMap.clear(); + } +} + +/** + * @deprecated Use GrayoutManager instead. Kept temporarily for backward compatibility. + */ export class GrayoutController { decoration:vscode.TextEditorDecorationType|undefined; @@ -15,7 +128,6 @@ export class GrayoutController { this.document = document; this.range = range; - // let subscriptions: vscode.Disposable[] = []; this.changeEvent = vscode.window.onDidChangeActiveTextEditor(()=>{ if(vscode.window.activeTextEditor?.document.uri.fsPath === this.document.uri.fsPath){ this.doGrayOut(); @@ -26,13 +138,13 @@ export class GrayoutController { grayoutRange(unusdedRanges:vscode.Range[]) { - gDebugLog.verbose("grayoutRange()"); + gDebugLog.trace("grayoutRange()"); let activeEditor = vscode.window.activeTextEditor; if(!activeEditor){return;} - if(activeEditor.document !== this.document){return;} + if(activeEditor.document.uri.fsPath !== this.document.uri.fsPath){return;} - gDebugLog.verbose(`Unused Ranges: ${JSON.stringify(unusdedRanges)}`); + gDebugLog.trace(`Unused Ranges: ${JSON.stringify(unusdedRanges)}`); this.disposeDecoration(); @@ -48,9 +160,6 @@ export class GrayoutController { this.decoration = decoration; - - - // Block content const blockDecorationOptions: vscode.DecorationOptions[] = []; for (const targetRange of unusdedRanges) { const decoration = { range: targetRange}; diff --git a/src/index/definitions.ts b/src/index/definitions.ts index c6a4b3f..5f39a90 100644 --- a/src/index/definitions.ts +++ b/src/index/definitions.ts @@ -57,8 +57,17 @@ export class WorkspaceDefinitions { return undefined; } + isBoolean(value:string){ + value = value.toLowerCase(); + return value === "true" || value === "false"; + } + setDefinition(key:string, value:string, location:vscode.Location|undefined){ - gDebugLog.verbose(`setDefinition: ${key} = ${value}`); + + // Expand any existing definitions referenced in the value + value = this.replaceDefines(value); + + gDebugLog.trace(`setDefinition: ${key} = ${value}`); this.defines.set(key, {name: key, value:value, location:location}); } @@ -66,6 +75,10 @@ export class WorkspaceDefinitions { this.defines = new Map(); } + isDefined(text:string){ + return this.defines.has(text); + } + replaceDefines(text: string) { let replaced = false; let maxIterations = 10; @@ -82,7 +95,17 @@ export class WorkspaceDefinitions { for (const [key,value] of this.defines.entries()) { if(text.includes(`$(${key})`)){ replaced = true; - text = text.replaceAll(`$(${key})`, value.value); + let replacement; + if(value.value.toLowerCase() === "true"){ + replacement = "TRUE"; + }else if(value.value.toLowerCase() === "false"){ + replacement = "FALSE"; + }else{ + replacement = value.value; + } + + + text = text.replaceAll(`$(${key})`, replacement); } } diff --git a/src/index/edkWorkspace.ts b/src/index/edkWorkspace.ts index e6d26f2..301e6e3 100644 --- a/src/index/edkWorkspace.ts +++ b/src/index/edkWorkspace.ts @@ -1,21 +1,31 @@ import * as vscode from 'vscode'; -import { gCompileCommands, gConfigAgent, gDebugLog, gMapFileManager, gPathFind, gWorkspacePath } from '../extension'; -import { GrayoutController } from '../grayout'; +import { edkWorkspaceTreeProvider, edkWorkspaceTreeView, gCompileCommands, gConfigAgent, gDebugLog, gMapFileManager, gPathFind, gWorkspacePath } from '../extension'; +import { GrayoutManager } from '../grayout'; import { createRange, openTextDocument, pathCompare, split } from '../utils'; -import { REGEX_DEFINE as REGEX_DEFINE, REGEX_DSC_SECTION, REGEX_INCLUDE as REGEX_INCLUDE, REGEX_LIBRARY_PATH, REGEX_MODULE_PATH, REGEX_PCD_LINE, REGEX_VAR_USAGE } from "../edkParser/commonParser"; +import { REGEX_DEFINE, REGEX_DSC_SECTION, REGEX_EQUAL, REGEX_INCLUDE as REGEX_INCLUDE, REGEX_LIBRARY_PATH, REGEX_MODULE_PATH, REGEX_PCD_LINE, REGEX_VAR_USAGE } from "../edkParser/commonParser"; import { UNDEFINED_VARIABLE, WorkspaceDefinitions } from "./definitions"; import * as fs from 'fs'; import path = require('path'); -import { ParserFactory, getParser } from '../edkParser/parserFactory'; +import { getParser } from '../edkParser/parserFactory'; import { Edk2SymbolType } from '../symbols/symbolsType'; import { EdkSymbolInfLibrary } from '../symbols/infSymbols'; import { DiagnosticManager, EdkDiagnosticCodes } from '../diagnostics'; import { PathFind } from '../pathfind'; +import { OPERATORS } from './operations'; +import * as edkStatusBar from '../statusBar'; +import { debuglog } from 'util'; const dscSectionTypes = ['defines','packages','buildoptions','skuids','libraryclasses','components','userextensions','defaultstores','pcdsfeatureflag','pcdsfixedatbuild','pcdspatchableinmodule','pcdsdynamicdefault','pcdsdynamichii','pcdsdynamicvpd','pcdsdynamicexdefault','pcdsdynamicexhii','pcdsdynamicexvpd']; +// EDK2 built-in macros that should not trigger undefined variable warnings +const EDK2_BUILTIN_MACROS = new Set([ + "WORKSPACE", "EDK_SOURCE", "EFI_SOURCE", "TARGET", "TOOL_CHAIN_TAG", + "ARCH", "MODULE_NAME", "OUTPUT_DIRECTORY", "BUILD_NUMBER", "INF_VERSION", + "NAMED_GUID", "INF_OUTPUT" +]); + type ConditionBlock = { active: boolean; // Whether the block is active or not satisfied: boolean; // Whether the condition has been satisfied @@ -26,12 +36,21 @@ type ConditonOpenBlock = { lineNo:number; }; -interface Pcd { +export interface Pcd { name:string; value:string position:vscode.Location } +export interface IncludeNode { + /** Location of the !include directive in the parent file */ + location: vscode.Location; + /** Resolved URI of the included file */ + uri: vscode.Uri; + /** Nested includes found inside the included file */ + children: IncludeNode[]; +} + export class SectionProperties{ properties: SectionProperty[] = []; constructor(){ @@ -274,11 +293,17 @@ async getWorkspace(uri: vscode.Uri): Promise { async loadConfig() { this.workspaces = []; - gDebugLog.verbose("Loading Configuration"); - // TODO: enable to get more commands available - //await vscode.commands.executeCommand('setContext', 'edk2code.parseComplete', false); + gDebugLog.trace("Loading Configuration"); + await vscode.commands.executeCommand('setContext', 'edk2code.parseComplete', false); + await vscode.commands.executeCommand('setContext', 'edk2code.isLoading', true); + + // If the previous workspace processing did not complete (e.g. + // after a clear), build a temporary T-tree so PathFind can + // resolve files without relying on the empty packagePaths. + await gConfigAgent.buildFileIndexIfNeeded(); + let dscPaths = gConfigAgent.getBuildDscPaths(); - gDebugLog.verbose(`dscPaths = ${dscPaths}`); + gDebugLog.trace(`dscPaths = ${dscPaths}`); for (const dscPath of dscPaths) { let dscDocument = await openTextDocument(vscode.Uri.file(path.join(gWorkspacePath, dscPath))); let edkWorkspace = new EdkWorkspace(dscDocument); @@ -288,7 +313,11 @@ async getWorkspace(uri: vscode.Uri): Promise { } gMapFileManager.load(); gCompileCommands.load(); - //await vscode.commands.executeCommand('setContext', 'edk2code.parseComplete', true); + // Workspace processing is complete – mark flag and dispose + // of the temporary T-tree so PathFind reverts to findFiles. + gConfigAgent.setWorkspaceProcessComplete(); + await vscode.commands.executeCommand('setContext', 'edk2code.parseComplete', true); + await vscode.commands.executeCommand('setContext', 'edk2code.isLoading', false); } } @@ -311,9 +340,12 @@ export class EdkWorkspace { private sectionsStack: string[] = []; private parsedDocuments: Map = new Map(); + + // Elements private defines: WorkspaceDefinitions = new WorkspaceDefinitions(); - definesFdf: WorkspaceDefinitions = new WorkspaceDefinitions(); private pcdDefinitions: Map> = new Map(); + + definesFdf: WorkspaceDefinitions = new WorkspaceDefinitions(); private libraryTypeTrack = new Map(); @@ -331,45 +363,39 @@ export class EdkWorkspace { public set filesModules(value: InfDsc[]) { this._filesModules = value; } - private _filesDsc: vscode.TextDocument[] = []; - private _filesFdf: vscode.TextDocument[] = []; + private _filesDsc: Set = new Set(); + private _filesFdf: Set = new Set(); - public get filesDsc(): vscode.TextDocument[] { + public get filesDsc(): Set { return this._filesDsc; } - public set filesDsc(value: vscode.TextDocument[]) { + public set filesDsc(value: Set) { this._filesDsc = value; } - public get filesFdf(): vscode.TextDocument[] { + public get filesFdf(): Set { return this._filesFdf; } - public set filesFdf(value: vscode.TextDocument[]) { + public set filesFdf(value: Set) { this._filesFdf = value; } - private _grayoutControllers:GrayoutController[] = []; + private _grayoutManager: GrayoutManager = new GrayoutManager(); - public updateGrayoutRange(document: vscode.TextDocument, range: vscode.Range[]){ - for (const grayoutController of this._grayoutControllers) { - - if(grayoutController.document.uri.fsPath === document.uri.fsPath){ - grayoutController.range = range; - grayoutController.doGrayOut(); - return; - } - } - const dscGrayoutController = new GrayoutController(document, range); - dscGrayoutController.doGrayOut(); - this._grayoutControllers.push(dscGrayoutController); + private _includeTree: IncludeNode[] = []; + public get includeTree(): IncludeNode[] { + return this._includeTree; + } + public updateGrayoutRange(document: vscode.TextDocument, range: vscode.Range[]){ + this._grayoutManager.setRanges(document.uri, range); } public dscList(){ - return this.filesDsc.map(x=>x.uri.fsPath); + return Array.from(this.filesDsc).map(x=>x.uri.fsPath); } public fdfList(){ - return this.filesFdf.map(x=>x.uri.fsPath); + return Array.from(this.filesFdf).map(x=>x.uri.fsPath); } public getFilesList(){ @@ -388,59 +414,80 @@ export class EdkWorkspace { async proccessWorkspace() { - if (this.workInProgress) { - return false; - } - // reset conditional stack - this.workInProgress = true; - gDebugLog.verbose(`Start finding defines in ${this.mainDsc.fsPath}`); - this.conditionalStack = []; - - this.defines.resetDefines(); - this.definesFdf.resetDefines(); - - this.filesLibraries = []; - this.filesModules = []; - this.filesDsc = []; - for (const ctrl of this._grayoutControllers) { - ctrl.dispose(); - } - this._grayoutControllers = []; - this.libraryTypeTrack = new Map(); - - - this.conditionStack = []; - this.result = []; - this.conditionOpen = []; - - - + return vscode.window.withProgress( + { location: { viewId: 'workspaceView' } }, + async () => { + return this._doProccessWorkspace(); + } + ); + } - + private async _doProccessWorkspace() { + try{ + if(this.platformName !== undefined){ + edkStatusBar.pushText(`Parsing ${this.platformName}`); + }else{ + edkStatusBar.pushText(`Loading ${this.mainDsc.fsPath}`); + } - this.parsedDocuments = new Map(); - let mainDscDocument = await vscode.workspace.openTextDocument(this.mainDsc); - await this._processDocument(mainDscDocument, "DSC"); - for (const conditionOpen of this.conditionOpen) { - DiagnosticManager.error(conditionOpen.uri,conditionOpen.lineNo,EdkDiagnosticCodes.conditionalMissform, "Condition block not closed"); - } - this.workInProgress = false; - gDebugLog.verbose("Finding done."); - - // Populate workspace definitions - this.platformName = this.defines.getDefinition("PLATFORM_NAME") || undefined; - let flashDefinitionString = this.defines.getDefinition("FLASH_DEFINITION") || undefined; - if (flashDefinitionString) { - let flashDefinitionPath = await gPathFind.findPath(flashDefinitionString); - if(flashDefinitionPath.length>0){ - this.flashDefinitionDocument = await openTextDocument(flashDefinitionPath[0].uri); + edkStatusBar.setWorking(); + if (this.workInProgress) { + return false; + } + // reset conditional stack + this.workInProgress = true; + gDebugLog.trace(`Start finding defines in ${this.mainDsc.fsPath}`); + this.conditionalStack = []; + + this.defines.resetDefines(); + this.definesFdf.resetDefines(); + + this.filesLibraries = []; + this.filesModules = []; + this.filesDsc = new Set(); + this._grayoutManager.clearAll(); + this.libraryTypeTrack = new Map(); + + + this.conditionStack = []; + this.result = []; + this.conditionOpen = []; + this._includeTree = []; + + + + + + + this.parsedDocuments = new Map(); + let mainDscDocument = await vscode.workspace.openTextDocument(this.mainDsc); + await this._processDocument(mainDscDocument, "DSC"); + for (const conditionOpen of this.conditionOpen) { + DiagnosticManager.error(conditionOpen.uri,conditionOpen.lineNo,EdkDiagnosticCodes.conditionalMissform, "Condition block not closed"); + } + this.workInProgress = false; + gDebugLog.trace("Finding done."); + + // Populate workspace definitions + this.platformName = this.defines.getDefinition("PLATFORM_NAME") || undefined; + let flashDefinitionString = this.defines.getDefinition("FLASH_DEFINITION") || undefined; + if (flashDefinitionString) { + let flashDefinitionPath = await gPathFind.findPath(flashDefinitionString, path.dirname(this.mainDsc.fsPath)); + if(flashDefinitionPath.length>0){ + this.flashDefinitionDocument = await openTextDocument(flashDefinitionPath[0].uri); + } } + + await this.findDefinesFdf(); + + this.processComplete = true; + edkWorkspaceTreeProvider.refresh(); + return true; + }finally{ + edkStatusBar.popText(); + edkStatusBar.clearWorking(); } - - await this.findDefinesFdf(); - - this.processComplete = true; - return true; + } async getInfReference(uri: vscode.Uri) { @@ -500,6 +547,10 @@ export class EdkWorkspace { return this.pcdDefinitions.get(namespace); } + getAllPcds() { + return this.pcdDefinitions; + } + private stripComment(line: string): string { if (!line.includes("#")) { @@ -535,177 +586,247 @@ export class EdkWorkspace { } - private async _processDocument(document: vscode.TextDocument, type: 'DSC' | 'FDF') { - DiagnosticManager.clearProblems(document.uri); - gDebugLog.verbose(`_process${type}: ${document.fileName}`); + private async _processDocument(document: vscode.TextDocument, type: 'DSC' | 'FDF', parentIncludeNode?: IncludeNode) { - if (this.isDocumentInIndex(document)) { - gDebugLog.warning(`_process${type}: ${document.fileName} already in inactiveLines`); - return; - } - - let doucumentGrayoutRange = []; - this.parsedDocuments.set(document.uri.fsPath, []); - if (type === 'DSC') { - this.filesDsc.push(document); - } else { - this.filesFdf.push(document); - } - - let text = document.getText().split(/\r?\n/); - let lineIndex = -1; - let isRangeActive = false; - let unuseRangeStart = 0; - - gDebugLog.verbose(`# Parsing ${type} Document: ${document.uri.fsPath}`); - for (let line of text) { - lineIndex++; - gDebugLog.verbose(`\t\t${lineIndex}: ${line}`); - line = this.stripComment(line); - - if (line.length === 0){continue;} - - // PCDs - if (line.match(REGEX_PCD_LINE)) { - let [fullPcd, pcdValue] = split(line, "|", 2); - pcdValue = pcdValue.split("|")[0].trim(); - if (pcdValue.startsWith('L"')) { - pcdValue = pcdValue.slice(1); - } - let [pcdNamespace, pcdName] = split(fullPcd, ".", 2); - if (!this.pcdDefinitions.has(pcdNamespace)) { - this.pcdDefinitions.set(pcdNamespace, new Map()); - } - this.pcdDefinitions.get(pcdNamespace)?.set( - pcdName, - { - name: pcdName, - value: pcdValue, - position: new vscode.Location(document.uri, new vscode.Position(lineIndex, 0)) - } - ); - } - - line = this.defines.replaceDefines(line); - line = this.replacePcds(line); - let isInActiveCode = this.processConditional(line, lineIndex, document.uri); - - if (!isInActiveCode) { - if (!isRangeActive) { - isRangeActive = true; - unuseRangeStart = lineIndex; - } - continue; + DiagnosticManager.clearProblems(document.uri); + gDebugLog.trace(`_process${type}: ${document.fileName}`); + + if (this.isDocumentInIndex(document)) { + gDebugLog.trace(`_process${type}: ${document.fileName} already in inactiveLines`); + return; } - - if (isRangeActive) { - isRangeActive = false; - let lineIndexEnd = lineIndex - 1; - doucumentGrayoutRange.push(new vscode.Range(new vscode.Position(unuseRangeStart, 0), new vscode.Position(lineIndexEnd, 0))); + + let doucumentGrayoutRange = []; + this.parsedDocuments.set(document.uri.fsPath, []); + if (type === 'DSC') { + this.filesDsc.add(document); + } else { + this.filesFdf.add(document); } + + let text = document.getText().split(/\r?\n/); + let lineIndex = -1; + let isRangeActive = false; + let unuseRangeStart = 0; + + gDebugLog.trace(`# Parsing ${type} Document: ${document.uri.fsPath}`); + for (let line of text) { + lineIndex++; + + gDebugLog.trace(`\t\t${lineIndex}: ${line}`); + line = this.stripComment(line); + let originalLine = line; - if (type === 'DSC') { - // Sections - let match = line.match(REGEX_DSC_SECTION); - if (match) { - const sectionType = match[0].split(".")[0]; - if (!dscSectionTypes.includes(sectionType.toLowerCase())) { - DiagnosticManager.warning(document.uri, lineIndex, EdkDiagnosticCodes.unknownSectionType, sectionType); + if (line.length === 0){continue;} + + // PCDs + if (line.match(REGEX_PCD_LINE)) { + let [fullPcd, pcdValue] = split(line, "|", 2); + pcdValue = pcdValue.split("|")[0].trim(); + if (pcdValue.startsWith('L"')) { + pcdValue = pcdValue.slice(1); } - this.sectionsStack = [match[0]]; - continue; + let [pcdNamespace, pcdName] = split(fullPcd, ".", 2); + if (!this.pcdDefinitions.has(pcdNamespace)) { + this.pcdDefinitions.set(pcdNamespace, new Map()); + } + this.pcdDefinitions.get(pcdNamespace)?.set( + pcdName, + { + name: pcdName, + value: pcdValue, + position: new vscode.Location(document.uri, new vscode.Position(lineIndex, 0)) + } + ); } - } - - // Defines - if (line.match(REGEX_DEFINE)) { - let key = line.replace(/define/gi, "").trim(); - key = split(key, "=", 2)[0].trim(); - let value = split(line, "=", 2)[1].trim(); - if (value.includes(`$(${key})`)) { - gDebugLog.info(`Circular define: ${key}: ${value}`); - } else { - if (type === 'DSC') { - this.defines.setDefinition(key, value, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0))); - } else { - this.definesFdf.setDefinition(key, value, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0))); + + line = this.defines.replaceDefines(line); + line = this.replacePcds(line); + let isInActiveCode = this.processConditional(line, lineIndex, document.uri); + + if (!isInActiveCode) { + if (!isRangeActive) { + isRangeActive = true; + unuseRangeStart = lineIndex; } + continue; } - continue; - } - - // Includes - if (line.match(REGEX_INCLUDE)) { - let value = line.replace(/!include/gi, "").trim(); - let location = await gPathFind.findPath(value, document.uri.fsPath); - if (location.length > 0) { - let includedDocument = await openTextDocument(location[0].uri); - if (type === 'DSC') { - await this._processDocument(includedDocument, 'DSC'); - } else { - this.filesFdf.push(includedDocument); - await this._processDocument(includedDocument, 'FDF'); + + if(line.startsWith("!error ")){ + DiagnosticManager.error(document.uri, lineIndex, EdkDiagnosticCodes.errorMessage, line.replace("!error ","")); + } + + if (isRangeActive) { + isRangeActive = false; + let lineIndexEnd = lineIndex - 1; + gDebugLog.debug(`New grayout ${document.fileName} -> unuseRangeStart: ${unuseRangeStart} lineIndexEnd: ${lineIndexEnd}`); + doucumentGrayoutRange.push(new vscode.Range(new vscode.Position(unuseRangeStart, 0), new vscode.Position(lineIndexEnd, 0))); + } + + if (type === 'DSC') { + // Sections + let match = line.match(REGEX_DSC_SECTION); + if (match) { + const sectionType = match[0].split(".")[0]; + if (!dscSectionTypes.includes(sectionType.toLowerCase())) { + DiagnosticManager.warning(document.uri, lineIndex, EdkDiagnosticCodes.unknownSectionType, sectionType); + } + this.sectionsStack = [match[0]]; + continue; } } - continue; - } + + // Defines + if (line.match(REGEX_DEFINE) || + (this.sectionsStack.length > 0 && + this.sectionsStack[this.sectionsStack.length - 1].toLowerCase() === "defines" && + line.match(REGEX_EQUAL))) { + let key = line.replace(/define/gi, "").trim(); + key = split(key, "=", 2)[0].trim(); + let value = split(line, "=", 2)[1].trim(); + let originalValue = split(originalLine, "=", 2)[1].trim(); + + if (value.includes(`$(${key})`) || originalValue.includes(`$(${key})`)) { + gDebugLog.trace(`Circular define: ${key}: ${value}`); + } else { + // Warn if DEFINE is being redefined without referencing itself + let defines = type === 'DSC' ? this.defines : this.definesFdf; + if (defines.isDefined(key)) { + let previousLocation = defines.getDefinitionLocation(key); + let relatedInfo: vscode.DiagnosticRelatedInformation[] | undefined; + if (previousLocation) { + relatedInfo = [ + new vscode.DiagnosticRelatedInformation( + previousLocation, + `Previous definition of '${key}'` + ) + ]; + } + DiagnosticManager.warning( + document.uri, + lineIndex, + EdkDiagnosticCodes.duplicateDefine, + `'${key}' is redefined without referencing its previous value. Use $(${key}) to extend it.`, + [], + relatedInfo + ); + } - if (type === 'DSC') { + if (type === 'DSC') { + this.defines.setDefinition(key, value, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0))); + } else { + this.definesFdf.setDefinition(key, value, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0))); + } - // Libraries - let match = line.match(REGEX_LIBRARY_PATH); - if (match) { - let filePath = match[0].trim(); - let results = await gPathFind.findPath(filePath, document.uri.fsPath); - if (results.length === 0) { - DiagnosticManager.error(document.uri, lineIndex, EdkDiagnosticCodes.missingPath, filePath); + // Warn about undefined variables directly referenced in the raw value + + let unresolvedVars = originalValue.match(REGEX_VAR_USAGE); + if (unresolvedVars) { + let defines = type === 'DSC' ? this.defines : this.definesFdf; + for (const unresolved of new Set(unresolvedVars)) { + let varName = unresolved.replace(/\$\(\s*/, '').replace(/\s*\)/, ''); + if (!defines.isDefined(varName) && !EDK2_BUILTIN_MACROS.has(varName.toUpperCase())) { + DiagnosticManager.warning(document.uri, lineIndex, EdkDiagnosticCodes.undefinedVariable, `${unresolved}`); + } + } + } } - let newLibDefinition = new InfDsc(filePath, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0)), this.sectionsStack[this.sectionsStack.length - 1], line); - const libName = newLibDefinition.text.split("|")[0].trim(); - const libNameTag = libName + " - " + newLibDefinition.getModuleTypeStr(); - if (this.libraryTypeTrack.has(libNameTag) && (libName.toLocaleLowerCase() !== "null") && newLibDefinition.parent === undefined) { - if (this.sectionsStack[this.sectionsStack.length - 1].toLowerCase().endsWith(".inf")) { - continue; + continue; + } + + // Includes + if (line.match(REGEX_INCLUDE)) { + let value = line.replace(/!include/gi, "").trim(); + let location = await gPathFind.findPath(value, document.uri.fsPath); + if (location.length > 0) { + gDebugLog.trace(`START Including: ${location[0].uri.fsPath}`); + const includeNode: IncludeNode = { + location: new vscode.Location(document.uri, new vscode.Position(lineIndex, 0)), + uri: location[0].uri, + children: [] + }; + if (parentIncludeNode) { + parentIncludeNode.children.push(includeNode); + } else { + this._includeTree.push(includeNode); } - let previousLibDefinition = this.libraryTypeTrack.get(libNameTag)!; - DiagnosticManager.warning(previousLibDefinition.location.uri, previousLibDefinition.location.range.start.line, - EdkDiagnosticCodes.duplicateStatement, - `Library overwritten: ${libName}`, - [vscode.DiagnosticTag.Unnecessary], - [new vscode.DiagnosticRelatedInformation(newLibDefinition.location, "New definition")]); - const index = this.filesLibraries.indexOf(previousLibDefinition); - if (index > -1) { - this.filesLibraries[index] = newLibDefinition; + let includedDocument = await openTextDocument(location[0].uri); + if (type === 'DSC') { + await this._processDocument(includedDocument, 'DSC', includeNode); + } else { + this.filesFdf.add(includedDocument); + await this._processDocument(includedDocument, 'FDF', includeNode); } - } else { - this.filesLibraries.push(newLibDefinition); + gDebugLog.trace(`END Including: ${location[0].uri.fsPath}`); } - this.libraryTypeTrack.set(libNameTag, newLibDefinition); continue; } - - // Modules - match = line.match(REGEX_MODULE_PATH); - if (match) { - let filePath = match[0].trim(); - let results = await gPathFind.findPath(filePath, document.uri.fsPath); - if (results.length === 0) { - DiagnosticManager.error(document.uri, lineIndex, EdkDiagnosticCodes.missingPath, filePath); + + if (type === 'DSC') { + + // Libraries + let match = line.match(REGEX_LIBRARY_PATH); + if (match) { + let filePath = match[0].trim(); + let results = await gPathFind.findPath(filePath, document.uri.fsPath); + if (results.length === 0) { + DiagnosticManager.error(document.uri, lineIndex, EdkDiagnosticCodes.missingPath, filePath); + } + let newLibDefinition = new InfDsc(filePath, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0)), this.sectionsStack[this.sectionsStack.length - 1], line); + const libName = newLibDefinition.text.split("|")[0].trim(); + const libNameTag = libName + " - " + newLibDefinition.getModuleTypeStr(); + if (this.libraryTypeTrack.has(libNameTag) && (libName.toLocaleLowerCase() !== "null") && newLibDefinition.parent === undefined) { + if (this.sectionsStack[this.sectionsStack.length - 1].toLowerCase().endsWith(".inf")) { + continue; + } + let previousLibDefinition = this.libraryTypeTrack.get(libNameTag)!; + DiagnosticManager.warning(previousLibDefinition.location.uri, previousLibDefinition.location.range.start.line, + EdkDiagnosticCodes.duplicateStatement, + `Library overwritten: ${libName}`, + [vscode.DiagnosticTag.Unnecessary], + [new vscode.DiagnosticRelatedInformation(newLibDefinition.location, "New definition")]); + const index = this.filesLibraries.indexOf(previousLibDefinition); + if (index > -1) { + this.filesLibraries[index] = newLibDefinition; + } + } else { + this.filesLibraries.push(newLibDefinition); + } + this.libraryTypeTrack.set(libNameTag, newLibDefinition); + continue; } - let inf = new InfDsc(filePath, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0)), this.sectionsStack[0], line); - if (filePath.toLowerCase().endsWith(".inf")) { - if (this.sectionsStack[this.sectionsStack.length - 1].toLowerCase().endsWith(".inf")) { - this.sectionsStack.pop(); + + // Modules + match = line.match(REGEX_MODULE_PATH); + if (match) { + let filePath = match[0].trim(); + let results = await gPathFind.findPath(filePath, document.uri.fsPath); + if (results.length === 0) { + DiagnosticManager.error(document.uri, lineIndex, EdkDiagnosticCodes.missingPath, filePath); } + let inf = new InfDsc(filePath, new vscode.Location(document.uri, new vscode.Position(lineIndex, 0)), this.sectionsStack[0], line); + if (filePath.toLowerCase().endsWith(".inf")) { + if (this.sectionsStack[this.sectionsStack.length - 1].toLowerCase().endsWith(".inf")) { + this.sectionsStack.pop(); + } + } + this.sectionsStack.push(filePath); + this.filesModules.push(inf); + continue; } - this.sectionsStack.push(filePath); - this.filesModules.push(inf); - continue; } } - } - - this.updateGrayoutRange(document, doucumentGrayoutRange); + + if (isRangeActive) { + isRangeActive = false; + let lineIndexEnd = lineIndex - 1; + gDebugLog.debug(`New grayout ${document.fileName} -> unuseRangeStart: ${unuseRangeStart} lineIndexEnd: ${lineIndexEnd}`); + doucumentGrayoutRange.push(new vscode.Range(new vscode.Position(unuseRangeStart, 0), new vscode.Position(lineIndexEnd, 0))); + } + + this.parsedDocuments.set(document.uri.fsPath, doucumentGrayoutRange); + this.updateGrayoutRange(document, doucumentGrayoutRange); + } private getLangId(uri:vscode.Uri){ @@ -755,21 +876,23 @@ export class EdkWorkspace { switch (this.getLangId(uri)) { case "edk2_dsc": + case "edk2_fdf": + // fdf files can be included in dsc files for (const dsc of this.filesDsc) { if(uri.fsPath === dsc.fileName) { return true; } } - return false; - case "edk2_fdf": + + for (const fdf of this.filesFdf) { if(uri.fsPath === fdf.fileName){ return true; } } - break; + return false; case "edk2_dec": break; case "edk2_inf": @@ -782,7 +905,7 @@ export class EdkWorkspace { if(uri.fsPath.includes(lib.path)){ return true; } - } + }`` return false; case "c": @@ -801,16 +924,16 @@ export class EdkWorkspace { } } for (const inf of relativeInf) { - let location = await gPathFind.findPath(inf); + let infLocation = await gPathFind.findPath(inf); - if (location.length === 0){continue;} + if (infLocation.length === 0){continue;} - let parser = await getParser(location[0].uri); + let parser = await getParser(infLocation[0].uri); if (parser) { let sources = parser.getSymbolsType(Edk2SymbolType.infSource); for (const source of sources) { - let location = await gPathFind.findPath(await source.getValue()); - if(location[0].uri.fsPath === uri.fsPath){ + let sourceLocation = await gPathFind.findPath(await source.getValue(), path.dirname(infLocation[0].uri.fsPath)); + if(sourceLocation[0].uri.fsPath === uri.fsPath){ return true; } } @@ -841,9 +964,11 @@ export class EdkWorkspace { if(line.match(/\!include\s/gi)){ line = this.defines.replaceDefines(line); let value = line.replace(/!include/gi, "").trim(); - let location = await gPathFind.findPath(value); + let location = await gPathFind.findPath(value, path.dirname(document.uri.fsPath)); if (!location.length){continue;} + gDebugLog.trace(`Looking include: ${location[0].uri.fsPath}`); if(location[0].uri.fsPath === uri.fsPath){ + gDebugLog.trace(`Include found: ${location[0].uri.fsPath} in ${document.uri.fsPath} line ${lineIndex}`); retLocations.push(new vscode.Location(document.uri, createRange(lineIndex,lineIndex,0))); } } @@ -854,18 +979,17 @@ export class EdkWorkspace { } isDocumentInIndex(document: vscode.TextDocument): boolean { - for (const doc of this.parsedDocuments.keys()) { - if(doc === document.fileName) { - return true; - } - } - return false; + return this.parsedDocuments.has(document.uri.fsPath); } getGrayoutRange(document: vscode.TextDocument): vscode.Range[] { return this.parsedDocuments.get(document.uri.fsPath) || []; } + getGrayoutRangeByUri(uri: vscode.Uri): vscode.Range[] { + return this.parsedDocuments.get(uri.fsPath) || []; + } + async findDefinesFdf() { @@ -874,17 +998,17 @@ export class EdkWorkspace { } // reset conditional stack this.workInProgress = true; - gDebugLog.verbose(`Start finding defines fdf`); + gDebugLog.trace(`Start finding defines fdf`); this.conditionalStack = []; if (this.flashDefinitionDocument) { if (!this.isDocumentInIndex(this.flashDefinitionDocument)) { - this.filesFdf = []; + this.filesFdf = new Set(); await this._processDocument(this.flashDefinitionDocument,"FDF"); } } this.workInProgress = false; - gDebugLog.verbose("Finding fdf done."); + gDebugLog.trace("Finding fdf done."); return true; } @@ -899,8 +1023,7 @@ export class EdkWorkspace { // check if document is in index documents if (this.isDocumentInIndex(document)) { let grayoutRange = this.getGrayoutRange(document); - let grayoutController = new GrayoutController(document, grayoutRange); - grayoutController.doGrayOut(); + this._grayoutManager.setRanges(document.uri, grayoutRange); return; } } @@ -939,13 +1062,13 @@ export class EdkWorkspace { this.conditionOpen.push({uri:documentUri, lineNo:lineIndex}); switch(tokens[0].toLowerCase()){ case "!if": - conditionValue = this.evaluateConditional(conditionStr.replaceAll(UNDEFINED_VARIABLE,'FALSE'), documentUri, lineIndex); + conditionValue = this.evaluateConditional(conditionStr, documentUri, lineIndex); break; case "!ifdef": - conditionValue = this.pushConditional((tokens[1] !== UNDEFINED_VARIABLE)); + conditionValue = this.pushConditional(this.defines.isDefined(tokens[1])); break; case "!ifndef": - conditionValue = this.pushConditional((tokens[1] === UNDEFINED_VARIABLE)); + conditionValue = this.pushConditional(!this.defines.isDefined(tokens[1])); break; } @@ -964,7 +1087,7 @@ export class EdkWorkspace { DiagnosticManager.error(documentUri, lineIndex, EdkDiagnosticCodes.conditionalMissform, "!elseif without !if"); return true; } - conditionValue = this.evaluateConditional(conditionStr.replaceAll(UNDEFINED_VARIABLE,'FALSE'), documentUri, lineIndex); + conditionValue = this.evaluateConditional(conditionStr, documentUri, lineIndex); parentActive = this.conditionStack.length <= 1 || this.conditionStack[this.conditionStack.length - 2].active; // Update the top of the stack @@ -1039,36 +1162,105 @@ export class EdkWorkspace { this.conditionalStack.push(v); return v; } + + evaluateExpression(expression: string): any { + const originalExpression = expression; - private evaluateConditional(text: string, documentUri:vscode.Uri, lineNo:number): any { - - let data = text.replaceAll(/"true"/gi, "true"); // replace booleans - data = data.replaceAll(/"false"/gi, "false"); - data = data.replaceAll(/ and /gi, " && "); - data = data.replaceAll(/ or /gi, " || "); - data = data.replace(/ not /gi, " ! "); - data = data.replaceAll(/(? ${false}`); + // return false; + // } + + // add space to parenteses + expression = expression.replaceAll(/\(/g, " ( "); + expression = expression.replaceAll(/\)/g, " ) "); + expression = expression.replaceAll(/\s+/g, " "); + + + let tokens = expression.match(/(?:[^\s"]+|"[^"]*")+/g) || []; + let outputQueue = []; + let operatorStack: string[] = []; + let parentesisBalance = 0; + for (const token of tokens) { + if (!isNaN(Number(token))) { + // Numbers + outputQueue.push(Number(token)); + } else if (token.toLowerCase() === 'true' || token.toLowerCase() === 'false') { + // Boolean values + outputQueue.push(token.toLowerCase() === 'true'); + } else if(token.trim().startsWith('"') && token.trim().endsWith('"')){ + // String values + outputQueue.push(token); + } else if (token in OPERATORS) { + // Operators + while (operatorStack.length && operatorStack[operatorStack.length - 1] in OPERATORS && + OPERATORS[token as keyof typeof OPERATORS].precedence <= OPERATORS[operatorStack[operatorStack.length - 1] as keyof typeof OPERATORS].precedence) { + outputQueue.push(operatorStack.pop()); + } + operatorStack.push(token); + } else if (token === '(') { + // Parentesis + parentesisBalance++; + operatorStack.push(token); + } else if (token === ')') { + // Parentesis + parentesisBalance--; + while (operatorStack.length && operatorStack[operatorStack.length - 1] !== '(') { + outputQueue.push(operatorStack.pop()); + } + operatorStack.pop(); + } else{ + // Strings without quotes + outputQueue.push(`"${token}"`); + // throw new Error(`Error evaluating expression: ${originalExpression} - bad Operand ${token}`); + } + } + if(parentesisBalance !== 0){ + throw new Error(`Error evaluating expression: ${originalExpression} - Parenthesis balance error`); + } + + while (operatorStack.length) { + outputQueue.push(operatorStack.pop()); + } + + let stack: any[] = []; + for (const token of outputQueue) { + if (token! as keyof typeof OPERATORS in OPERATORS) { + let y = stack.pop() as never; + let x = stack.pop() as never; + stack.push(OPERATORS[token! as keyof typeof OPERATORS].fn(x, y)); + }else{ + stack.push(token); + } + } - while (data.match(/"in"/gi)) { - let inIndex = data.search(/"in"/i); - let last = data.slice(inIndex + '"in"'.length).replace(/("\w+")/i, '$1)').trim(); - let beging = data.slice(0, inIndex).trim(); - data = beging + ".includes(" + last; + if(stack.length > 1){ + throw new Error(`Error evaluating expression: ${originalExpression}`); } + const retValue = (stack[0]===UNDEFINED_VARIABLE)? false : stack[0]; + gDebugLog.debug(`Evaluate expression: ${originalExpression} -> ${retValue}`); + return retValue; + } + private evaluateConditional(text: string, documentUri:vscode.Uri, lineNo:number): any { + + + // let data = text.replaceAll(/(?=': { precedence: 7, fn: _greaterOrEquals }, + '<': { precedence: 7, fn: _lessThan }, + '>': { precedence: 7, fn: _greaterThan }, + '+': { precedence: 8, fn: _plus }, + '-': { precedence: 8, fn: _subtract}, + '*': { precedence: 9, fn: _times }, + '/': { precedence: 9, fn: _division }, + '%': { precedence: 9, fn: _module }, + '!': { precedence: 10, fn: _not }, + 'not': { precedence: 10, fn: _not }, + 'NOT': { precedence: 10, fn: _not }, + '~': { precedence: 10, fn: _bitwiseNot }, + '<<': { precedence: 11, fn: _leftShift }, + '>>': { precedence: 11, fn: _rightShift }, + 'in': { precedence: 12, fn: _in}, + 'IN': { precedence: 12, fn: _in}, +}; + + + +function boolToNumber(value: boolean): number { + if (typeof value !== 'boolean') { + return value; + } + return value ? 1 : 0; +} + +function _in(x:any, y: any) { + if (x === UNDEFINED_VARIABLE || y === UNDEFINED_VARIABLE) { + return false; + } + if(typeof x === 'string'){ + x = x.replaceAll('"', ''); + } + if(typeof y === 'string'){ + y = y.replaceAll('"', ''); + y = y.split(" "); + } + + if(y.includes){ + return y.includes(x); + } + return false; + +} + +function _bitwiseNot(y:any, x: number) { + if(validateSingle(x, 'number', '~')) { + return ~x; + } + return false; + +} + +function _lessThan(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '>')) { + return x > y; + } + return false; +} + +function _lessOrEquals(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '<=')) { + return x <= y; + } + return false; +} + +function _greaterThan(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '>')) { + return x > y; + } + return false; +} + +function _greaterOrEquals(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '>=')) { + return x >= y; + } + return false; +} + +function _leftShift(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '<<')) { + return x << y; + } + false; +} + +function _rightShift(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '>>')) { + return x >> y; + } + false; +} + +function _times(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '*')) { + return x * y; + } + return false; +} + +function _division(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '/')) { + if (y === 0) { + throw new Error(`Invalid expression: Division by zero.`); + } + return x / y; + } + + return false; +} + +function _module(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + + + + if(validate(x, y, 'number', '%')) { + if (y === 0) { + throw new Error(`Invalid expression: Module by zero.`); + } + return x % y; + } + + return false; +} + +function _plus(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + + if(validate(x, y, 'number', '+')) { + return x + y; + } + return false; +} + +function _subtract(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '-')) { + return x - y; + } + return false; +} + +function _xor(x:any, y: any) { + y = boolToNumber(y); + x = boolToNumber(x); + if(validate(x, y, 'number', '^')) { + return x ^ y; + } + return false; + +} + +function _bitwise_or(x:number, y: number) { + + if(validate(x, y, 'number', '|')) { + return x | y; + } + return false; + +} + +function _bitsiwe_and(x:number, y: number) { + if(validate(x, y, 'number', '&')) { + return x & y; + } + return false; + +} + +function _equals(x:any, y: any) { + if (x === UNDEFINED_VARIABLE || y === UNDEFINED_VARIABLE) { + return false; + } + return x === y; +} + +function _not_equals(x:any, y: any) { + if (x === UNDEFINED_VARIABLE || y === UNDEFINED_VARIABLE) { + return false; + } + return x !== y; +} + +function _not(y:any, x: boolean) { + if(validateSingle(x, 'boolean', 'not')) { + return !x; + } + return false; +} + +function _and(x:any, y: boolean) { + if (validate(x, y, 'boolean', 'and')) { + return x && y; + } + return false; +} + +function _or(x:any, y: boolean) { + + if(validate(x, y, 'boolean', 'or')) { + return x || y; + } + return false; + + +} + +function validateSingle(x:any, expected:string, operator:string) { + if (x === UNDEFINED_VARIABLE) { + return false; + } + if (typeof x !== expected) { + throw new Error(`Invalid expression: This operator cannot be used in ${typeof x} expression: [${operator}].`); + } + return true; +} + +function validate(x:any, y:any, expected:string, operator:string) { + if (x === UNDEFINED_VARIABLE || y === UNDEFINED_VARIABLE) { + return false; + } + if (typeof x !== expected || typeof y !== expected) { + throw new Error(`Invalid expression: This operator cannot be used in ${typeof x} ${operator} ${typeof y} expression: [${operator}].`); + } + return true; +} diff --git a/src/libraryTree.ts b/src/libraryTree.ts deleted file mode 100644 index 210e7e3..0000000 --- a/src/libraryTree.ts +++ /dev/null @@ -1,109 +0,0 @@ - -import { gDebugLog, gModuleReport } from "./extension"; -import { EdkIniFile, EdkModule, ModuleReport } from "./moduleReport"; - -import { getNonce } from "./utilities/getNonce"; -import { getUri } from "./utilities/getUri"; -import { getCurrentDocument, gotoFile } from "./utils"; -import * as vscode from 'vscode'; -import { TreeWebview } from "./TreeWebview"; -import { TreeDetailsDataProvider } from "./TreeDataProvider"; -import { TreeItem } from "./treeElements/TreeItem"; - -class ModulePickOption implements vscode.QuickPickItem{ - label: string; - kind?: vscode.QuickPickItemKind | undefined; - description?: string | undefined; - detail?: string | undefined; - picked?: boolean | undefined; - alwaysShow?: boolean | undefined; - buttons?: readonly vscode.QuickInputButton[] | undefined; - module:EdkModule; - constructor(module:EdkModule){ - this.label = module.name; - this.description=module.arch; - this.detail = module.path; - this.module = module; - } - -} - -export async function showLibraryTree(){ - gDebugLog.debug("CMD show library tree"); - let document = getCurrentDocument(); - - let moduleReport = gModuleReport; - if(document === undefined){return;} - - gDebugLog.debug(document.fileName); - - if(!document.fileName.endsWith(".inf")){ - // eslint-disable-next-line @typescript-eslint/no-floating-promises - vscode.window.showErrorMessage("Run this command when you are on a .inf file"); - return; - } - - // if(!gEdkDatabase.isFileInuse(document.fileName)){ - // // eslint-disable-next-line @typescript-eslint/no-floating-promises - // vscode.window.showErrorMessage("This file is not in use on the current compilation"); - // return; - // } - - - if(!moduleReport.isPopulated){ - void vscode.window.showErrorMessage("Module information was not generated during compilation.", "Help").then(async selection => { - if (selection === "Help"){ - await vscode.env.openExternal(vscode.Uri.parse( - 'https://github.com/intel/Edk2Code/wiki/Index-source-code#enable-compile-information')); - } - }); - return; - } - - let moduleTarget = moduleReport.getModule(document.fileName); - if(moduleTarget===undefined){ - // eslint-disable-next-line @typescript-eslint/no-floating-promises - vscode.window.showErrorMessage("This works after EDK project is loaded from build folder"); - return; - } - - let contextModule; - if(moduleTarget.isLibrary){ - let moduleUsers = moduleTarget.getUsages(); - var options: ModulePickOption[] = []; - for (const mod of moduleUsers) { - options.push(new ModulePickOption(mod)); - } - - let option = await vscode.window.showQuickPick(options, {title: "This is a library, select module context", matchOnDescription:true, matchOnDetail:true}); - if (option === undefined) {return;} - contextModule = option.module; - }else{ - contextModule = moduleTarget; - } - - let reportObject = moduleReport.getLibraryTree(moduleTarget, contextModule); - let treeProvider = new TreeDetailsDataProvider(); - let root = new TreeItem(reportObject["children"][0]["name"]); - root.description = reportObject["children"][0]["path"]; - treeProvider.addChildren(root); - for (const curerntNode of reportObject["children"][0]["children"]) { - processReport(curerntNode, root); - } - - let treeView = new TreeWebview(treeProvider,moduleTarget.name, contextModule.name); - treeView.render(); - -} - -function processReport(currentNode:any, node: TreeItem){ - let newNode = new TreeItem(currentNode["name"]); - newNode.description = currentNode["path"]; - node.addChildren(newNode); - - if(currentNode["children"].length > 0){ - for (const r of currentNode["children"]) { - processReport(r, newNode); - } - } -} diff --git a/src/mcp/mcpServer.ts b/src/mcp/mcpServer.ts new file mode 100644 index 0000000..8d2c7a4 --- /dev/null +++ b/src/mcp/mcpServer.ts @@ -0,0 +1,393 @@ +import * as http from 'http'; +import * as vscode from 'vscode'; +import path = require('path'); +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { gDebugLog, gEdkWorkspaces, gPathFind, gWorkspacePath } from '../extension'; +import { openTextDocument } from '../utils'; +import { getParserForDocument } from '../edkParser/parserFactory'; +import { Edk2SymbolType } from '../symbols/symbolsType'; +import { z } from 'zod'; + +let httpServer: http.Server | undefined; +let mcpServer: McpServer | undefined; +const transports: Record = {}; + +function createMcpServer(): McpServer { + const server = new McpServer( + { name: 'edk2code', version: '1.0.0' }, + { capabilities: { tools: {} } } + ); + + // ── Tool: list_files ──────────────────────────────────────────────── + server.registerTool( + 'list_edk2_workspace_files', + { + description: + 'List all files used in the EDK2 compilation across all loaded workspaces. ' + + 'Returns file names with their full paths. ' + + 'If a file you are looking for is not in this list, it is NOT used in the current compilation.', + inputSchema: { + fileName: z + .string() + .optional() + .describe( + 'Optional regex pattern to filter files by name (e.g. "Pci" to find all PCI-related files, "Pci.*\\.inf" for PCI .inf files). ' + + 'The pattern is matched case-insensitively against the file base name. ' + + 'If no match is found the file is not part of the current compilation.' + ), + }, + }, + async ({ fileName }) => { + const wps = gEdkWorkspaces.workspaces; + const allFiles: { name: string; fullPath: string }[] = []; + + for (const wp of wps) { + for (const filePath of wp.getFilesList()) { + allFiles.push({ + name: path.basename(filePath), + fullPath: filePath, + }); + } + } + + let results = allFiles; + if (fileName) { + try { + const regex = new RegExp(fileName, 'i'); + results = allFiles.filter((f) => regex.test(f.name)); + } catch { + // If invalid regex, fall back to substring match + const needle = fileName.toLowerCase(); + results = allFiles.filter( + (f) => f.name.toLowerCase().includes(needle) + ); + } + } + + if (results.length === 0) { + const msg = fileName + ? `File "${fileName}" was not found in any EDK2 workspace. It is not used in the current compilation.` + : 'No files found. The EDK2 workspace may not be indexed yet. Run "EDK2: Rebuild index database" first.'; + return { content: [{ type: 'text', text: msg }] }; + } + + const text = results + .map((f) => `${f.name}\t${f.fullPath}`) + .join('\n'); + return { content: [{ type: 'text', text }] }; + } + ); + + // ── Tool: resolve_edk2_file_path ──────────────────────────────────── + server.registerTool( + 'resolve_edk2_file_path', + { + description: + 'Resolve an EDK2 relative path or file name to its full filesystem path using the EDK2 path resolution logic. ' + + 'If the file cannot be resolved, it is not used in the current compilation.', + inputSchema: { + filePath: z + .string() + .describe( + 'The EDK2 relative path or file name to resolve (e.g. "MdeModulePkg/Core/Pei/PeiMain.inf").' + ), + }, + }, + async ({ filePath }) => { + const locations = await gPathFind.findPath(filePath); + if (locations.length === 0) { + return { + content: [ + { + type: 'text', + text: `Could not resolve "${filePath}". The file is not used in the current compilation or the workspace is not indexed.`, + }, + ], + }; + } + const text = locations.map((l) => l.uri.fsPath).join('\n'); + return { content: [{ type: 'text', text }] }; + } + ); + + // ── Tool: find_library_implementation ─────────────────────────────── + server.registerTool( + 'find_edk2_library_implementation', + { + description: + 'Find the implementation (INF file) of an EDK2 library by base name. ' + + 'Searches all loaded workspaces for library declarations in DSC files. ' + + 'Returns the library name, the INF path implementing it, the DSC location where it is declared, and the section properties (arch, module type). ' + + 'If no match is found, the library is not used in the current compilation.', + inputSchema: { + libraryName: z + .string() + .describe( + 'Library class name or regex pattern to search for (e.g. "BaseMemoryLib", "Pci.*Lib", "UefiBootServicesTableLib"). ' + + 'Matched case-insensitively against library class names declared in DSC files.' + ), + }, + }, + async ({ libraryName }) => { + const wps = gEdkWorkspaces.workspaces; + const results: string[] = []; + + let regex: RegExp | null = null; + try { + regex = new RegExp(libraryName, 'i'); + } catch { + // invalid regex, fall back to substring + } + + for (const wp of wps) { + for (const lib of wp.filesLibraries) { + const libNameDsc = lib.text.split('|')[0].trim(); + const matches = regex + ? regex.test(libNameDsc) + : libNameDsc.toLowerCase().includes(libraryName.toLowerCase()); + + if (matches) { + const resolvedPaths = await gPathFind.findPath(lib.path); + const fullPath = resolvedPaths.length + ? resolvedPaths[0].uri.fsPath + : lib.path; + const dscFile = lib.location.uri.fsPath; + const dscLine = lib.location.range.start.line + 1; + const section = lib.sectionProperties.toString() || 'unknown'; + results.push( + `${libNameDsc}\t${fullPath}\t${dscFile}:${dscLine}\t[${section}]` + ); + } + } + } + + if (results.length === 0) { + return { + content: [ + { + type: 'text', + text: `No library matching "${libraryName}" was found in any EDK2 workspace. It is not used in the current compilation.`, + }, + ], + }; + } + + const header = 'LibraryClass\tImplementation\tDSC Location\tSection'; + return { + content: [{ type: 'text', text: header + '\n' + results.join('\n') }], + }; + } + ); + + // ── Tool: find_inf_for_source ─────────────────────────────────────── + server.registerTool( + 'find_edk2_inf_for_source', + { + description: + 'Given a C/H source file, find the EDK2 INF file (library or component) that includes it in its [Sources] section. ' + + 'This is useful for determining which EDK2 module or library a source file belongs to. ' + + 'If no INF is found, the file may not be part of a compiled EDK2 component.', + inputSchema: { + sourceFile: z + .string() + .describe( + 'Full path or file name of a C/H source file (e.g. "PeiMain.c", "D:/edk2/MdeModulePkg/Core/Pei/PeiMain.c"). ' + + 'If only a file name is given, the tool will attempt to resolve it.' + ), + }, + }, + async ({ sourceFile }) => { + // Resolve to a URI + let fileUri: vscode.Uri; + if (path.isAbsolute(sourceFile)) { + fileUri = vscode.Uri.file(sourceFile); + } else { + const resolved = await gPathFind.findPath(sourceFile); + if (resolved.length === 0) { + return { + content: [{ + type: 'text', + text: `Could not resolve "${sourceFile}". The file was not found in the workspace.`, + }], + }; + } + fileUri = resolved[0].uri; + } + + const results: string[] = []; + + // Try indexed workspaces first (like gotoInf) + const wps = await gEdkWorkspaces.getWorkspace(fileUri); + if (wps.length) { + for (const wp of wps) { + const locations = await wp.getInfReference(fileUri); + for (const loc of locations) { + const infPath = loc.uri.fsPath; + const line = loc.range.start.line + 1; + if (!results.includes(infPath)) { + results.push(infPath); + } + } + } + } + + // Fallback: walk up from the source file looking for INF files + // that list it in their [Sources] section + if (results.length === 0) { + const fileName = path.basename(fileUri.fsPath); + const folderPath = path.dirname(fileUri.fsPath); + const workspaceRoot = gWorkspacePath; + + let currentDir = folderPath; + while (currentDir.length >= workspaceRoot.length) { + // Check all modules and libraries whose INF is relative to this dir + for (const wp of gEdkWorkspaces.workspaces) { + const allInfs = [...wp.filesModules, ...wp.filesLibraries]; + for (const inf of allInfs) { + const resolvedInf = await gPathFind.findPath(inf.path); + if (resolvedInf.length === 0) { continue; } + const infDir = path.dirname(resolvedInf[0].uri.fsPath); + if (!folderPath.startsWith(infDir)) { continue; } + try { + const doc = await openTextDocument(resolvedInf[0].uri); + const parser = await getParserForDocument(doc); + if (parser) { + const sources = parser.getSymbolsType(Edk2SymbolType.infSource); + for (const source of sources) { + const srcPath = await gPathFind.findPath( + await source.getValue(), + path.dirname(resolvedInf[0].uri.fsPath) + ); + if (srcPath.length && srcPath[0].uri.fsPath === fileUri.fsPath) { + const infFullPath = resolvedInf[0].uri.fsPath; + if (!results.includes(infFullPath)) { + results.push(infFullPath); + } + } + } + } + } catch { /* skip parse errors */ } + } + } + if (results.length > 0) { break; } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { break; } + currentDir = parentDir; + } + } + + if (results.length === 0) { + return { + content: [{ + type: 'text', + text: `No INF file found for "${sourceFile}". The file may not be part of a compiled EDK2 component or library.`, + }], + }; + } + + const text = results.map((infPath) => { + const name = path.basename(infPath, '.inf'); + return `${name}\t${infPath}`; + }).join('\n'); + return { + content: [{ type: 'text', text: `INF files for ${path.basename(sourceFile)}:\n${text}` }], + }; + } + ); + + return server; +} + +export async function startMcpServer(port: number): Promise { + if (httpServer) { + vscode.window.showInformationMessage(`MCP SSE server is already running on port ${port}`); + return; + } + + mcpServer = createMcpServer(); + + httpServer = http.createServer(async (req, res) => { + const url = new URL(req.url ?? '', `http://localhost:${port}`); + + // SSE stream endpoint + if (req.method === 'GET' && url.pathname === '/sse') { + gDebugLog.info('MCP SSE: new client connection'); + try { + const transport = new SSEServerTransport('/messages', res); + transports[transport.sessionId] = transport; + transport.onclose = () => { + gDebugLog.info(`MCP SSE: session ${transport.sessionId} closed`); + delete transports[transport.sessionId]; + }; + const server = createMcpServer(); + await server.connect(transport); + } catch (err) { + gDebugLog.error(`MCP SSE error: ${err}`); + if (!res.headersSent) { + res.writeHead(500); + res.end('Internal Server Error'); + } + } + return; + } + + // Message endpoint for client→server JSON-RPC + if (req.method === 'POST' && url.pathname === '/messages') { + const sessionId = url.searchParams.get('sessionId'); + if (!sessionId || !transports[sessionId]) { + res.writeHead(400); + res.end('Invalid or missing sessionId'); + return; + } + await transports[sessionId].handlePostMessage(req, res); + return; + } + + // Health check + if (req.method === 'GET' && url.pathname === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok' })); + return; + } + + res.writeHead(404); + res.end('Not Found'); + }); + + return new Promise((resolve, reject) => { + httpServer!.listen(port, () => { + gDebugLog.info(`MCP SSE server listening on http://localhost:${port}/sse`); + vscode.window.showInformationMessage( + `EDK2 MCP SSE server started on http://localhost:${port}/sse` + ); + resolve(); + }); + httpServer!.on('error', (err) => { + gDebugLog.error(`MCP SSE server error: ${err}`); + vscode.window.showErrorMessage(`Failed to start MCP server: ${err.message}`); + httpServer = undefined; + reject(err); + }); + }); +} + +export function stopMcpServer(): void { + if (!httpServer) { + vscode.window.showInformationMessage('MCP SSE server is not running'); + return; + } + for (const id of Object.keys(transports)) { + transports[id].close(); + delete transports[id]; + } + httpServer.close(); + httpServer = undefined; + mcpServer = undefined; + gDebugLog.info('MCP SSE server stopped'); + vscode.window.showInformationMessage('EDK2 MCP SSE server stopped'); +} + +export function isMcpServerRunning(): boolean { + return httpServer !== undefined; +} diff --git a/src/moduleReport.ts b/src/moduleReport.ts index a24b400..21825ca 100644 --- a/src/moduleReport.ts +++ b/src/moduleReport.ts @@ -1,5 +1,5 @@ import path = require("path"); -import { gModuleReport, gWorkspacePath } from "./extension"; +import { gModuleReport } from "./extension"; import * as fs from 'fs'; import { assert } from "console"; import { normalizePath, pathCompare } from "./utils"; diff --git a/src/newVersionPage/1.1.0.md b/src/newVersionPage/1.1.0.md new file mode 100644 index 0000000..5ff6d3d --- /dev/null +++ b/src/newVersionPage/1.1.0.md @@ -0,0 +1,31 @@ +# EDK2Code new version + +Thank you for installing the new version of the EDK2Code extension! 🎉 + +For detailed documentation, please visit: [EDK2Code Documentation](https://intel.github.io/Edk2Code/). + +You can find our GitHub repository here: [EDK2Code GitHub Repository](https://github.com/intel/Edk2Code). + +If you find this extension useful, please consider giving us a star on GitHub or leaving a review in the VSCode Marketplace. Your support is greatly appreciated! ⭐ + +# New features + +## [Module map](https://intel.github.io/Edk2Code/advance_features/#module-map) + +You can right click on a compiled INF file and select `EDK2: Show Module Map` + +![module-map-context-menu](https://intel.github.io/Edk2Code/images/module-map-context-menu.png) + +This will open the EDK2 submenu showing the libraries and source files that were used to compile that INF. + +![module-map](https://intel.github.io/Edk2Code/images/module-map.png) + +This feature is helpful in visualizing how a module includes various libraries. It also provides insights into how C files within the module include header files. By understanding these relationships, developers can better manage dependencies. + +## [Error Detection](https://intel.github.io/Edk2Code/advance_features/#error-detection) + +The DSC analysis can identify potential issues within the DSC files, such incorrect paths, duplicated libraries, etc. These issues are highlighted and shown in the Visual Studio Code "Problems" window. + +![55e73504-3e8b-4b58-a9fa-5fc64a89614f](https://intel.github.io/Edk2Code/images/55e73504-3e8b-4b58-a9fa-5fc64a89614f.png) + +You can disable this using the `edk2code.enableDiagnostics` setting \ No newline at end of file diff --git a/src/newVersionPage/2.0.0.md b/src/newVersionPage/2.0.0.md new file mode 100644 index 0000000..28d784f --- /dev/null +++ b/src/newVersionPage/2.0.0.md @@ -0,0 +1,81 @@ +# EDK2Code 2.0.0 + +Thank you for installing the new version of the EDK2Code extension! 🎉 + +For detailed documentation, please visit: [EDK2Code Documentation](https://intel.github.io/Edk2Code/). + +You can find our GitHub repository here: [EDK2Code GitHub Repository](https://github.com/intel/Edk2Code). + +If you find this extension useful, please consider giving us a star on GitHub or leaving a review in the VSCode Marketplace. Your support is greatly appreciated! ⭐ + +# New features + +## Edk2Code sidebar + +The extension now lives in its own dedicated activity bar container, replacing the old `Edk2` panel under the Explorer view. You can find all Edk2Code views grouped together in the sidebar, including the new **Workspace** and **Module Info** views. + +## Workspace view + +The new **Workspace** view provides a unified, navigable representation of your parsed EDK2 workspace. It replaces the previous Module Map / Reference Tree / Library Tree commands with a single, persistent tree. + +Key capabilities: + +- **Sub-trees and includes** — DSC, INF, FDF and source files are organized hierarchically. Header includes and library sub-trees are integrated into the tree, so you can navigate dependencies in one place. +- **Search and filter** — Use the search action to quickly find nodes anywhere in the workspace tree. A dedicated filter is available to hide inactive (grayed-out) elements. +- **Reveal active editor** — Jump from the currently open file to its location in the workspace tree. +- **Drag and drop** — Reorganize and rearrange tree nodes via drag and drop. +- **Multiple workspaces** — When multiple build configurations are loaded, you can switch between them from the view title bar. Searching for symbols automatically switches to the workspace that owns the result. +- **Copy node path / Copy tree** — Quickly copy the path of a node or export the entire tree as text. +- **Welcome view** — When no workspace is loaded, the view guides you through discovering build folders or opening the configuration UI. + +## Module Info view + +The new **Module Info** view shows the EDK2 module information for the file you are currently editing. As soon as you open a C, INF or related source file that belongs to a module, the view populates with: + +- The owning INF and DSC declaration +- Libraries linked to the module +- Quick navigation actions (go to definition, open file, go to DSC declaration) + +Double-click any entry to jump directly to the corresponding source location. + +## Build folder auto-discovery + +EDK2Code can now scan your workspace and automatically detect existing build output folders. + +- Run **`EDK2: Discover build folders`** to scan the workspace. +- Once discovered, run **`EDK2: Use discovered build folders`** to load them directly — no manual configuration needed. +- The Workspace view welcome page surfaces these actions when nothing is loaded yet. + +## Compile EDK2 file + +You can now compile an individual EDK2 file (e.g. a `.c` belonging to a module) directly from the editor. + +- A play (▶) icon is shown on supported source files. +- The command **`EDK2: Compile Edk2 file`** invokes the build for the parent module of the active file. + +## MCP server integration + +EDK2Code now exposes an **MCP (Model Context Protocol) SSE server** that lets AI agents and external tools query your parsed EDK2 workspace. + +- Start with **`EDK2: Start MCP SSE Server`** and stop with **`EDK2: Stop MCP SSE Server`**. +- The server port is configurable via the `edk2code.mcpServerPort` setting (default `3100`). + +## Settings UI + +A graphical **Workspace configuration (UI)** panel has been added to make managing build configurations easier. Launch it from the Workspace view title bar (`$(gear)` icon) or via the **`EDK2: Workspace configuration (UI)`** command — no more hand-editing JSON for common setups. + +## Goto overwriting definition + +When a symbol is overwritten in a DSC (libraries, PCDs, modules), a new code action lets you jump directly to the overwriting definition. + +## Other improvements + +- **Disable cscope** — A new `edk2code.useCscope` setting lets you turn off cscope integration entirely. When disabled, the cscope database is not built or queried. +- **Unload workspace** — New `EDK2: Unload workspace` command to clear the currently loaded build configuration. +- **Refresh workspace config** — Reload the workspace configuration without restarting VS Code. +- **Focus INF from C files** — Improved navigation from C source files back to their owning INF module. +- **Better module symbols** — DSC parser now recognizes module sub-context sections, build options, and improved symbols for libraries and modules. +- **Loading and discovery indicators** — Progress indicators are shown while the workspace is being parsed or build folders are being discovered. +- **Improved grayout controller** — Inactive code regions are computed and updated more reliably. +- **Path improvements** — Missing paths now report folders instead of file names, and tooltips include the full path. +- **Extensive test suite** — New parser and workspace tests covering ASL, DEC, DSC, FDF, INF and VFR. diff --git a/src/newVersionPage/newVersionMessage.ts b/src/newVersionPage/newVersionMessage.ts new file mode 100644 index 0000000..9c1ae46 --- /dev/null +++ b/src/newVersionPage/newVersionMessage.ts @@ -0,0 +1,18 @@ +import getCurrentVersion from "../utils"; +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; + +export async function showReleaseNotes(context: vscode.ExtensionContext){ + const CURRENT_VERSION = getCurrentVersion(); + const previousVersion = context.globalState.get('extensionVersion'); + if (previousVersion !== CURRENT_VERSION) { + await context.globalState.update('extensionVersion', CURRENT_VERSION); + + const releaseNotesPath = path.join(context.extensionPath, 'src', 'newVersionPage', `${CURRENT_VERSION}.md`); + if (fs.existsSync(releaseNotesPath)) { + const releaseNotesUri = vscode.Uri.file(releaseNotesPath); + await vscode.commands.executeCommand('markdown.showPreview', releaseNotesUri); + } + } +} \ No newline at end of file diff --git a/src/pathfind.ts b/src/pathfind.ts index eecb74e..7add752 100644 --- a/src/pathfind.ts +++ b/src/pathfind.ts @@ -2,9 +2,10 @@ import path = require("path"); import * as fs from 'fs'; import * as vscode from 'vscode'; import glob = require("fast-glob"); -import { getRealPathRelative } from "./utils"; + import { gConfigAgent, gDebugLog, gWorkspacePath } from "./extension"; import { REGEX_VAR_USAGE } from "./edkParser/commonParser"; +import { toPosix, getRealPathRelative } from "./utils"; export class PathFind{ @@ -37,12 +38,11 @@ export class PathFind{ ]; - private missingFiles: string[] = []; + private missingPaths: string[] = []; async findPath(pathArg: string, relativePath: string|undefined = "") { pathArg = pathArg.replaceAll(/(\\+|\/+)/gi, path.sep); - let ws = gWorkspacePath; - gDebugLog.verbose(`Looking for: ${pathArg}`); + gDebugLog.trace(`Looking for: ${pathArg}`); // Restrict path characters if (pathArg.match(/[\[\]\#\%\&\{\}\<\>\*\?\!\'\@\|\‘\`\“,'"'\^]/gi)) { @@ -58,8 +58,7 @@ export class PathFind{ return []; } - if(this.missingFiles.includes(pathArg)){ - //Todo: Add job to look for the file + if(this.isKnownMissing(pathArg)){ return []; } @@ -96,20 +95,48 @@ export class PathFind{ gDebugLog.warning(`Global find: ${pathArg}`); + let normalizedArg = pathArg.replaceAll('\\', '/').replaceAll(REGEX_VAR_USAGE, '**'); + // Search by filename only (like Ctrl+P), then filter by path suffix + let fileName = path.basename(pathArg); + + // If a temporary T-tree index exists (workspace is being rebuilt + // after a clear) use it for an instant lookup; otherwise fall + // back to the normal vscode.workspace.findFiles API. + const fileIndex = gConfigAgent.getFileIndex(); + let paths: vscode.Uri[]; + if (fileIndex) { + paths = fileIndex.findFilesByName(fileName); + } else { + paths = await vscode.workspace.findFiles(`**/${fileName}`, null); + } + + // Filter results that match the full path pattern + let filteredPaths = paths.filter(p => { + let normalizedFsPath = p.fsPath.replaceAll('\\', '/'); + // Build a regex from the normalized path arg, replacing ** with .* + let patternStr = normalizedArg.replaceAll('**', '.*').replace(/[.*+?^${}()|[\]\\]/g, (m) => m === '.*' ? '.*' : '\\' + m); + // Re-apply .* for the glob wildcards after escaping + patternStr = normalizedArg.split('**').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*'); + return new RegExp(patternStr + '$', 'i').test(normalizedFsPath); + }); + + // If no filtered results but we had unfiltered results, use filename matches as fallback + if(filteredPaths.length === 0 && paths.length > 0){ + filteredPaths = paths; + } - let globPath = pathArg.replaceAll('/', '\\').replaceAll(REGEX_VAR_USAGE, '**'); - let paths = await vscode.workspace.findFiles(`**\\${globPath}`); let retPath = []; - for (const p of paths) { + for (const p of filteredPaths) { retPath.push(new vscode.Location(vscode.Uri.file(p.fsPath), new vscode.Position(0, 0))); + // Add dinamyc include paths let newPath = p.fsPath.slice(0,p.fsPath.length - pathArg.length - 1); - gConfigAgent.pushBuildPackagePaths(getRealPathRelative(newPath)); + gConfigAgent.pushBuildPackagePaths(getRealPathRelative(newPath)); } if(retPath.length === 0){ gDebugLog.warning(`Missing file: ${pathArg}`); - this.missingFiles.push(path.join(relativePath, pathArg)); + this.addMissingPath(pathArg); } return retPath; @@ -128,6 +155,62 @@ export class PathFind{ } } + /** + * Check if pathArg (or any of its parent segments) is already known to be missing. + */ + private isKnownMissing(pathArg: string): boolean { + const normalized = toPosix(pathArg); + return this.missingPaths.some(missing => { + return normalized === missing || normalized.startsWith(missing + '/'); + }); + } + + /** + * Walk up parent directories of pathArg and find the highest-level + * directory that doesn't exist in the workspace or package paths. + * Cache that root so all future lookups under it are skipped. + */ + private addMissingPath(pathArg: string): void { + // Don't add if already covered by an existing missing path + if (this.isKnownMissing(pathArg)) { + return; + } + + const posixPath = toPosix(pathArg); + const parts = posixPath.split('/'); + let missingRoot = posixPath; + + // Walk from the top-level segment downward; find the first parent that + // does NOT exist anywhere, and cache that instead of the full path. + for (let i = 1; i < parts.length; i++) { + const parentPath = parts.slice(0, i).join(path.sep); + if (!this.parentExistsInWorkspace(parentPath)) { + missingRoot = parentPath; + break; + } + } + + gDebugLog.trace(`Caching missing path: ${missingRoot}`); + this.missingPaths.push(toPosix(missingRoot)); + } + + /** + * Check whether a relative directory exists under the workspace root + * or any of the configured package paths. + */ + private parentExistsInWorkspace(parentPath: string): boolean { + if (fs.existsSync(path.join(gWorkspacePath, parentPath))) { + return true; + } + const packagePaths = gConfigAgent.getBuildPackagePaths(); + for (const relPath of packagePaths) { + if (fs.existsSync(path.join(relPath, parentPath))) { + return true; + } + } + return false; + } + produceLocation(path:string){ return [new vscode.Location(vscode.Uri.file(path), new vscode.Position(0, 0))]; } diff --git a/src/rg.ts b/src/rg.ts index 1f5f92b..4fad83d 100644 --- a/src/rg.ts +++ b/src/rg.ts @@ -32,18 +32,18 @@ export async function rgSearch(text:string, includeExt:string[]=[], filesPath:st command += " -- ./"; } - gDebugLog.verbose(command); + gDebugLog.trace(command); let textResult=""; try { edkStatusBar.setWorking(); textResult = await exec(command, gWorkspacePath); edkStatusBar.clearWorking(); } catch (error) { - gDebugLog.verbose(`rgSearch: ${error}`); + gDebugLog.trace(`rgSearch: ${error}`); resolve([]); } - gDebugLog.verbose(textResult); + gDebugLog.trace(textResult); let locations:Location[] = []; for (const jsonData of textResult.split('\n')) { if(jsonData===""){continue;} @@ -82,18 +82,18 @@ export async function rgSearchText(text:string, includeExt:string[]=[], filesPat command += " -- ./"; } - gDebugLog.verbose(command); + gDebugLog.trace(command); let textResult=""; try { edkStatusBar.setWorking(); textResult = await exec(command, gWorkspacePath); edkStatusBar.clearWorking(); } catch (error) { - gDebugLog.verbose(`rgSearch: ${error}`); + gDebugLog.trace(`rgSearch: ${error}`); resolve([]); } - gDebugLog.verbose(textResult); + gDebugLog.trace(textResult); let textMatches:string[] = []; for (const jsonData of textResult.split('\n')) { if(jsonData===""){continue;} diff --git a/src/settings/settings.html b/src/settings/settings.html index b893552..7cf3a4c 100644 --- a/src/settings/settings.html +++ b/src/settings/settings.html @@ -379,6 +379,123 @@ background-color: rgba(255, 255, 255, 0.15) } + .dsc-list-item { + display: flex; + align-items: center; + padding: 4px 8px; + margin-bottom: 4px; + border: 1px solid var(--vscode-settings-textInputBorder); + background: var(--vscode-settings-textInputBackground); + border-radius: 3px; + } + + .dsc-list-item .dsc-path { + flex: 1; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .dsc-list-item .dsc-edit-input { + flex: 1; + height: 17px; + font-size: 13px; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + color: var(--vscode-settings-textInputForeground); + background: var(--vscode-settings-textInputBackground); + border: 1px solid var(--vscode-focusBorder); + } + + .dsc-list-item button { + padding: 2px 8px; + margin-left: 4px; + font-size: 12px; + } + + .define-list-item { + display: flex; + align-items: center; + padding: 4px 8px; + margin-bottom: 4px; + border: 1px solid var(--vscode-settings-textInputBorder); + background: var(--vscode-settings-textInputBackground); + border-radius: 3px; + } + + .define-list-item .define-name { + flex: 1; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 13px; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .define-list-item .define-value { + flex: 1; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-left: 8px; + } + + .define-list-item .define-edit-input { + flex: 1; + height: 17px; + font-size: 13px; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + color: var(--vscode-settings-textInputForeground); + background: var(--vscode-settings-textInputBackground); + border: 1px solid var(--vscode-focusBorder); + margin-right: 4px; + } + + .define-list-item button { + padding: 2px 8px; + margin-left: 4px; + font-size: 12px; + } + + .pkg-list-item { + display: flex; + align-items: center; + padding: 4px 8px; + margin-bottom: 4px; + border: 1px solid var(--vscode-settings-textInputBorder); + background: var(--vscode-settings-textInputBackground); + border-radius: 3px; + } + + .pkg-list-item .pkg-path { + flex: 1; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .pkg-list-item .pkg-edit-input { + flex: 1; + height: 17px; + font-size: 13px; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + color: var(--vscode-settings-textInputForeground); + background: var(--vscode-settings-textInputBackground); + border: 1px solid var(--vscode-focusBorder); + } + + .pkg-list-item button { + padding: 2px 8px; + margin-left: 4px; + font-size: 12px; + } + .footer { padding: 25px; text-align: center @@ -441,6 +558,26 @@
+ +
+
MCP Server
+
+ Control the EDK2 MCP SSE server and configure workspace MCP integration. +
+
+ + +
+
+ + +
+
+ + +
+
+
DSC relative path
@@ -448,9 +585,10 @@ Main DSC file relative paths. This are the DSC files used for your platform. In your build log you will see this as Active Platform
-
One DSC path per line.
- -
+
Add .dsc files used for your platform.
+
+ +
@@ -460,9 +598,14 @@ Build definitions injected on build command. Usually this are the -D arguments on your build command
-
One Build define per line in the format: name=value
- -
+
Define/value pairs passed as -D arguments.
+
+
+ + + +
+
@@ -472,9 +615,10 @@ Workspace relative package paths. This is what you see in your build log as PACKAGES_PATH env variable.
-
One path per line.
- -
+
Add workspace-relative package paths.
+
+ +
diff --git a/src/settings/settings.ts b/src/settings/settings.ts index c079b35..9841f81 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -7,8 +7,16 @@ const elementId: { [key: string]: string } = { // Basic settings dscPaths: "dscPaths", + dscPathsList: "dscPathsList", + dscAddBtn: "dscAddBtn", buildDefines: "buildDefines", + buildDefinesList: "buildDefinesList", + buildDefinesAddBtn: "buildDefinesAddBtn", + buildDefineNewName: "buildDefineNewName", + buildDefineNewValue: "buildDefineNewValue", packagePaths: "packagePaths", + packagePathsList: "packagePathsList", + packagePathsAddBtn: "packagePathsAddBtn", }; @@ -23,6 +31,9 @@ declare function acquireVsCodeApi(): VsCodeApi; class SettingsApp { private readonly vsCodeApi: VsCodeApi; private updating: boolean = false; + private dscPaths: string[] = []; + private buildDefines: {name: string, value: string}[] = []; + private packagePaths: string[] = []; constructor() { this.vsCodeApi = acquireVsCodeApi(); @@ -32,11 +43,325 @@ class SettingsApp { // Add event listeners to UI elements this.addEventsToInputValues(); + this.addDscListEvents(); + this.addBuildDefinesEvents(); + this.addPackagePathsEvents(); + this.addMcpEvents(); this.vsCodeApi.postMessage({ command: "initialized" }); } + private addDscListEvents(): void { + document.getElementById(elementId.dscAddBtn)!.addEventListener("click", () => { + this.vsCodeApi.postMessage({ command: "selectDscFile" }); + }); + } + + // --- MCP --- + + private addMcpEvents(): void { + document.getElementById("mcpToggleBtn")!.addEventListener("click", () => { + this.vsCodeApi.postMessage({ command: "toggleMcp" }); + }); + document.getElementById("mcpAutoConfigBtn")!.addEventListener("click", () => { + this.vsCodeApi.postMessage({ command: "autoConfigureMcp" }); + }); + document.getElementById("mcpPort")!.addEventListener("change", () => { + const port = parseInt((document.getElementById("mcpPort")).value, 10); + if (port >= 1 && port <= 65535) { + this.vsCodeApi.postMessage({ command: "changeMcpPort", port }); + } + }); + } + + private updateMcpStatus(running: boolean): void { + const btn = document.getElementById("mcpToggleBtn") as HTMLButtonElement; + const status = document.getElementById("mcpStatus")!; + if (running) { + btn.textContent = "Stop MCP Server"; + status.textContent = "Running"; + status.style.color = "var(--vscode-charts-green)"; + } else { + btn.textContent = "Start MCP Server"; + status.textContent = "Stopped"; + status.style.color = "var(--vscode-foreground)"; + } + } + + private updateMcpConfigStatus(message: string): void { + document.getElementById("mcpConfigStatus")!.textContent = message; + } + + private renderDscList(): void { + const container = document.getElementById(elementId.dscPathsList)!; + container.innerHTML = ""; + this.dscPaths.forEach((path, index) => { + const item = document.createElement("div"); + item.className = "dsc-list-item"; + + const pathSpan = document.createElement("span"); + pathSpan.className = "dsc-path"; + pathSpan.textContent = path; + item.appendChild(pathSpan); + + const editBtn = document.createElement("button"); + editBtn.type = "button"; + editBtn.textContent = "Edit"; + editBtn.addEventListener("click", () => this.editDscItem(index)); + item.appendChild(editBtn); + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.textContent = "Delete"; + deleteBtn.addEventListener("click", () => this.deleteDscItem(index)); + item.appendChild(deleteBtn); + + container.appendChild(item); + }); + } + + private editDscItem(index: number): void { + const container = document.getElementById(elementId.dscPathsList)!; + const item = container.children[index] as HTMLElement; + const currentPath = this.dscPaths[index]; + + item.innerHTML = ""; + const input = document.createElement("input"); + input.type = "text"; + input.className = "dsc-edit-input"; + input.value = currentPath; + item.appendChild(input); + + const saveBtn = document.createElement("button"); + saveBtn.type = "button"; + saveBtn.textContent = "Save"; + saveBtn.addEventListener("click", () => { + this.dscPaths[index] = input.value; + this.renderDscList(); + this.onDscPathsChanged(); + }); + item.appendChild(saveBtn); + + const cancelBtn = document.createElement("button"); + cancelBtn.type = "button"; + cancelBtn.textContent = "Cancel"; + cancelBtn.addEventListener("click", () => this.renderDscList()); + item.appendChild(cancelBtn); + + input.focus(); + } + + private deleteDscItem(index: number): void { + this.dscPaths.splice(index, 1); + this.renderDscList(); + this.onDscPathsChanged(); + } + + private onDscPathsChanged(): void { + if (this.updating) { return; } + this.vsCodeApi.postMessage({ + command: "change", + config: this.collectConfig() + }); + } + + // --- Build Defines --- + + private addBuildDefinesEvents(): void { + document.getElementById(elementId.buildDefinesAddBtn)!.addEventListener("click", () => { + const nameInput = document.getElementById(elementId.buildDefineNewName); + const valueInput = document.getElementById(elementId.buildDefineNewValue); + const name = nameInput.value.trim(); + const value = valueInput.value.trim(); + if (name) { + this.buildDefines.push({ name, value }); + this.renderBuildDefinesList(); + this.onBuildDefinesChanged(); + nameInput.value = ""; + valueInput.value = ""; + } + }); + } + + private renderBuildDefinesList(): void { + const container = document.getElementById(elementId.buildDefinesList)!; + container.innerHTML = ""; + this.buildDefines.forEach((def, index) => { + const item = document.createElement("div"); + item.className = "define-list-item"; + + const nameSpan = document.createElement("span"); + nameSpan.className = "define-name"; + nameSpan.textContent = def.name; + item.appendChild(nameSpan); + + const valueSpan = document.createElement("span"); + valueSpan.className = "define-value"; + valueSpan.textContent = def.value; + item.appendChild(valueSpan); + + const editBtn = document.createElement("button"); + editBtn.type = "button"; + editBtn.textContent = "Edit"; + editBtn.addEventListener("click", () => this.editBuildDefineItem(index)); + item.appendChild(editBtn); + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.textContent = "Delete"; + deleteBtn.addEventListener("click", () => this.deleteBuildDefineItem(index)); + item.appendChild(deleteBtn); + + container.appendChild(item); + }); + } + + private editBuildDefineItem(index: number): void { + const container = document.getElementById(elementId.buildDefinesList)!; + const item = container.children[index] as HTMLElement; + const current = this.buildDefines[index]; + + item.innerHTML = ""; + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.className = "define-edit-input"; + nameInput.value = current.name; + item.appendChild(nameInput); + + const valueInput = document.createElement("input"); + valueInput.type = "text"; + valueInput.className = "define-edit-input"; + valueInput.value = current.value; + item.appendChild(valueInput); + + const saveBtn = document.createElement("button"); + saveBtn.type = "button"; + saveBtn.textContent = "Save"; + saveBtn.addEventListener("click", () => { + this.buildDefines[index] = { name: nameInput.value.trim(), value: valueInput.value.trim() }; + this.renderBuildDefinesList(); + this.onBuildDefinesChanged(); + }); + item.appendChild(saveBtn); + + const cancelBtn = document.createElement("button"); + cancelBtn.type = "button"; + cancelBtn.textContent = "Cancel"; + cancelBtn.addEventListener("click", () => this.renderBuildDefinesList()); + item.appendChild(cancelBtn); + + nameInput.focus(); + } + + private deleteBuildDefineItem(index: number): void { + this.buildDefines.splice(index, 1); + this.renderBuildDefinesList(); + this.onBuildDefinesChanged(); + } + + private onBuildDefinesChanged(): void { + if (this.updating) { return; } + this.vsCodeApi.postMessage({ + command: "change", + config: this.collectConfig() + }); + } + + // --- Package Paths --- + + private addPackagePathsEvents(): void { + document.getElementById(elementId.packagePathsAddBtn)!.addEventListener("click", () => { + this.vsCodeApi.postMessage({ command: "selectPackagePath" }); + }); + } + + private renderPackagePathsList(): void { + const container = document.getElementById(elementId.packagePathsList)!; + container.innerHTML = ""; + this.packagePaths.forEach((p, index) => { + const item = document.createElement("div"); + item.className = "pkg-list-item"; + + const pathSpan = document.createElement("span"); + pathSpan.className = "pkg-path"; + pathSpan.textContent = p; + item.appendChild(pathSpan); + + const editBtn = document.createElement("button"); + editBtn.type = "button"; + editBtn.textContent = "Edit"; + editBtn.addEventListener("click", () => this.editPackagePathItem(index)); + item.appendChild(editBtn); + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.textContent = "Delete"; + deleteBtn.addEventListener("click", () => this.deletePackagePathItem(index)); + item.appendChild(deleteBtn); + + container.appendChild(item); + }); + } + + private editPackagePathItem(index: number): void { + const container = document.getElementById(elementId.packagePathsList)!; + const item = container.children[index] as HTMLElement; + const currentPath = this.packagePaths[index]; + + item.innerHTML = ""; + const input = document.createElement("input"); + input.type = "text"; + input.className = "pkg-edit-input"; + input.value = currentPath; + item.appendChild(input); + + const saveBtn = document.createElement("button"); + saveBtn.type = "button"; + saveBtn.textContent = "Save"; + saveBtn.addEventListener("click", () => { + this.packagePaths[index] = input.value; + this.renderPackagePathsList(); + this.onPackagePathsChanged(); + }); + item.appendChild(saveBtn); + + const cancelBtn = document.createElement("button"); + cancelBtn.type = "button"; + cancelBtn.textContent = "Cancel"; + cancelBtn.addEventListener("click", () => this.renderPackagePathsList()); + item.appendChild(cancelBtn); + + input.focus(); + } + + private deletePackagePathItem(index: number): void { + this.packagePaths.splice(index, 1); + this.renderPackagePathsList(); + this.onPackagePathsChanged(); + } + + private onPackagePathsChanged(): void { + if (this.updating) { return; } + this.vsCodeApi.postMessage({ + command: "change", + config: this.collectConfig() + }); + } + + private collectConfig(): any { + const elements: NodeListOf = document.getElementsByName("inputValue"); + let config: any = {}; + elements.forEach(el => { + const data: HTMLInputElement = document.getElementById(el.id); + config[data.id] = data.value; + }); + config[elementId.dscPaths] = this.dscPaths.join("\n"); + config[elementId.buildDefines] = this.buildDefines.map(d => `${d.name}=${d.value}`).join("\n"); + config[elementId.packagePaths] = this.packagePaths.join("\n"); + return config; + } + private addEventsToInputValues(): void { const elements: NodeListOf = document.getElementsByName("inputValue"); elements.forEach(el => { @@ -66,16 +391,9 @@ class SettingsApp { return; } - const elements: NodeListOf = document.getElementsByName("inputValue"); - let config:any = {}; - elements.forEach(el => { - const data: HTMLInputElement = document.getElementById(el.id); - config[data.id]=data.value; - }); - this.vsCodeApi.postMessage({ command: "change", - config: config + config: this.collectConfig() }); } @@ -88,18 +406,56 @@ class SettingsApp { case 'updateErrors': this.updateErrors(message.errors); break; + case 'addDscFile': + if (message.path && !this.dscPaths.includes(message.path)) { + this.dscPaths.push(message.path); + this.renderDscList(); + this.onDscPathsChanged(); + } + break; + case 'addPackagePath': + if (message.path && !this.packagePaths.includes(message.path)) { + this.packagePaths.push(message.path); + this.renderPackagePathsList(); + this.onPackagePathsChanged(); + } + break; + case 'mcpStatus': + this.updateMcpStatus(message.running); + if (message.port !== undefined) { + (document.getElementById("mcpPort")).value = message.port.toString(); + } + break; + case 'mcpConfigResult': + this.updateMcpConfigStatus(message.message); + break; } } private updateConfig(config: any): void { this.updating = true; try { - const joinEntries: (input: any) => string = (input: string[]) => (input && input.length) ? input.join("\n") : ""; + // DSC paths + this.dscPaths = (config.dscPaths && config.dscPaths.length) ? [...config.dscPaths] : []; + this.renderDscList(); + + // Build defines (stored as "name=value" strings) + if (config.buildDefines && config.buildDefines.length) { + this.buildDefines = config.buildDefines.map((entry: string) => { + const eqIdx = entry.indexOf("="); + if (eqIdx >= 0) { + return { name: entry.substring(0, eqIdx), value: entry.substring(eqIdx + 1) }; + } + return { name: entry, value: "" }; + }); + } else { + this.buildDefines = []; + } + this.renderBuildDefinesList(); - // Basic settings - (document.getElementById(elementId.dscPaths)).value = joinEntries(config.dscPaths); - (document.getElementById(elementId.packagePaths)).value = joinEntries(config.packagePaths); - (document.getElementById(elementId.buildDefines)).value = joinEntries(config.buildDefines); + // Package paths + this.packagePaths = (config.packagePaths && config.packagePaths.length) ? [...config.packagePaths] : []; + this.renderPackagePathsList(); } finally { this.updating = false; diff --git a/src/settings/settingsPanel.ts b/src/settings/settingsPanel.ts index ac4a3e7..c942c7f 100644 --- a/src/settings/settingsPanel.ts +++ b/src/settings/settingsPanel.ts @@ -7,6 +7,7 @@ import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn } from "vsco import { gExtensionContext } from '../extension'; import { ConfigAgent, WorkspaceConfig, WorkspaceConfigErrors } from '../configuration'; import { askReloadFiles } from '../ui/messages'; +import { isMcpServerRunning } from '../mcp/mcpServer'; function deepCopy(obj: any) { @@ -161,6 +162,21 @@ export class SettingsPanel { this.initialized = true; this.initilizePanel(); break; + case 'selectDscFile': + this.selectDscFile(); + break; + case 'selectPackagePath': + this.selectPackagePath(); + break; + case 'toggleMcp': + this.toggleMcpServer(); + break; + case 'autoConfigureMcp': + this.autoConfigureMcp(); + break; + case 'changeMcpPort': + this.changeMcpPort(message.port); + break; } @@ -168,6 +184,8 @@ export class SettingsPanel { initilizePanel() { SettingsPanel.currentPanel!.updateWebview(this.configAgent!.getWorkspaceConfig(), this.configAgent!.getWorkspaceErrors()); + const port = vscode.workspace.getConfiguration('edk2code').get('mcpServerPort', 3100); + void this._panel.webview.postMessage({ command: 'mcpStatus', running: isMcpServerRunning(), port }); } updateConfig(message: any) { @@ -192,6 +210,102 @@ export class SettingsPanel { this.addConfigRequested.fire(name); } + private async selectDscFile(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + const defaultUri = workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0].uri : undefined; + const result = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: "Select DSC file", + defaultUri: defaultUri, + filters: { "DSC Files": ["dsc"] } + }); + if (result && result.length > 0 && workspaceFolders && workspaceFolders.length > 0) { + const relativePath = path.relative(workspaceFolders[0].uri.fsPath, result[0].fsPath).replace(/\\/g, '/'); + void this._panel.webview.postMessage({ command: 'addDscFile', path: relativePath }); + } + } + + private async selectPackagePath(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + const defaultUri = workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0].uri : undefined; + const result = await vscode.window.showOpenDialog({ + canSelectMany: false, + canSelectFolders: true, + canSelectFiles: false, + openLabel: "Select package path", + defaultUri: defaultUri, + }); + if (result && result.length > 0 && workspaceFolders && workspaceFolders.length > 0) { + const relativePath = path.relative(workspaceFolders[0].uri.fsPath, result[0].fsPath).replace(/\\/g, '/'); + void this._panel.webview.postMessage({ command: 'addPackagePath', path: relativePath }); + } + } + + private async toggleMcpServer(): Promise { + if (isMcpServerRunning()) { + await vscode.commands.executeCommand('edk2code.stopMcpServer'); + } else { + await vscode.commands.executeCommand('edk2code.startMcpServer'); + } + const port = vscode.workspace.getConfiguration('edk2code').get('mcpServerPort', 3100); + void this._panel.webview.postMessage({ command: 'mcpStatus', running: isMcpServerRunning(), port }); + } + + private async changeMcpPort(port: number): Promise { + await vscode.workspace.getConfiguration('edk2code').update('mcpServerPort', port, vscode.ConfigurationTarget.Workspace); + } + + private async autoConfigureMcp(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + void this._panel.webview.postMessage({ command: 'mcpConfigResult', message: 'No workspace folder found.' }); + return; + } + const vscodePath = path.join(workspaceFolders[0].uri.fsPath, '.vscode'); + const mcpConfigPath = path.join(vscodePath, 'mcp.json'); + const port = vscode.workspace.getConfiguration('edk2code').get('mcpServerPort', 3100); + const expectedUrl = `http://localhost:${port}/sse`; + + if (fs.existsSync(mcpConfigPath)) { + try { + const existing = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8')); + const serverEntry = existing?.servers?.edk2code; + if (serverEntry && serverEntry.url === expectedUrl) { + void this._panel.webview.postMessage({ command: 'mcpConfigResult', message: 'edk2code MCP already configured correctly.' }); + return; + } + // Add or update the edk2code entry + if (!existing.servers) { + existing.servers = {}; + } + existing.servers.edk2code = { type: "sse", url: expectedUrl }; + fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 4), 'utf-8'); + void this._panel.webview.postMessage({ command: 'mcpConfigResult', message: 'Updated edk2code entry in .vscode/mcp.json' }); + } catch { + // File exists but is not valid JSON, overwrite + const mcpConfig = { servers: { edk2code: { type: "sse", url: expectedUrl } } }; + fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 4), 'utf-8'); + void this._panel.webview.postMessage({ command: 'mcpConfigResult', message: 'Replaced invalid .vscode/mcp.json' }); + } + return; + } + + const mcpConfig = { + servers: { + "edk2code": { + "type": "sse", + "url": expectedUrl + } + } + }; + + if (!fs.existsSync(vscodePath)) { + fs.mkdirSync(vscodePath, { recursive: true }); + } + fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 4), 'utf-8'); + void this._panel.webview.postMessage({ command: 'mcpConfigResult', message: 'Created .vscode/mcp.json' }); + } + /** * Sets up an event listener to listen for messages passed from the webview context and diff --git a/src/statusBar.ts b/src/statusBar.ts index 6ffce08..bd7c84c 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -43,7 +43,7 @@ export function pushText(text:string){ export function popText(){ let text = textStack.pop(); - myStatusBarItem.text = text? text:"EDK2: Started*"; + myStatusBarItem.text = text? text:"EDK2Code"; myStatusBarItem.show(); myStatusBarItem.tooltip = ""; } diff --git a/src/symbols/README.md b/src/symbols/README.md new file mode 100644 index 0000000..3cc1290 --- /dev/null +++ b/src/symbols/README.md @@ -0,0 +1,191 @@ +# Symbols + +This folder contains the symbol type system used by the EDK II parser. Each language (DSC, INF, DEC, FDF, ASL, VFR) has its own symbol file, plus shared infrastructure. + +## File Overview + +| File | Purpose | +|---|---| +| `symbolsType.ts` | Enum of all symbol types + string mapping | +| `edkSymbols.ts` | Abstract base class `EdkSymbol` | +| `symbolFactory.ts` | Factory that instantiates the correct symbol class from a type | +| `dscSymbols.ts` | DSC-specific symbol classes | +| `infSymbols.ts` | INF-specific symbol classes | +| `decSymbols.ts` | DEC-specific symbol classes | +| `fdfSymbols.ts` | FDF-specific symbol classes | +| `aslSymbols.ts` | ASL-specific symbol classes | +| `vfrSymbols.ts` | VFR-specific symbol classes | +| `commonSymbols.ts` | Shared symbol classes (conditions, unknown) | + +--- + +## How to Add a New Symbol Type + +Adding a new symbol requires changes in **four places**. The example below follows the addition of `dscComponentSubSection` and `dscBuildOption` for DSC component scoped sub-elements (``, ``, ``). + +### 1. Add the type to `symbolsType.ts` + +Add the new enum value to `Edk2SymbolType` and a corresponding entry in `typeToStr`: + +```typescript +// symbolsType.ts +export enum Edk2SymbolType { + // ... + dscSection, + dscComponentSubSection, // <-- new + dscPcdDefinition, + dscBuildOption, // <-- new + // ... +} + +export var typeToStr: Map = new Map([ + // ... + [Edk2SymbolType.dscComponentSubSection, "dscComponentSubSection"], + [Edk2SymbolType.dscBuildOption, "dscBuildOption"], + // ... +]); +``` + +### 2. Create the symbol class in the appropriate `*Symbols.ts` file + +Extend `EdkSymbol` and set at minimum `type` and `kind`. Implement `onDefinition`, `onCompletion`, etc. if the symbol needs navigation or hover support. + +```typescript +// dscSymbols.ts +export class EdkSymbolDscComponentSubSection extends EdkSymbol { + type = Edk2SymbolType.dscComponentSubSection; + kind = vscode.SymbolKind.Namespace; + + onCompletion: undefined; + onDefinition: undefined; + onHover: undefined; + onDeclaration: undefined; +} + +export class EdkSymbolDscBuildOption extends EdkSymbol { + type = Edk2SymbolType.dscBuildOption; + kind = vscode.SymbolKind.Property; + + onCompletion: undefined; + onDefinition: undefined; + onHover: undefined; + onDeclaration: undefined; +} +``` + +Optionally override `nameRegex` (defined on the base `EdkSymbol`) to extract a cleaner display name from the raw text line: + +```typescript +// Example from EdkSymbolDscModuleDefinition +protected get nameRegex(): RegExp { return /^\s*([\w/.\\-]+\.inf)/i; } +``` + +When `nameRegex` is set, the constructor uses the first capture group (or the full match) as `this.name` instead of the raw text line. + +### 3. Register the type in `symbolFactory.ts` + +Import the new class and add a `case` to `produceSymbol()`: + +```typescript +// symbolFactory.ts +import { /* existing */, EdkSymbolDscComponentSubSection, EdkSymbolDscBuildOption } from "./dscSymbols"; + +produceSymbol(type: Edk2SymbolType, textLine: string, location: vscode.Location, parser: DocumentParser) { + switch (type) { + // ... + case Edk2SymbolType.dscComponentSubSection: + return new EdkSymbolDscComponentSubSection(textLine, location, true, true, parser); + case Edk2SymbolType.dscBuildOption: + return new EdkSymbolDscBuildOption(textLine, location, true, true, parser); + // ... + } +} +``` + +### 4. Add a `BlockParser` in the relevant parser file + +In `src/edkParser/`, create a `BlockParser` subclass that sets `type` to the new enum value and define `tag` / `end` / `context` as needed. Then add it to the `context` array of the parent block. + +```typescript +// dscParser.ts +class BlockBuildOption extends BlockParser { + name = "BuildOption"; + tag = /^[\w\*\:\|]+\s*=\s*.*/gi; + start = undefined; + end = undefined; + type = Edk2SymbolType.dscBuildOption; + visible: boolean = true; +} + +class BlockComponentSubBuildOptions extends BlockParser { + name = "ComponentBuildOptions"; + tag = /^<\s*BuildOptions\s*>/gi; + start = undefined; + end = /(^<)|(^\})/gi; + type = Edk2SymbolType.dscComponentSubSection; + visible: boolean = true; + context: BlockParser[] = [ + new BlockBuildOption(), + new BlockIncludes(), + ]; +} + +// Wire into the parent block's context: +class BlockComponentInf extends BlockParser { + // ... + context: BlockParser[] = [ + new BlockComponentSubBuildOptions(), + // ... + ]; +} +``` + +If the new type should also appear as a root-level fallback (so lines are captured even when they appear outside any section), register an additional `isRoot = true` instance at the end of `DscParser.blockParsers`: + +```typescript +blockParsers: BlockParser[] = [ + new BlockBuildOptionsSection(), // normal section block + // ... + new BlockBuildOption(true), // isRoot fallback +]; +``` + +### 5. Register the type in `WorkspaceTreeProvider.ts` + +The workspace tree view (`src/workspaceTree/WorkspaceTreeProvider.ts`) uses `DSC_FILTER_TYPES` to control which symbol types are rendered. Any new type that should appear in the tree **must** be added here — both for top-level rendering and for collapsible-state detection on parent nodes. + +```typescript +// WorkspaceTreeProvider.ts +export const DSC_FILTER_TYPES: { type: Edk2SymbolType; label: string; description: string }[] = [ + // ... + { type: Edk2SymbolType.dscBuildOptionsSection, label: 'Build options', description: 'dscBuildOptionsSection' }, + { type: Edk2SymbolType.dscBuildOption, label: 'Build option entries', description: 'dscBuildOption' }, + // ... +]; +``` + +This array serves three roles: +1. **Root filter** — only symbols whose type is in this set are shown at the workspace root level. +2. **Child filter** — `getChildren()` uses it to filter children of every tree node. +3. **Collapsible state** — `DocumentSymbolItem` counts `visibleChildren` against this set to decide whether a node should be expandable. + +Omitting a type from `DSC_FILTER_TYPES` silently hides the symbol and all its children in the tree, even if parsing logs show "Added symbol" correctly. + +--- + +## `EdkSymbol` base class + +All symbol classes extend `EdkSymbol` (defined in `edkSymbols.ts`), which itself extends `vscode.DocumentSymbol`. Key members: + +| Member | Description | +|---|---| +| `type` | The `Edk2SymbolType` enum value | +| `kind` | The `vscode.SymbolKind` used for the outline/breadcrumb icon | +| `textLine` | The raw source line (with defines resolved via `parser.defines`) | +| `location` | `vscode.Location` (URI + range) | +| `sectionProperties` | Inherited from the parent symbol's section context | +| `nameRegex` | Optional getter; when set, extracts the symbol's display name from `textLine` | +| `onDefinition` | Async function returning locations for go-to-definition | +| `onCompletion` | Async function returning completion items | +| `onHover` | Async function returning hover content | +| `onDeclaration` | Async function returning declaration locations | diff --git a/src/symbols/dscSymbols.ts b/src/symbols/dscSymbols.ts index 1abf6c0..852497d 100644 --- a/src/symbols/dscSymbols.ts +++ b/src/symbols/dscSymbols.ts @@ -30,6 +30,12 @@ export class EdkSymbolDscDefine extends EdkSymbol{ onHover: undefined; onDeclaration: undefined; + + protected get nameRegex(): RegExp { return /define\s+(\w+).*=.*/i; } + protected get descriptionRegex(): RegExp { return /define\s+\w+\s*=\s*(.*)/i; } + + + async getKey() { let key = this.textLine.replace(/define/gi, "").trim(); key = split(key, "=", 2)[0].trim(); @@ -47,7 +53,10 @@ export class EdkSymbolDscDefine extends EdkSymbol{ export class EdkSymbolDscLibraryDefinition extends EdkSymbol{ type = Edk2SymbolType.dscLibraryDefinition; - kind = vscode.SymbolKind.Module; + kind = vscode.SymbolKind.Field; + + protected get descriptionRegex(): RegExp { return /.*\|(.*)/i; } + protected get nameRegex(): RegExp { return /(.*)\|.*/i; } onCompletion: undefined; onDefinition = async (parser:DocumentParser)=>{ @@ -99,9 +108,41 @@ export class EdkSymbolDscLine extends EdkSymbol{ onDeclaration: undefined; } +export class EdkSymbolDscBuildOptionsSection extends EdkSymbol { + type = Edk2SymbolType.dscBuildOptionsSection; + kind = vscode.SymbolKind.Class; + + onCompletion: undefined; + onDefinition: undefined; + onHover: undefined; + onDeclaration: undefined; +} + +export class EdkSymbolDscComponentSubSection extends EdkSymbol { + type = Edk2SymbolType.dscComponentSubSection; + kind = vscode.SymbolKind.Namespace; + + onCompletion: undefined; + onDefinition: undefined; + onHover: undefined; + onDeclaration: undefined; +} + +export class EdkSymbolDscBuildOption extends EdkSymbol { + type = Edk2SymbolType.dscBuildOption; + kind = vscode.SymbolKind.Property; + + onCompletion: undefined; + onDefinition: undefined; + onHover: undefined; + onDeclaration: undefined; +} + export class EdkSymbolDscModuleDefinition extends EdkSymbol{ type = Edk2SymbolType.dscModuleDefinition; - kind = vscode.SymbolKind.Event; + kind = vscode.SymbolKind.Method; + + protected get nameRegex(): RegExp { return /.*?\.inf/i; } onCompletion: undefined; onDefinition = async ()=>{ diff --git a/src/symbols/edkSymbols.ts b/src/symbols/edkSymbols.ts index 8fac23c..4398bcf 100644 --- a/src/symbols/edkSymbols.ts +++ b/src/symbols/edkSymbols.ts @@ -3,8 +3,12 @@ import { gDebugLog } from '../extension'; import { Edk2SymbolType, typeToStr } from './symbolsType'; import { DocumentParser } from '../edkParser/languageParser'; import { SectionProperties } from '../index/edkWorkspace'; +import { getParserForDocument } from '../edkParser/parserFactory'; +import path = require('path'); +import { debuglog } from 'util'; +import { log } from 'console'; - +let decSymbolCache = new Map(); export abstract class EdkSymbol extends vscode.DocumentSymbol { @@ -26,8 +30,16 @@ export abstract class EdkSymbol extends vscode.DocumentSymbol { guid:string = ""; parser:DocumentParser; + + /** Override in subclasses to extract a specific portion of the text line as the symbol name. */ + protected get nameRegex(): RegExp | undefined { return undefined; } + + /** Override in subclasses to extract a description (detail) from the text line. */ + protected get descriptionRegex(): RegExp | undefined { return undefined; } + + protected _textLine: string; public get textLine(): string { return this.parser.defines.replaceDefines(this._textLine); @@ -39,6 +51,21 @@ export abstract class EdkSymbol extends vscode.DocumentSymbol { updateRange(range:vscode.Range){ this.location.range = range; this.range = range; + // Trim selectionRange to the uncommented, non-whitespace content on the first line. + // _textLine is already the trimmed, comment-free text from parsing. + const lineStart = range.start.line; + const content = this._textLine; + if (content.length > 0 && lineStart < this.parser.document.lineCount) { + const rawLine = this.parser.document.lineAt(lineStart).text; + const idx = rawLine.indexOf(content); + if (idx >= 0) { + this.selectionRange = new vscode.Range( + lineStart, idx, + lineStart, idx + content.length + ); + return; + } + } this.selectionRange = range; } @@ -57,15 +84,77 @@ export abstract class EdkSymbol extends vscode.DocumentSymbol { this.location = location; this._textLine = textLine; this.parser = parser; - this.name = textLine.replaceAll(/\s+/gi," "); + const regex = this.nameRegex; + if (regex) { + const match = textLine.match(regex); + this.name = match ? (match[1] ?? match[0]).trim() : textLine.trim(); + } else { + this.name = textLine.replaceAll(/\s+/gi, " "); + } + + const descRegex = this.descriptionRegex; + if (descRegex) { + const descMatch = textLine.match(descRegex); + this.detail = descMatch ? (descMatch[1] ?? descMatch[0]).trim() : ''; + } + let parent = parser.symbolStack[parser.symbolStack.length - 1]; this.sectionProperties = new SectionProperties(); if(parent){ this.sectionProperties = parent.sectionProperties; } - gDebugLog.verbose(`Symbol Created: ${location.range.start.line}: ${this.toString()}`); + gDebugLog.trace(`Symbol Created: ${location.range.start.line}: ${this.toString()}`); } + async decCompletion(type:Edk2SymbolType, completionKind:vscode.CompletionItemKind=vscode.CompletionItemKind.File){ + let retData = []; + let decs = this.parser.getSymbolsType(Edk2SymbolType.infPackage); + for (const dec of decs) { + let decTextPath = await dec.getValue(); + let document = await vscode.workspace.openTextDocument(vscode.Uri.file(decTextPath)); + let decParser = await getParserForDocument(document); + if(decParser){ + let decPpis = decParser.getSymbolsType(type); + for (const decPpi of decPpis) { + let decPpiValue = await decPpi.getKey(); + retData.push(new vscode.CompletionItem({label:decPpiValue, detail:" " + path.basename(decPpi.location.uri.fsPath), description:""}, completionKind)); + } + } + } + return retData; + } + + async isInDec(type:Edk2SymbolType){ + let decs = this.parser.getSymbolsType(Edk2SymbolType.infPackage); + const testKey = await this.getKey(); + + + for (const dec of decs) { + let decTextPath = await dec.getValue(); + if(decSymbolCache.has(testKey)){ + let cachedPath = decSymbolCache.get(testKey); + if(cachedPath === decTextPath){ + return true; + } + } + let document = await vscode.workspace.openTextDocument(vscode.Uri.file(decTextPath)); + let decParser = await getParserForDocument(document); + if(decParser){ + let decSymbols = decParser.getSymbolsType(type); + for (const decSymbol of decSymbols) { + + const symbolKey = await decSymbol.getKey(); + decSymbolCache.set(symbolKey, decTextPath); + if(symbolKey === testKey){ + return true; + } + } + } + } + return false; + } + + toString() { return `(${this.range.start.line + 1},${this.range.start.character}),(${this.range.end.line + 1},${this.range.end.character})(${this.typeToString()}): ${this.name}`; } @@ -76,14 +165,50 @@ export abstract class EdkSymbol extends vscode.DocumentSymbol { } async getValue(){ - gDebugLog.error(`getValue not implemented for ${this.type}`); + // gDebugLog.warning(`getValue not implemented for ${this.type}`); return this.textLine; } async getKey(){ - gDebugLog.error(`getKey not implemented for ${this.type}`); + // gDebugLog.warning(`getKey not implemented for ${this.type}`); return this.textLine; } + static iconForKind(kind: vscode.SymbolKind): vscode.ThemeIcon { + const map: Partial> = { + [vscode.SymbolKind.File]: { icon: 'symbol-file', color: 'symbolIcon.fileForeground' }, + [vscode.SymbolKind.Module]: { icon: 'symbol-module', color: 'symbolIcon.moduleForeground' }, + [vscode.SymbolKind.Namespace]: { icon: 'symbol-namespace', color: 'symbolIcon.namespaceForeground' }, + [vscode.SymbolKind.Package]: { icon: 'symbol-package', color: 'symbolIcon.packageForeground' }, + [vscode.SymbolKind.Class]: { icon: 'symbol-class', color: 'symbolIcon.classForeground' }, + [vscode.SymbolKind.Method]: { icon: 'symbol-method', color: 'symbolIcon.methodForeground' }, + [vscode.SymbolKind.Property]: { icon: 'symbol-property', color: 'symbolIcon.propertyForeground' }, + [vscode.SymbolKind.Field]: { icon: 'symbol-field', color: 'symbolIcon.fieldForeground' }, + [vscode.SymbolKind.Constructor]: { icon: 'symbol-constructor', color: 'symbolIcon.constructorForeground' }, + [vscode.SymbolKind.Enum]: { icon: 'symbol-enum', color: 'symbolIcon.enumeratorForeground' }, + [vscode.SymbolKind.Interface]: { icon: 'symbol-interface', color: 'symbolIcon.interfaceForeground' }, + [vscode.SymbolKind.Function]: { icon: 'symbol-function', color: 'symbolIcon.functionForeground' }, + [vscode.SymbolKind.Variable]: { icon: 'symbol-variable', color: 'symbolIcon.variableForeground' }, + [vscode.SymbolKind.Constant]: { icon: 'symbol-constant', color: 'symbolIcon.constantForeground' }, + [vscode.SymbolKind.String]: { icon: 'symbol-string', color: 'symbolIcon.stringForeground' }, + [vscode.SymbolKind.Number]: { icon: 'symbol-number', color: 'symbolIcon.numberForeground' }, + [vscode.SymbolKind.Boolean]: { icon: 'symbol-boolean', color: 'symbolIcon.booleanForeground' }, + [vscode.SymbolKind.Array]: { icon: 'symbol-array', color: 'symbolIcon.arrayForeground' }, + [vscode.SymbolKind.Object]: { icon: 'symbol-object', color: 'symbolIcon.objectForeground' }, + [vscode.SymbolKind.Key]: { icon: 'symbol-key', color: 'symbolIcon.keyForeground' }, + [vscode.SymbolKind.Null]: { icon: 'symbol-null', color: 'symbolIcon.nullForeground' }, + [vscode.SymbolKind.EnumMember]: { icon: 'symbol-enum-member', color: 'symbolIcon.enumeratorMemberForeground' }, + [vscode.SymbolKind.Struct]: { icon: 'symbol-struct', color: 'symbolIcon.structForeground' }, + [vscode.SymbolKind.Event]: { icon: 'symbol-event', color: 'symbolIcon.eventForeground' }, + [vscode.SymbolKind.Operator]: { icon: 'symbol-operator', color: 'symbolIcon.operatorForeground' }, + [vscode.SymbolKind.TypeParameter]: { icon: 'symbol-type-parameter', color: 'symbolIcon.typeParameterForeground' }, + }; + const entry = map[kind]; + if (entry) { + return new vscode.ThemeIcon(entry.icon, new vscode.ThemeColor(entry.color)); + } + return new vscode.ThemeIcon('symbol-misc'); + } + } diff --git a/src/symbols/fdfSymbols.ts b/src/symbols/fdfSymbols.ts index 605ae7c..66e5647 100644 --- a/src/symbols/fdfSymbols.ts +++ b/src/symbols/fdfSymbols.ts @@ -4,6 +4,7 @@ import * as vscode from 'vscode'; import { Edk2SymbolType } from "./symbolsType"; import { WorkspaceDefinitions } from "../index/definitions"; import { DocumentParser } from "../edkParser/languageParser"; +import * as path from 'path'; export class EdkSymbolDscSection extends EdkSymbol { type = Edk2SymbolType.dscSection; @@ -28,13 +29,14 @@ export class EdkSymbolFdfSection extends EdkSymbol { export class EdkSymbolFdfInf extends EdkSymbol { type = Edk2SymbolType.fdfInf; - kind = vscode.SymbolKind.Event; + kind = vscode.SymbolKind.Method; onCompletion: undefined; onDefinition = async (parser:DocumentParser)=>{ - let path = await this.getValue(); - return await gPathFind.findPath(path); + let pathValue = await this.getValue(); + let relPath = path.dirname(parser.document.uri.fsPath); + return await gPathFind.findPath(pathValue, relPath); }; async getValue(){ @@ -75,8 +77,9 @@ export class EdkSymbolFdfInclude extends EdkSymbol { onCompletion: undefined; onDefinition = async (parser:DocumentParser)=>{ - let path = await this.getValue(); - return await gPathFind.findPath(path); + let pathValue = await this.getValue(); + let relPath = path.dirname(parser.document.uri.fsPath); + return await gPathFind.findPath(pathValue, relPath); }; async getValue(){ diff --git a/src/symbols/infSymbols.ts b/src/symbols/infSymbols.ts index d942edc..f813dde 100644 --- a/src/symbols/infSymbols.ts +++ b/src/symbols/infSymbols.ts @@ -8,7 +8,7 @@ import { isFileEdkLibrary, listFiles, listFilesRecursive, openTextDocument, spli import { InfDsc } from "../index/edkWorkspace"; import { DocumentParser } from "../edkParser/languageParser"; import * as fs from 'fs'; -import { ParserFactory } from "../edkParser/parserFactory"; +import { getParserForDocument } from "../edkParser/parserFactory"; import { REGEX_INF_SECTION } from "../edkParser/commonParser"; @@ -82,7 +82,7 @@ export class EdkSymbolInfSectionProtocols extends InfSection { kind = vscode.SymbolKind.Class; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decProtocol, vscode.CompletionItemKind.Interface); + return this.decCompletion(Edk2SymbolType.decProtocol, vscode.CompletionItemKind.Interface); }; onDefinition: undefined; onHover: undefined; @@ -96,7 +96,7 @@ export class EdkSymbolInfPpi extends EdkSymbol { kind = vscode.SymbolKind.Event; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decPpi, vscode.CompletionItemKind.Interface); + return this.decCompletion(Edk2SymbolType.decPpi, vscode.CompletionItemKind.Interface); }; onDefinition = async (parser:DocumentParser)=>{ @@ -112,7 +112,7 @@ export class EdkSymbolInfSectionPpis extends InfSection { kind = vscode.SymbolKind.Class; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decPpi, vscode.CompletionItemKind.Interface); + return this.decCompletion(Edk2SymbolType.decPpi, vscode.CompletionItemKind.Interface); }; onDefinition: undefined; onHover: undefined; @@ -124,7 +124,7 @@ export class EdkSymbolInfSectionGuids extends InfSection { kind = vscode.SymbolKind.Class; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decGuid, vscode.CompletionItemKind.Constant); + return this.decCompletion(Edk2SymbolType.decGuid, vscode.CompletionItemKind.Constant); }; onDefinition: undefined; onHover: undefined; @@ -137,7 +137,7 @@ export class EdkSymbolinfSectionDepex extends InfSection { kind = vscode.SymbolKind.Class; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decGuid, vscode.CompletionItemKind.Constant); + return this.decCompletion(Edk2SymbolType.decGuid, vscode.CompletionItemKind.Constant); }; onDefinition: undefined; onHover: undefined; @@ -150,7 +150,7 @@ export class EdkSymbolInfSectionPcds extends InfSection { kind = vscode.SymbolKind.Class; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decPcd, vscode.CompletionItemKind.Constant); + return this.decCompletion(Edk2SymbolType.decPcd, vscode.CompletionItemKind.Constant); }; onDefinition: undefined; onHover: undefined; @@ -424,7 +424,7 @@ export class EdkSymbolInfProtocol extends EdkSymbol { kind = vscode.SymbolKind.Event; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decProtocol, vscode.CompletionItemKind.Interface); + return this.decCompletion(Edk2SymbolType.decProtocol, vscode.CompletionItemKind.Interface); }; onDefinition = async (parser:DocumentParser)=>{ return decDefinition(this, Edk2SymbolType.decProtocol); @@ -438,7 +438,7 @@ export class EdkSymbolInfPcd extends EdkSymbol { kind = vscode.SymbolKind.String; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decPcd, vscode.CompletionItemKind.Constant); + return this.decCompletion(Edk2SymbolType.decPcd, vscode.CompletionItemKind.Constant); }; onDefinition = async (parser:DocumentParser)=>{ return decDefinition(this, Edk2SymbolType.decPcd); @@ -452,7 +452,7 @@ export class EdkSymbolInfGuid extends EdkSymbol { kind = vscode.SymbolKind.Number; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decGuid, vscode.CompletionItemKind.Constant); + return this.decCompletion(Edk2SymbolType.decGuid, vscode.CompletionItemKind.Constant); }; onDefinition = async (parser:DocumentParser)=>{ return decDefinition(this,Edk2SymbolType.decGuid); @@ -466,7 +466,7 @@ export class EdkSymbolInfDepex extends EdkSymbol { kind = vscode.SymbolKind.Property; onCompletion = async (parser:DocumentParser)=>{ - return decCompletion(this, Edk2SymbolType.decPpi, vscode.CompletionItemKind.Interface); + return this.decCompletion(Edk2SymbolType.decPpi, vscode.CompletionItemKind.Interface); }; onDefinition = async (parser:DocumentParser)=>{ @@ -497,37 +497,16 @@ export class EdkSymbolInfFunction extends EdkSymbol { } -async function decCompletion(thisSymbol:EdkSymbol, type:Edk2SymbolType, completionKind:vscode.CompletionItemKind=vscode.CompletionItemKind.File){ - let retData = []; - let factory = new ParserFactory(); - let thisPpi = await thisSymbol.getKey(); - let decs = thisSymbol.parser.getSymbolsType(Edk2SymbolType.infPackage); - for (const dec of decs) { - let decTextPath = await dec.getValue(); - let document = await vscode.workspace.openTextDocument(vscode.Uri.file(decTextPath)); - let decParser = factory.getParser(document); - if(decParser){ - await decParser.parseFile(); - let decPpis = decParser.getSymbolsType(type); - for (const decPpi of decPpis) { - let decPpiValue = await decPpi.getKey(); - retData.push(new vscode.CompletionItem({label:decPpiValue, detail:" " + path.basename(decPpi.location.uri.fsPath), description:""}, completionKind)); - } - } - } - return retData; -} + async function decDefinition(thisSymbol:EdkSymbol, type:Edk2SymbolType){ - let factory = new ParserFactory(); let thisPpi = await thisSymbol.getKey(); let decs = thisSymbol.parser.getSymbolsType(Edk2SymbolType.infPackage); for (const dec of decs) { let decTextPath = await dec.getValue(); let document = await openTextDocument(vscode.Uri.file(decTextPath)); - let decParser = factory.getParser(document); + let decParser = await getParserForDocument(document); if(decParser){ - await decParser.parseFile(); let decPpis = decParser.getSymbolsType(type); for (const decPpi of decPpis) { let decPpiValue = await decPpi.getKey(); diff --git a/src/symbols/symbolFactory.ts b/src/symbols/symbolFactory.ts index abd76d3..8bef072 100644 --- a/src/symbols/symbolFactory.ts +++ b/src/symbols/symbolFactory.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { gDebugLog } from "../extension"; -import { EdkSymbolDscDefine, EdkSymbolDscInclude, EdkSymbolDscLibraryDefinition, EdkSymbolDscLine, EdkSymbolDscModuleDefinition, EdkSymbolDscPcdDefinition, EdkSymbolDscSection } from "./dscSymbols"; +import { EdkSymbolDscDefine, EdkSymbolDscInclude, EdkSymbolDscLibraryDefinition, EdkSymbolDscLine, EdkSymbolDscModuleDefinition, EdkSymbolDscPcdDefinition, EdkSymbolDscSection, EdkSymbolDscBuildOptionsSection, EdkSymbolDscComponentSubSection, EdkSymbolDscBuildOption } from "./dscSymbols"; import { EdkSymbolFdfSection, EdkSymbolFdfInf, EdkSymbolFdfDefinition, EdkSymbolFdfFile, EdkSymbolFdfInclude } from './fdfSymbols'; import { EdkSymbolInfSectionLibraries, EdkSymbolInfSectionProtocols, EdkSymbolInfSectionPpis, EdkSymbolInfSectionGuids, EdkSymbolInfSectionPcds, EdkSymbolInfSection, EdkSymbolInfDefine, EdkSymbolInfSource, EdkSymbolInfLibrary, EdkSymbolInfPackage, EdkSymbolInfPpi, EdkSymbolInfProtocol, EdkSymbolInfPcd, EdkSymbolInfGuid, EdkSymbolInfDepex, EdkSymbolInfBinary, EdkSymbolInfFunction, EdkSymbolInfSectionSource, EdkSymbolSectionPackages, EdkSymbolinfSectionDepex } from './infSymbols'; import { EdkSymbolDecSection, EdkSymbolDecDefine, EdkSymbolDecLibrary, EdkSymbolDecPackage, EdkSymbolDecPpi, EdkSymbolDecProtocol, EdkSymbolDecPcd, EdkSymbolDecGuid, EdkSymbolDecIncludes } from './decSymbols'; @@ -20,6 +20,8 @@ export class SymbolFactory { switch (type) { case Edk2SymbolType.dscSection: return new EdkSymbolDscSection(textLine, location, true, true, parser); + case Edk2SymbolType.dscBuildOptionsSection: + return new EdkSymbolDscBuildOptionsSection(textLine, location, true, true, parser); case Edk2SymbolType.dscDefine: return new EdkSymbolDscDefine(textLine, location, true, true, parser); case Edk2SymbolType.dscLibraryDefinition: @@ -32,6 +34,10 @@ export class SymbolFactory { return new EdkSymbolDscInclude(textLine, location, true, true, parser); case Edk2SymbolType.dscLine: return new EdkSymbolDscLine(textLine, location, true, true, parser); + case Edk2SymbolType.dscComponentSubSection: + return new EdkSymbolDscComponentSubSection(textLine, location, true, true, parser); + case Edk2SymbolType.dscBuildOption: + return new EdkSymbolDscBuildOption(textLine, location, true, true, parser); case Edk2SymbolType.infSectionSource: return new EdkSymbolInfSectionSource(textLine, location, true, true, parser); case Edk2SymbolType.infSectionPackages: diff --git a/src/symbols/symbolsType.ts b/src/symbols/symbolsType.ts index 89b3ed9..c2f179e 100644 --- a/src/symbols/symbolsType.ts +++ b/src/symbols/symbolsType.ts @@ -4,7 +4,10 @@ export enum Edk2SymbolType { dscLibraryDefinition, dscModuleDefinition, dscSection, + dscBuildOptionsSection, + dscComponentSubSection, dscPcdDefinition, + dscBuildOption, dscInclude, dscLine, infDefine, @@ -61,6 +64,8 @@ export enum Edk2SymbolType { fdfInclude, condition, unknown, + /** Sentinel used by the workspace tree filter to toggle visibility of inactive nodes. */ + showInactiveNodes, } export var typeToStr: Map = new Map( @@ -69,7 +74,10 @@ export var typeToStr: Map = new Map( [Edk2SymbolType.dscLibraryDefinition, "dscLibraryDefinition"], [Edk2SymbolType.dscModuleDefinition, "dscModuleDefinition"], [Edk2SymbolType.dscSection, "dscSection"], + [Edk2SymbolType.dscBuildOptionsSection, "dscBuildOptionsSection"], + [Edk2SymbolType.dscComponentSubSection, "dscComponentSubSection"], [Edk2SymbolType.dscPcdDefinition, "dscPcdDefinition"], + [Edk2SymbolType.dscBuildOption, "dscBuildOption"], [Edk2SymbolType.dscInclude, "dscInclude"], [Edk2SymbolType.dscLine, "dscLine"], [Edk2SymbolType.infDefine, "infDefine"], diff --git a/src/ternarySearchTree.ts b/src/ternarySearchTree.ts new file mode 100644 index 0000000..def3846 --- /dev/null +++ b/src/ternarySearchTree.ts @@ -0,0 +1,155 @@ +import * as vscode from 'vscode'; +import { gDebugLog, gWorkspacePath } from './extension'; + +/** + * A node in the Ternary Search Tree. + * Each node stores a single character and branches into three children: + * left – characters less than this node's character + * mid – next character in the same key + * right – characters greater than this node's character + * + * When `isEnd` is true the path from the root through mid-pointers + * spells out a complete key, and `values` holds the associated URIs. + */ +interface TSTNode { + char: string; + left: TSTNode | null; + mid: TSTNode | null; + right: TSTNode | null; + isEnd: boolean; + values: vscode.Uri[]; +} + +/** + * Ternary Search Tree used as a temporary in-memory file index. + * + * Built when the workspace is cleared and being rebuilt so that + * `PathFind.findPath` can resolve file names without waiting for + * `vscode.workspace.findFiles` (which may be slow / stale during + * a rebuild). + * + * Keys are **lower-cased file basenames** so look-ups are + * case-insensitive. Each key maps to one or more `vscode.Uri`s. + */ +export class TernarySearchTree { + private root: TSTNode | null = null; + private _size: number = 0; + + /** Number of unique keys stored in the tree. */ + get size(): number { + return this._size; + } + + // ── Insertion ──────────────────────────────────────────────── + + /** + * Insert a key → value pair into the tree. + * If the key already exists the value is appended to its list. + */ + insert(key: string, value: vscode.Uri): void { + if (key.length === 0) { return; } + this.root = this._insert(this.root, key, value, 0); + } + + private _insert(node: TSTNode | null, key: string, value: vscode.Uri, idx: number): TSTNode { + const ch = key[idx]; + + if (node === null) { + node = { char: ch, left: null, mid: null, right: null, isEnd: false, values: [] }; + } + + if (ch < node.char) { + node.left = this._insert(node.left, key, value, idx); + } else if (ch > node.char) { + node.right = this._insert(node.right, key, value, idx); + } else if (idx < key.length - 1) { + node.mid = this._insert(node.mid, key, value, idx + 1); + } else { + if (!node.isEnd) { + node.isEnd = true; + this._size++; + } + node.values.push(value); + } + + return node; + } + + // ── Exact-match search ─────────────────────────────────────── + + /** + * Return all URIs associated with `key`, or an empty array if not found. + */ + search(key: string): vscode.Uri[] { + if (key.length === 0) { return []; } + const node = this._search(this.root, key, 0); + if (node && node.isEnd) { + return node.values; + } + return []; + } + + private _search(node: TSTNode | null, key: string, idx: number): TSTNode | null { + if (node === null) { return null; } + + const ch = key[idx]; + + if (ch < node.char) { + return this._search(node.left, key, idx); + } else if (ch > node.char) { + return this._search(node.right, key, idx); + } else if (idx < key.length - 1) { + return this._search(node.mid, key, idx + 1); + } else { + return node; + } + } + + // ── Bulk build ─────────────────────────────────────────────── + + /** + * Scan *all* files in the workspace and index them by their + * lower-cased basename. Returns the number of files indexed. + */ + async buildFromWorkspace(): Promise { + const startTime = Date.now(); + gDebugLog.info('TernarySearchTree: building file index from workspace…'); + + // findFiles with ** grabs every file in the workspace + const allFiles = await vscode.workspace.findFiles('**/*', null); + + for (const uri of allFiles) { + const baseName = this.baseName(uri); + this.insert(baseName, uri); + } + + const elapsed = Date.now() - startTime; + gDebugLog.info(`TernarySearchTree: indexed ${allFiles.length} files (${this._size} unique names) in ${elapsed} ms`); + return allFiles.length; + } + + // ── Helpers ────────────────────────────────────────────────── + + /** + * Return the lower-cased basename of a URI (case-insensitive keys). + */ + private baseName(uri: vscode.Uri): string { + const segments = uri.fsPath.split(/[\\/]/); + return (segments[segments.length - 1] || '').toLowerCase(); + } + + /** + * Look up a file by its basename string (case-insensitive). + */ + findFilesByName(fileName: string): vscode.Uri[] { + return this.search(fileName.toLowerCase()); + } + + /** + * Clear the tree and release memory. + */ + dispose(): void { + this.root = null; + this._size = 0; + } +} diff --git a/src/test/suite/README.md b/src/test/suite/README.md new file mode 100644 index 0000000..b9876d6 --- /dev/null +++ b/src/test/suite/README.md @@ -0,0 +1,461 @@ +# Test Suite + +Integration tests for the Edk2Code VS Code extension. +Tests run under Mocha (TDD UI) via `@vscode/test-electron`. + +## Running Tests + +- **VS Code**: Use the **Extension Tests** launch configuration (F5). +- **CLI**: `node ./out/test/runTest.js` (bypasses lint). + +## Fixture Files + +Test fixtures live in the `test/` folder at the repository root: + +| Fixture | Used By | +|---------|---------| +| `testDscParsing.dsc` | `dscParser.test.ts`, `edkWorkspace.test.ts` | +| `testDecParsing.dec` | `decParser.test.ts` | +| `testFdfParsing.fdf` | `fdfParser.test.ts`, `edkWorkspace.test.ts` | +| `testInfParsing.inf` | `infParser.test.ts` | +| `testAslParsing.asl` | `aslParser.test.ts` | +| `testVfrParsing.vfr` | `vfrParser.test.ts` | +| `testDscProcess.dsc` | `edkWorkspaceProcess.test.ts` | + +--- + +## Test Files — Detailed Breakdown + +### `extension.test.ts` + +**Suite: Extension Test Suite** — Smoke test that validates the test harness is working. + +| # | Test | Description | +|---|------|-------------| +| 1 | Sample test | Asserts `Array.indexOf` returns `-1` for missing elements. Proves the Mocha runner and VS Code test host are functional. | + +--- + +### `dscParser.test.ts` + +**Suite: DSC Parser – Symbol Extraction** — Parses `test/testDscParsing.dsc` with `DscParser` and validates that all DSC symbol types are correctly extracted. The fixture includes `[Defines]`, `[SkuIds]`, `[LibraryClasses]` (common + `.X64`), `[Components]` (common + `.X64` with sub-sections), PCD sections, `[BuildOptions]`, root-level `DEFINE`, `!include`, and block comments. + +| # | Test | Description | +|---|------|-------------| +| 1 | Parses [Defines] section | At least one `dscSection` symbol with name matching "defines". | +| 2 | Parses [LibraryClasses] sections (common + arch-specific) | At least 2 `dscSection` symbols for `LibraryClasses` (common and `.X64`). | +| 3 | Parses [Components] and [Components.X64] sections | At least 2 `dscSection` symbols for `Components`. | +| 4 | Parses [SkuIds] section | Exactly 1 `dscSection` symbol for `SkuIds`. | +| 5 | Parses [BuildOptions] section | At least 1 `dscBuildOptionsSection` symbol. | +| 6 | Parses PCD sections (FixedAtBuild + DynamicDefault) | At least 2 `dscSection` symbols with names matching "pcd". | +| 7 | Parses DEFINE statements inside [Defines] | At least 3 `dscDefine` symbols (`DEFINE MY_VAR`, `DEFINE EMPTY_VAR`, `ROOT_DEFINE`). | +| 8 | DEFINE with empty value is still parsed | Finds `EMPTY_VAR` in the define list (covers `DEFINE EMPTY_VAR=`). | +| 9 | Root-level DEFINE (outside section) is parsed | Finds `ROOT_DEFINE` as a `dscDefine` (defined outside any section header). | +| 10 | Parses library class definitions | At least 4 `dscLibraryDefinition` symbols (BaseLib, DebugLib, PrintLib, TimerLib, plus overrides). | +| 11 | Library definition with extra whitespace around pipe is parsed | Finds `PrintLib` even when the line has extra spaces around `\|`. | +| 12 | Parses simple .inf module references | At least 3 `dscModuleDefinition` symbols. | +| 13 | Module with sub-sections (curly braces) is parsed | Finds `ComplexModule.inf` and asserts it has `children.length > 0`. | +| 14 | Component sub-sections are parsed | At least 2 `dscComponentSubSection` symbols (``, ``, ``). | +| 15 | Parses PCD definitions | At least 3 `dscPcdDefinition` symbols across FixedAtBuild, DynamicDefault, and component-scoped PCDs. | +| 16 | Parses build option entries | At least 2 `dscBuildOption` symbols (GCC and MSFT lines). | +| 17 | Parses !include directive | At least 1 `dscInclude` symbol; specifically finds `TestInclude.dsc.inc`. | +| 18 | symbolsTree contains top-level section nodes | `symbolsTree` is non-empty and every root node has an expected DSC symbol type. | +| 19 | Library definitions are children of their parent section | The `[LibraryClasses]` section node contains at least 3 child `dscLibraryDefinition` symbols. | +| 20 | symbolsList length equals total nodes across the tree | Flat `symbolsList.length` equals recursive count of all nodes in `symbolsTree`. | +| 21 | Comments are not parsed as symbols | No symbol name starts with `#`. | + +--- + +### `decParser.test.ts` + +**Suite: DEC Parser – Symbol Extraction** — Parses `test/testDecParsing.dec` with `DecParser`. The fixture includes `[Defines]`, `[Includes]` (common + `.X64`), `[LibraryClasses]`, `[Guids]` with GUID-format values, `[Protocols]`, `[Ppis]`, `[PcdsFixedAtBuild]`, and `[PcdsPatchableInModule]`. + +| # | Test | Description | +|---|------|-------------| +| 1 | Parses [Defines] section | At least 1 `decSection` matching "defines". | +| 2 | Parses [Includes] sections (common + arch-specific) | At least 2 `decSection` symbols for `Includes` (common and `.X64`). | +| 3 | Parses [LibraryClasses] section | At least 1 `decSection` matching "libraryclasses". | +| 4 | Parses [Guids] section | At least 1 `decSection` matching "guids". | +| 5 | Parses [Protocols] section | At least 1 `decSection` matching "protocols". | +| 6 | Parses [Ppis] section | At least 1 `decSection` matching "ppis". | +| 7 | Parses PCD sections | At least 2 `decSection` symbols with names matching "pcds" (FixedAtBuild + PatchableInModule). | +| 8 | Parses include directory entries | At least 2 `decInclude` symbols (e.g. `Include`, `Include/Library`). | +| 9 | Parses library class entries | At least 2 `decLibrary` symbols. | +| 10 | Parses GUID entries | At least 1 `decGuid` symbol with GUID-format value. | +| 11 | Parses protocol entries | At least 2 `decProtocol` symbols. | +| 12 | Parses PPI entries | At least 1 `decPpi` symbol. | +| 13 | Parses PCD entries | At least 2 `decPcd` symbols across different PCD sections. | +| 14 | symbolsTree is not empty | `symbolsTree.length > 0`. | +| 15 | symbolsList equals recursive tree count | Flat list count matches recursive tree traversal count. | +| 16 | Comments are not parsed as symbols | No symbol name starts with `#`. | + +--- + +### `fdfParser.test.ts` + +**Suite: FDF Parser – Symbol Extraction** — Parses `test/testFdfParsing.fdf` with `FdfParser`. The fixture includes `[FD.TestFd]`, `[FV.FVMAIN]` with INF entries and a `DEFINE`, `[FV.FVRECOVERY]` with an `APRIORI` block, `[Rule.Common.SEC]`, and `!include`. + +| # | Test | Description | +|---|------|-------------| +| 1 | Parses [FD.*] section | At least 1 `fdfSection` matching "FD.". | +| 2 | Parses [FV.*] sections | At least 2 `fdfSection` symbols matching "FV." (FVMAIN and FVRECOVERY). | +| 3 | Parses [Rule.*] section | At least 1 `fdfSection` matching "Rule.". | +| 4 | Parses INF module references | At least 3 `fdfInf` symbols (SimpleModule, AnotherModule, AprioriModule, RecoveryModule). | +| 5 | Parses DEFINE statements | At least 2 `fdfDefinition` symbols. | +| 6 | Parses !include directive | At least 1 `fdfInclude` symbol; specifically finds `TestFdfInclude.fdf.inc`. | +| 7 | symbolsTree is not empty | `symbolsTree.length > 0`. | +| 8 | symbolsList equals recursive tree count | Flat list count matches recursive tree traversal count. | +| 9 | Comments are not parsed as symbols | No symbol name starts with `#`. | + +--- + +### `infParser.test.ts` + +**Suite: INF Parser – Symbol Extraction** — Parses `test/testInfParsing.inf` with `InfParser`. The fixture includes `[Defines]` (INF_VERSION, BASE_NAME, MODULE_TYPE, ENTRY_POINT, LIBRARY_CLASS, CONSTRUCTOR, DESTRUCTOR, DEFINE), `[Sources]` + `[Sources.X64]`, `[Packages]`, `[LibraryClasses]`, `[Protocols]`, `[Ppis]`, `[Guids]`, `[FixedPcd]`, and `[Depex]`. + +| # | Test | Description | +|---|------|-------------| +| 1 | Parses [Defines] section | At least 1 `infSection` matching "defines". | +| 2 | Parses [Sources] section(s) | At least 1 `infSectionSource` symbol. | +| 3 | Parses [Packages] section | At least 1 `infSectionPackages` symbol. | +| 4 | Parses [LibraryClasses] section | At least 1 `infSectionLibraries` symbol. | +| 5 | Parses [Protocols] section | At least 1 `infSectionProtocols` symbol. | +| 6 | Parses [Ppis] section | At least 1 `infSectionPpis` symbol. | +| 7 | Parses [Guids] section | At least 1 `infSectionGuids` symbol. | +| 8 | Parses [Pcd] / [FixedPcd] section | At least 1 `infSectionPcds` symbol. | +| 9 | Parses [Depex] section | At least 1 `infSectionDepex` symbol. | +| 10 | Parses INF defines (MODULE_TYPE, BASE_NAME, etc.) | At least 7 `infDefine` symbols covering all key-value pairs in `[Defines]`. | +| 11 | Parses ENTRY_POINT define | Finds a define with name matching `ENTRY_POINT`. | +| 12 | Parses CONSTRUCTOR define | Finds a define with name matching `CONSTRUCTOR`. | +| 13 | Parses DESTRUCTOR define | Finds a define with name matching `DESTRUCTOR`. | +| 14 | Parses source file entries | At least 3 `infSource` symbols (`.c`, `.h`, arch-specific). | +| 15 | Parses package references | At least 2 `infPackage` symbols (e.g. `MdePkg/MdePkg.dec`). | +| 16 | Parses library class references | At least 3 `infLibrary` symbols. | +| 17 | Parses protocol entries | At least 2 `infProtocol` symbols. | +| 18 | Parses PPI entries | At least 1 `infPpi` symbol. | +| 19 | Parses GUID entries | At least 2 `infGuid` symbols. | +| 20 | Parses PCD entries | At least 1 `infPcd` symbol. | +| 21 | Parses dependency expression entries | At least 1 `infDepex` symbol. | +| 22 | symbolsTree is not empty | `symbolsTree.length > 0`. | +| 23 | symbolsList equals recursive tree count | Flat list count matches recursive tree traversal count. | +| 24 | Comments are not parsed as symbols | No symbol name starts with `#`. | + +--- + +### `aslParser.test.ts` + +**Suite: ASL Parser – Symbol Extraction** — Parses `test/testAslParsing.asl` with `AslParser`. The fixture includes a `DefinitionBlock`, 2 `External` declarations, 2 `Scope` blocks, 2 `Device` blocks (TPM0 with Name/Method/OperationRegion/Field, EC0 with Name/Method/OperationRegion/Field). + +| # | Test | Description | +|---|------|-------------| +| 1 | Parses DefinitionBlock | At least 1 `aslDefinitionBlock` symbol. | +| 2 | Parses External declarations | At least 2 `aslExternal` symbols (`\_SB.PCI0`, `\_SB.PCI0.LPCB`). | +| 3 | Parses Scope blocks | At least 2 `aslScope` symbols (`\_SB`, `\_SB.PCI0`). | +| 4 | Parses Device blocks | At least 2 `aslDevice` symbols (`TPM0`, `EC0`). | +| 5 | Parses Name declarations | At least 4 `aslName` symbols (TVAR, _HID, _STR, PVAR, RBUF, etc.). | +| 6 | Parses Method blocks | At least 2 `aslMethod` symbols (_STA, _CRS, RFAN). | +| 7 | Parses OperationRegion declarations | At least 2 `aslOpRegion` symbols (TPMR, ECOR). | +| 8 | Parses Field declarations | At least 2 `aslField` symbols (Field on TPMR, Field on ECOR). | +| 9 | DefinitionBlock has children | The first `aslDefinitionBlock` has `children.length > 0`. | +| 10 | Device contains Names as children | `TPM0` device's children include at least one `aslName`. | +| 11 | symbolsTree is not empty | `symbolsTree.length > 0`. | +| 12 | symbolsList equals recursive tree count | Flat list count matches recursive tree traversal count. | +| 13 | Comments are not parsed as symbols | No symbol name starts with `//`. | + +--- + +### `vfrParser.test.ts` + +**Suite: VFR Parser – Symbol Extraction** — Parses `test/testVfrParsing.vfr` with `VfrParser`. The fixture includes a `formset` containing 2 `form` blocks, `oneof`, `checkbox`, `numeric`, `string`, `password`, and `goto` controls. + +| # | Test | Description | +|---|------|-------------| +| 1 | Parses formset | At least 1 `vfrFormset` symbol. | +| 2 | Parses form blocks | Checks for `vfrForm` symbols. Note: the VFR parser does not nest `BlockFormSection` inside `BlockFormsetSection.context`, so `form` blocks inside `formset` may not be parsed. The test validates the parser does not crash. | +| 3 | Parses oneof controls | At least 2 `vfrOneof` symbols (Option1, Option2). | +| 4 | Parses checkbox controls | At least 1 `vfrCheckbox` symbol. | +| 5 | Parses numeric controls | At least 1 `vfrNumeric` symbol. | +| 6 | Parses string controls | At least 1 `vfrString` symbol. | +| 7 | Parses password controls | At least 1 `vfrPassword` symbol. | +| 8 | Parses goto references | At least 2 `vfrGoto` symbols (goto to form IDs). | +| 9 | Parses prompt entries inside controls | At least 2 `vfrString` symbols from `prompt` lines inside controls and the standalone string control. | +| 10 | symbolsTree is not empty | `symbolsTree.length > 0`. | +| 11 | Formset or form has children | At least one `vfrFormset` or `vfrForm` symbol has `children.length > 0`. | +| 12 | symbolsList equals recursive tree count | Flat list count matches recursive tree traversal count. | +| 13 | Comments are not parsed as symbols | No symbol name starts with `//`. | + +--- + +### `edkWorkspace.test.ts` + +Tests the workspace index classes from `src/index/edkWorkspace.ts`. These are unit-style tests that construct objects directly without running the full workspace processing pipeline. + +#### Suite: SectionProperty + +| # | Test | Description | +|---|------|-------------| +| 1 | constructor lowercases all fields | `SectionProperty('LibraryClasses', 'X64', 'DXE_DRIVER')` stores all values in lowercase. | +| 2 | constructor with already lowercase values | Passing already-lowercase strings preserves them unchanged. | + +#### Suite: SectionProperties + +| # | Test | Description | +|---|------|-------------| +| 1 | starts with empty properties | A new `SectionProperties()` has `properties.length === 0`. | +| 2 | addProperty adds correctly | After `addProperty('LibraryClasses', 'X64', 'DXE_DRIVER')`, length is 1 and sectionType is lowercased. | +| 3 | addProperty multiple | Adding 2 properties results in `properties.length === 2`. | +| 4 | compareArch returns true for matching arch | Two `SectionProperties` sharing `X64` arch return `true`. | +| 5 | compareArch returns false for different archs | `X64` vs `IA32` returns `false`. | +| 6 | compareArchStr returns true for matching arch string | Matches case-insensitively (both `'X64'` and `'x64'` match). | +| 7 | compareArchStr returns false for non-matching arch | `'IA32'` does not match an `X64`-only property set. | +| 8 | compareLibSectionType returns true for matching section type | Two `SectionProperties` both with `LibraryClasses` section type return `true`. | +| 9 | compareLibSectionType returns false for different section types | `LibraryClasses` vs `Components` returns `false`. | +| 10 | compareLibSectionTypeStr case-insensitive match | `'LibraryClasses'`, `'libraryclasses'`, and `'LIBRARYCLASSES'` all match. | +| 11 | compareLibSectionTypeStr returns false for non-matching | `'LibraryClasses'` does not match a `Components`-only property set. | +| 12 | compareModuleType returns true for matching module type | Two properties sharing `DXE_DRIVER` return `true`. | +| 13 | compareModuleType returns false for different module types | `DXE_DRIVER` vs `PEIM` returns `false`. | +| 14 | compareModuleTypeStr case-insensitive | Both `'DXE_DRIVER'` and `'dxe_driver'` match. | +| 15 | compareModuleTypeStr returns false for non-matching | `'PEIM'` does not match a `DXE_DRIVER`-only property set. | +| 16 | toString returns comma-separated properties | With 2 properties added, `toString()` output contains a comma. | +| 17 | compareArch matches any combination | A property set with both `X64` and `IA32` entries matches another set with only `IA32`. | + +#### Suite: InfDsc + +| # | Test | Description | +|---|------|-------------| +| 1 | constructor with section parent sets sectionProperties | Parent `'Components.X64'` sets `parent = undefined` and populates `sectionProperties` with `components/x64/common`. | +| 2 | constructor with INF parent sets parent path | Parent `'SomeModule/Module.inf'` (ending in `.inf`) sets `parent` to the normalized path and leaves `sectionProperties` empty. | +| 3 | constructor normalizes path separators | Forward slashes in the file path are replaced with `path.sep`. | +| 4 | constructor with multi-section parent | Parent `'LibraryClasses.X64.DXE_DRIVER,LibraryClasses.IA32.PEIM'` creates 2 section properties with correct arch and moduleType. | +| 5 | constructor with section missing arch defaults to common | Parent `'libraryclasses'` defaults both arch and moduleType to `'common'`. | +| 6 | getModuleTypeStr returns comma-separated module types | Multi-property InfDsc returns `'dxe_driver,peim'`. | +| 7 | getModuleTypeStr single property | Single `'Components.X64'` property returns `'common'` (moduleType defaults). | +| 8 | toString includes path and line number | `toString()` of an InfDsc at line 42 contains `'42'`. | +| 9 | text property stores original line | The `text` field preserves the raw DSC line text passed to the constructor. | +| 10 | location is preserved | The `vscode.Location` passed to the constructor is stored as-is (line 99). | + +#### Suite: EdkWorkspaces + +| # | Test | Description | +|---|------|-------------| +| 1 | isConfigured returns false when no workspaces | A fresh `EdkWorkspaces()` with no workspaces added returns `false`. | +| 2 | isConfigured returns true after adding a workspace | After pushing an `EdkWorkspace` into `workspaces`, returns `true`. | +| 3 | getInstance returns singleton | Two calls to `EdkWorkspaces.getInstance()` return the same reference. | +| 4 | isFileInUse returns undefined when no workspaces | With no workspaces, `isFileInUse()` returns `undefined` (indeterminate). | +| 5 | getWorkspace returns empty array when no workspaces | Returns `[]` when no workspaces are configured. | +| 6 | getDefinition returns undefined when no workspaces | Returns `undefined` for any variable lookup with no workspaces. | +| 7 | replaceDefines returns original text when no workspaces | `'$(MY_VAR)/path'` is returned unchanged when no workspace can resolve it. | +| 8 | getLib returns empty array when no workspaces | Returns `[]` for any location lookup with no workspaces. | + +#### Suite: EdkWorkspace + +| # | Test | Description | +|---|------|-------------| +| 1 | constructor sets mainDsc from document | `mainDsc.fsPath` ends with `testDscParsing.dsc`. | +| 2 | constructor generates a numeric id | `id` is a `number` and `> 0`. | +| 3 | platformName starts as undefined | Before `proccessWorkspace()`, `platformName` is `undefined`. | +| 4 | flashDefinitionDocument starts as undefined | Before processing, `flashDefinitionDocument` is `undefined`. | +| 5 | filesLibraries starts empty | `filesLibraries.length === 0`. | +| 6 | filesModules starts empty | `filesModules.length === 0`. | +| 7 | dscList is initially empty | `dscList()` returns `[]` before any processing. | +| 8 | fdfList is initially empty | `fdfList()` returns `[]` before any processing. | +| 9 | getFilesList aggregates all file lists | Returns an array combining DSC, FDF, module, and library paths. | +| 10 | includeTree starts empty | `includeTree.length === 0`. | +| 11 | getDefinitions returns a Map | The defines map is a `Map` instance even before processing. | +| 12 | getDefinition returns undefined for unknown key | `getDefinition('NON_EXISTENT')` returns `undefined`. | +| 13 | getDefinitionLocation returns undefined for unknown key | `getDefinitionLocation('NON_EXISTENT')` returns `undefined`. | +| 14 | replaceDefine passes through text with no defines | `'$(UNKNOWN_VAR)/path'` is unchanged when no defines are set. | +| 15 | getPcds returns undefined for unknown namespace | `getPcds('gUnknownPkg')` returns `undefined`. | +| 16 | getAllPcds returns a Map | PCD definitions map is a `Map` instance. | +| 17 | filesLibraries can be set | Setting via the property setter and reading back works correctly. | +| 18 | filesModules can be set | Setting via the property setter and reading back works correctly. | +| 19 | getFilesList includes libraries and modules | After setting both, `getFilesList()` returns at least 2 paths. | +| 20 | filesDsc can be set and read | Setting the `Set` and reading `dscList()` returns 1 entry. | +| 21 | filesFdf can be set and read | Setting the `Set` and reading `fdfList()` returns 1 entry. | +| 22 | getLib returns undefined when no matching library | A non-matching location returns `undefined`. | +| 23 | getLib finds matching library by location | An InfDsc with the exact same URI and line is found by `getLib()`. | + +#### Suite: EdkWorkspace.evaluateExpression + +Tests the Shunting-Yard expression evaluator used for `!if` / `!ifdef` / `!ifndef` conditional processing. + +| # | Test | Description | +|---|------|-------------| +| 1 | TRUE evaluates to true | Literal `TRUE` → `true`. | +| 2 | FALSE evaluates to false | Literal `FALSE` → `false`. | +| 3 | true/false case-insensitive | `'true'`, `'True'`, `'false'` all parse correctly. | +| 4 | numeric 1 is truthy | `'1'` → `1`. | +| 5 | numeric 0 is falsy | `'0'` → `0`. | +| 6 | == with matching strings | `'"hello" == "hello"'` → `true`. | +| 7 | == with non-matching strings | `'"hello" == "world"'` → `false`. | +| 8 | != with different strings | `'"hello" != "world"'` → `true`. | +| 9 | != with same strings | `'"hello" != "hello"'` → `false`. | +| 10 | EQ operator | `'"a" EQ "a"'` → `true` (EDK2-style equality). | +| 11 | NE operator | `'"a" NE "b"'` → `true` (EDK2-style inequality). | +| 12 | AND with both true | `'TRUE AND TRUE'` → `true`. | +| 13 | AND with one false | `'TRUE AND FALSE'` → `false`. | +| 14 | OR with one true | `'FALSE OR TRUE'` → `true`. | +| 15 | OR with both false | `'FALSE OR FALSE'` → `false`. | +| 16 | && operator | `'TRUE && TRUE'` → `true` (C-style AND). | +| 17 | \|\| operator | `'FALSE \|\| TRUE'` → `true` (C-style OR). | +| 18 | NOT TRUE evaluates to false | `'NOT TRUE'` → `false` (unary NOT). | +| 19 | NOT FALSE evaluates to true | `'NOT FALSE'` → truthy. | +| 20 | addition | `'3 + 2'` → `5`. | +| 21 | subtraction | `'5 - 2'` → `3`. | +| 22 | multiplication | `'3 * 4'` → `12`. | +| 23 | division | `'10 / 2'` → `5`. | +| 24 | modulus | `'10 % 3'` → `1`. | +| 25 | greater than | `'5 > 3'` → `true`. | +| 26 | less than | `'3 > 5'` → `false`. | +| 27 | greater or equal | `'5 >= 5'` → `true`. | +| 28 | less or equal | `'3 <= 5'` → `true`. | +| 29 | parentheses group expressions | `'(TRUE OR FALSE) AND TRUE'` → `true`. | +| 30 | nested parentheses | `'((1 + 2) * 3)'` → `9`. | +| 31 | unbalanced parentheses throw | `'(TRUE AND FALSE'` throws an error. | +| 32 | IN operator with match | `'"X64" IN "X64 IA32 ARM"'` → `true`. | +| 33 | IN operator without match | `'"AARCH64" IN "X64 IA32 ARM"'` → `false`. | +| 34 | undefined variable evaluates to false | `'"???"'` (the undefined-variable sentinel) → `false`. | +| 35 | bare word is treated as quoted string | `'hello == "hello"'` → `true` (unquoted words auto-quoted). | +| 36 | complex: (1 + 2) > 2 AND TRUE | Mixed arithmetic + comparison + logical → `true`. | +| 37 | complex: FALSE OR (5 == 5) | Logical OR with parenthesised equality → `true`. | + +--- + +### `edkWorkspaceProcess.test.ts` + +Integration tests for the core workspace processing pipeline: `proccessWorkspace()`, `_doProccessWorkspace()`, and `_processDocument()`. Uses the `test/testDscProcess.dsc` fixture which includes `[Defines]`, `[LibraryClasses]`, `[Components]`, PCD sections, `[BuildOptions]`, conditional blocks (`!if`/`!ifdef`/`!ifndef`/`!else`/`!endif`), nested conditionals, and comments. + +A `ensureProcessingGlobals()` helper stubs all required globals: `gDebugLog`, `gWorkspacePath`, `gConfigAgent`, `gPathFind`, `edkWorkspaceTreeProvider`, `DiagnosticManager`, and `edkStatusBar.myStatusBarItem`. + +#### Suite: EdkWorkspace._processDocument + +Processes the DSC fixture via the private `_processDocument(doc, 'DSC')` and validates all extracted data. + +##### Document Registration + +| # | Test | Description | +|---|------|-------------| +| 1 | Adds document to filesDsc | `dscList()` contains at least 1 entry ending with `testDscProcess.dsc`. | +| 2 | isDocumentInIndex returns true after processing | Private `isDocumentInIndex(doc)` returns `true` for the processed document. | +| 3 | Processing same document twice is a no-op | Calling `_processDocument` again does not add a duplicate entry to `dscList()`. | + +##### Defines Extraction + +| # | Test | Description | +|---|------|-------------| +| 4 | Extracts PLATFORM_NAME from [Defines] | `getDefinition('PLATFORM_NAME')` returns `'TestProcess'`. | +| 5 | Extracts PLATFORM_GUID | `getDefinition('PLATFORM_GUID')` returns `'11111111-2222-3333-4444-555555555555'`. | +| 6 | Extracts DEFINE MY_FLAG | `getDefinition('MY_FLAG')` returns `'TRUE'`. | +| 7 | Extracts DEFINE with empty value | `getDefinition('EMPTY_DEF')` is defined but trims to empty string. | +| 8 | Extracts root-level DEFINE | `getDefinition('ROOT_DEF')` returns `'RootValue'` (defined outside any section). | +| 9 | getDefinitionLocation returns a Location for known define | Location URI ends with `testDscProcess.dsc`. | +| 10 | getDefinitions returns all defines as a Map | Map instance with `size >= 5`. | + +##### Define Variable Substitution + +| # | Test | Description | +|---|------|-------------| +| 11 | DERIVED define has $(BASE) resolved | `DEFINE DERIVED = $(BASE)World` resolves to `'HelloWorld'`. | +| 12 | replaceDefine substitutes known variables | `replaceDefine('$(MY_FLAG)')` returns `'TRUE'`. | +| 13 | replaceDefine leaves unknown variables untouched | `'$(TOTALLY_UNKNOWN)'` passes through unchanged. | + +##### PCD Extraction + +| # | Test | Description | +|---|------|-------------| +| 14 | Extracts PCDs in gTestPkg namespace | `getPcds('gTestPkg')` is not `undefined`. | +| 15 | PcdTestMask has correct value | `PcdTestMask.value` is `'0x2F'`. | +| 16 | PcdBootTimeout from DynamicDefault | `PcdBootTimeout.value` is `'5'`. | +| 17 | PCD with L"string" value strips L prefix | `PcdStringVal.value` starts with `'"'` (L prefix removed). | +| 18 | PCD has location information | PCD position URI ends with `testDscProcess.dsc`. | +| 19 | getAllPcds returns all namespaces | Map contains `'gTestPkg'` key. | + +##### Conditional Processing + +| # | Test | Description | +|---|------|-------------| +| 20 | !if TRUE branch: COND_TAKEN is defined | `getDefinition('COND_TAKEN')` returns `'IfTrueValue'`. | +| 21 | !if TRUE branch: else branch not taken | `COND_TAKEN` is not `'IfFalseValue'`. | +| 22 | !if FALSE: else branch taken, COND_ELSE is defined | `getDefinition('COND_ELSE')` returns `'ElseValue'`. | +| 23 | !if FALSE: if branch not taken, COND_FALSE_IF not defined | `getDefinition('COND_FALSE_IF')` returns `undefined`. | +| 24 | Nested conditionals: outer TRUE inner FALSE -> INNER_ELSE defined | Both `OUTER_TRUE` (`'OuterOk'`) and `INNER_ELSE` (`'InnerElseOk'`) are set. | +| 25 | Nested conditionals: INNER_FALSE not defined | `getDefinition('INNER_FALSE')` returns `undefined`. | +| 26 | !ifdef on existing variable takes the branch | `IFDEF_TAKEN` is `'yes'` (variable `EXISTING_VAR` was defined). | +| 27 | !ifndef on undefined variable takes the branch | `IFNDEF_TAKEN` is `'yes'` (`TOTALLY_UNDEFINED_XYZ` not defined). | +| 28 | COND_A before conditional is still defined | `getDefinition('COND_A')` returns `'BeforeIf'`. | + +##### Library and Module References + +| # | Test | Description | +|---|------|-------------| +| 29 | Libraries are collected (even if paths unresolved) | `filesLibraries.length >= 2` (BaseLib, DebugLib, TimerLib). | +| 30 | Modules are collected (even if paths unresolved) | `filesModules.length >= 2` (ModuleA, ModuleB, ModuleC). | +| 31 | Library InfDsc has correct section properties | BaseLib's `sectionProperties.properties.length > 0`. | +| 32 | Module InfDsc preserves location | First module's location URI ends with `testDscProcess.dsc`. | + +##### Grayout Ranges + +| # | Test | Description | +|---|------|-------------| +| 33 | parsedDocuments has entry for processed document | Private `parsedDocuments` map contains an entry for the document's `fsPath`. | +| 34 | Grayout ranges exist for inactive conditional blocks | At least 1 grayout range from `!if FALSE` blocks. | + +##### Comment Stripping + +| # | Test | Description | +|---|------|-------------| +| 35 | stripComment removes line comments | `'DEFINE X = 1 # comment'` → `'DEFINE X = 1'`. | +| 36 | stripComment preserves hash inside quotes | `'DEFINE X = "value#with#hash"'` → unchanged. | +| 37 | stripComment trims whitespace | `' some text '` → `'some text'`. | +| 38 | stripComment returns empty for comment-only line | `'# just a comment'` → `''`. | + +#### Suite: EdkWorkspace._doProccessWorkspace + +Runs the full private `_doProccessWorkspace()` pipeline (reset state → open main DSC → process → populate `platformName` → find FDF). + +##### Workspace Initialization + +| # | Test | Description | +|---|------|-------------| +| 1 | platformName is populated from PLATFORM_NAME define | `platformName` is `'TestProcess'` after processing. | +| 2 | workInProgress is false after completion | Private `workInProgress` is `false`. | +| 3 | processComplete is true after completion | Private `processComplete` is `true`. | + +##### State Reset + +| # | Test | Description | +|---|------|-------------| +| 4 | Running again resets and re-processes | Second call returns `true` and `platformName` is still `'TestProcess'`. | +| 5 | Returns false if already in progress | Setting `workInProgress = true` causes the method to return `false`. | + +##### Defines After Full Processing + +| # | Test | Description | +|---|------|-------------| +| 6 | All defines from [Defines] section are available | Map has `PLATFORM_NAME`, `MY_FLAG`, `MY_PATH`. | +| 7 | Conditional defines are correctly resolved | `COND_TAKEN` is `'IfTrueValue'`, `COND_ELSE` is `'ElseValue'`. | +| 8 | replaceDefine works after processing | `replaceDefine('$(PLATFORM_NAME)')` returns `'TestProcess'`. | + +##### PCDs After Full Processing + +| # | Test | Description | +|---|------|-------------| +| 9 | PCDs are available after processing | `getPcds('gTestPkg')` has `size >= 3`. | + +##### File Lists After Full Processing + +| # | Test | Description | +|---|------|-------------| +| 10 | dscList contains the main document | At least 1 entry ending with `testDscProcess.dsc`. | +| 11 | Libraries are populated | `filesLibraries.length >= 2`. | +| 12 | Modules are populated | `filesModules.length >= 2`. | +| 13 | getFilesList aggregates all lists | Total `length >= 4` (DSC + libraries + modules). | + +#### Suite: EdkWorkspace.proccessWorkspace + +Tests the public `proccessWorkspace()` API which wraps `_doProccessWorkspace` with `vscode.window.withProgress`. + +| # | Test | Description | +|---|------|-------------| +| 1 | proccessWorkspace runs without errors | Returns `true` and `platformName` is `'TestProcess'`. | +| 2 | proccessWorkspace populates defines and PCDs | `getDefinitions().size > 0` and `getAllPcds().size > 0`. | diff --git a/src/test/suite/aslParser.test.ts b/src/test/suite/aslParser.test.ts new file mode 100644 index 0000000..a5411b7 --- /dev/null +++ b/src/test/suite/aslParser.test.ts @@ -0,0 +1,118 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { AslParser } from '../../edkParser/aslParser'; +import { Edk2SymbolType } from '../../symbols/symbolsType'; +import { DebugLog } from '../../debugLog'; + +function ensureGlobals() { + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } +} + +async function parseAslFile(filename: string): Promise { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test', filename); + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + const parser = new AslParser(document); + await parser.parseFile(); + return parser; +} + +function symbolsOfType(parser: AslParser, type: Edk2SymbolType) { + return parser.symbolsList.filter(s => s.type === type); +} + +suite('ASL Parser – Symbol Extraction', () => { + + let parser: AslParser; + + suiteSetup(async function () { + this.timeout(10_000); + parser = await parseAslFile('testAslParsing.asl'); + }); + + suite('Blocks', () => { + test('Parses DefinitionBlock', () => { + const defs = symbolsOfType(parser, Edk2SymbolType.aslDefinitionBlock); + assert.ok(defs.length >= 1, 'Expected at least one DefinitionBlock'); + }); + + test('Parses External declarations', () => { + const externals = symbolsOfType(parser, Edk2SymbolType.aslExternal); + assert.ok(externals.length >= 2, `Expected >=2 externals, got ${externals.length}`); + }); + + test('Parses Scope blocks', () => { + const scopes = symbolsOfType(parser, Edk2SymbolType.aslScope); + assert.ok(scopes.length >= 2, `Expected >=2 scopes, got ${scopes.length}`); + }); + + test('Parses Device blocks', () => { + const devices = symbolsOfType(parser, Edk2SymbolType.aslDevice); + assert.ok(devices.length >= 2, `Expected >=2 devices, got ${devices.length}`); + }); + }); + + suite('Members', () => { + test('Parses Name declarations', () => { + const names = symbolsOfType(parser, Edk2SymbolType.aslName); + assert.ok(names.length >= 4, `Expected >=4 names, got ${names.length}`); + }); + + test('Parses Method blocks', () => { + const methods = symbolsOfType(parser, Edk2SymbolType.aslMethod); + assert.ok(methods.length >= 2, `Expected >=2 methods, got ${methods.length}`); + }); + + test('Parses OperationRegion declarations', () => { + const regions = symbolsOfType(parser, Edk2SymbolType.aslOpRegion); + assert.ok(regions.length >= 2, `Expected >=2 OperationRegions, got ${regions.length}`); + }); + + test('Parses Field declarations', () => { + const fields = symbolsOfType(parser, Edk2SymbolType.aslField); + assert.ok(fields.length >= 2, `Expected >=2 fields, got ${fields.length}`); + }); + }); + + suite('Tree Structure', () => { + test('DefinitionBlock has children', () => { + const defBlock = symbolsOfType(parser, Edk2SymbolType.aslDefinitionBlock); + assert.ok(defBlock.length > 0); + assert.ok(defBlock[0].children.length > 0, 'DefinitionBlock should have child symbols'); + }); + + test('Device contains Names as children', () => { + const devices = symbolsOfType(parser, Edk2SymbolType.aslDevice); + const tpm = devices.find(d => /TPM0/i.test(d.name)); + assert.ok(tpm, 'TPM0 device should exist'); + const childTypes = tpm!.children.map(c => (c as any).type); + assert.ok(childTypes.includes(Edk2SymbolType.aslName), 'TPM0 should contain a Name'); + }); + }); + + suite('Consistency', () => { + test('symbolsTree is not empty', () => { + assert.ok(parser.symbolsTree.length > 0); + }); + + test('symbolsList equals recursive tree count', () => { + function countNodes(nodes: vscode.DocumentSymbol[]): number { + let c = 0; + for (const n of nodes) { c++; c += countNodes(n.children); } + return c; + } + assert.strictEqual(parser.symbolsList.length, countNodes(parser.symbolsTree)); + }); + + test('Comments are not parsed as symbols', () => { + for (const s of parser.symbolsList) { + assert.ok(!s.name.startsWith('//'), `Symbol should not start with //: "${s.name}"`); + } + }); + }); +}); diff --git a/src/test/suite/decParser.test.ts b/src/test/suite/decParser.test.ts new file mode 100644 index 0000000..d5c50b0 --- /dev/null +++ b/src/test/suite/decParser.test.ts @@ -0,0 +1,144 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { DecParser } from '../../edkParser/decParser'; +import { Edk2SymbolType } from '../../symbols/symbolsType'; +import { DebugLog } from '../../debugLog'; + +function ensureGlobals() { + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } +} + +async function parseDecFile(filename: string): Promise { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test', filename); + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + const parser = new DecParser(document); + await parser.parseFile(); + return parser; +} + +function symbolsOfType(parser: DecParser, type: Edk2SymbolType) { + return parser.symbolsList.filter(s => s.type === type); +} + +suite('DEC Parser – Symbol Extraction', () => { + + let parser: DecParser; + + suiteSetup(async function () { + this.timeout(10_000); + parser = await parseDecFile('testDecParsing.dec'); + }); + + suite('Sections', () => { + test('Parses [Defines] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.decSection); + const defines = sections.filter(s => /defines/i.test(s.name)); + assert.ok(defines.length >= 1, 'Expected at least one [Defines] section'); + }); + + test('Parses [Includes] sections (common + arch-specific)', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.decSection); + const incSections = sections.filter(s => /includes/i.test(s.name)); + assert.ok(incSections.length >= 2, `Expected >=2 Includes sections, got ${incSections.length}`); + }); + + test('Parses [LibraryClasses] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.decSection); + const libSections = sections.filter(s => /libraryclasses/i.test(s.name)); + assert.ok(libSections.length >= 1, 'Expected at least one [LibraryClasses] section'); + }); + + test('Parses [Guids] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.decSection); + const guidSections = sections.filter(s => /guids/i.test(s.name)); + assert.ok(guidSections.length >= 1, 'Expected at least one [Guids] section'); + }); + + test('Parses [Protocols] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.decSection); + const protoSections = sections.filter(s => /protocols/i.test(s.name)); + assert.ok(protoSections.length >= 1, 'Expected at least one [Protocols] section'); + }); + + test('Parses [Ppis] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.decSection); + const ppiSections = sections.filter(s => /ppis/i.test(s.name)); + assert.ok(ppiSections.length >= 1, 'Expected at least one [Ppis] section'); + }); + + test('Parses PCD sections', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.decSection); + const pcdSections = sections.filter(s => /pcds/i.test(s.name)); + assert.ok(pcdSections.length >= 2, `Expected >=2 PCD sections, got ${pcdSections.length}`); + }); + }); + + suite('Includes', () => { + test('Parses include directory entries', () => { + const includes = symbolsOfType(parser, Edk2SymbolType.decInclude); + assert.ok(includes.length >= 2, `Expected >=2 include paths, got ${includes.length}`); + }); + }); + + suite('Libraries', () => { + test('Parses library class entries', () => { + const libs = symbolsOfType(parser, Edk2SymbolType.decLibrary); + assert.ok(libs.length >= 2, `Expected >=2 library classes, got ${libs.length}`); + }); + }); + + suite('GUIDs', () => { + test('Parses GUID entries', () => { + const guids = symbolsOfType(parser, Edk2SymbolType.decGuid); + assert.ok(guids.length >= 1, `Expected >=1 GUID, got ${guids.length}`); + }); + }); + + suite('Protocols', () => { + test('Parses protocol entries', () => { + const protocols = symbolsOfType(parser, Edk2SymbolType.decProtocol); + assert.ok(protocols.length >= 2, `Expected >=2 protocols, got ${protocols.length}`); + }); + }); + + suite('PPIs', () => { + test('Parses PPI entries', () => { + const ppis = symbolsOfType(parser, Edk2SymbolType.decPpi); + assert.ok(ppis.length >= 1, `Expected >=1 PPI, got ${ppis.length}`); + }); + }); + + suite('PCDs', () => { + test('Parses PCD entries', () => { + const pcds = symbolsOfType(parser, Edk2SymbolType.decPcd); + assert.ok(pcds.length >= 2, `Expected >=2 PCDs, got ${pcds.length}`); + }); + }); + + suite('Consistency', () => { + test('symbolsTree is not empty', () => { + assert.ok(parser.symbolsTree.length > 0, 'symbolsTree should not be empty'); + }); + + test('symbolsList equals recursive tree count', () => { + function countNodes(nodes: vscode.DocumentSymbol[]): number { + let c = 0; + for (const n of nodes) { c++; c += countNodes(n.children); } + return c; + } + assert.strictEqual(parser.symbolsList.length, countNodes(parser.symbolsTree)); + }); + + test('Comments are not parsed as symbols', () => { + for (const s of parser.symbolsList) { + assert.ok(!s.name.startsWith('#'), `Symbol should not start with #: "${s.name}"`); + } + }); + }); +}); diff --git a/src/test/suite/dscParser.test.ts b/src/test/suite/dscParser.test.ts new file mode 100644 index 0000000..ac45031 --- /dev/null +++ b/src/test/suite/dscParser.test.ts @@ -0,0 +1,213 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { DscParser } from '../../edkParser/dscParser'; +import { Edk2SymbolType } from '../../symbols/symbolsType'; +import { DebugLog } from '../../debugLog'; + +/** + * Ensure gDebugLog is initialised even when the extension does not activate + * (e.g. no workspace folders in the test environment). + */ +function ensureGlobals() { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } +} + +/** + * Helper: open a .dsc file from the test/ folder and parse it with DscParser. + * Returns the parser instance so callers can inspect symbolsList / symbolsTree. + */ +async function parseDscFile(filename: string): Promise { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test', filename); + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + const parser = new DscParser(document); + await parser.parseFile(); + return parser; +} + +/** Filter helper: return all symbols of a given type from the flat list. */ +function symbolsOfType(parser: DscParser, type: Edk2SymbolType) { + return parser.symbolsList.filter(s => s.type === type); +} + +suite('DSC Parser – Symbol Extraction', () => { + + let parser: DscParser; + + suiteSetup(async function () { + this.timeout(10_000); + parser = await parseDscFile('testDscParsing.dsc'); + }); + + suite('Sections', () => { + test('Parses [Defines] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.dscSection); + const defines = sections.filter(s => /defines/i.test(s.name)); + assert.ok(defines.length >= 1, 'Expected at least one [Defines] section'); + }); + + test('Parses [LibraryClasses] sections (common + arch-specific)', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.dscSection); + const libSections = sections.filter(s => /libraryclasses/i.test(s.name)); + assert.ok(libSections.length >= 2, `Expected >=2 LibraryClasses sections, got ${libSections.length}`); + }); + + test('Parses [Components] and [Components.X64] sections', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.dscSection); + const compSections = sections.filter(s => /components/i.test(s.name)); + assert.ok(compSections.length >= 2, `Expected >=2 Components sections, got ${compSections.length}`); + }); + + test('Parses [SkuIds] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.dscSection); + const sku = sections.filter(s => /skuids/i.test(s.name)); + assert.strictEqual(sku.length, 1, 'Expected exactly one [SkuIds] section'); + }); + + test('Parses [BuildOptions] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.dscBuildOptionsSection); + assert.ok(sections.length >= 1, 'Expected at least one [BuildOptions] section'); + }); + + test('Parses PCD sections (FixedAtBuild + DynamicDefault)', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.dscSection); + const pcdSections = sections.filter(s => /pcd/i.test(s.name)); + assert.ok(pcdSections.length >= 2, `Expected >=2 PCD sections, got ${pcdSections.length}`); + }); + }); + + suite('Defines', () => { + test('Parses DEFINE statements inside [Defines]', () => { + const defines = symbolsOfType(parser, Edk2SymbolType.dscDefine); + assert.ok(defines.length >= 3, `Expected >=3 defines, got ${defines.length}`); + }); + + test('DEFINE with empty value is still parsed', () => { + const defines = symbolsOfType(parser, Edk2SymbolType.dscDefine); + const emptyVar = defines.find(s => /EMPTY_VAR/i.test(s.name)); + assert.ok(emptyVar, 'EMPTY_VAR define should be parsed'); + }); + + test('Root-level DEFINE (outside section) is parsed', () => { + const defines = symbolsOfType(parser, Edk2SymbolType.dscDefine); + const rootDef = defines.find(s => /ROOT_DEFINE/i.test(s.name)); + assert.ok(rootDef, 'ROOT_DEFINE should be parsed as a root-level define'); + }); + }); + + suite('Libraries', () => { + test('Parses library class definitions', () => { + const libs = symbolsOfType(parser, Edk2SymbolType.dscLibraryDefinition); + assert.ok(libs.length >= 4, `Expected >=4 library definitions, got ${libs.length}`); + }); + + test('Library definition with extra whitespace around pipe is parsed', () => { + const libs = symbolsOfType(parser, Edk2SymbolType.dscLibraryDefinition); + const printLib = libs.find(s => /PrintLib/i.test(s.name)); + assert.ok(printLib, 'PrintLib (extra whitespace around |) should be parsed'); + }); + }); + + suite('Modules / Components', () => { + test('Parses simple .inf module references', () => { + const modules = symbolsOfType(parser, Edk2SymbolType.dscModuleDefinition); + assert.ok(modules.length >= 3, `Expected >=3 module definitions, got ${modules.length}`); + }); + + test('Module with sub-sections (curly braces) is parsed', () => { + const modules = symbolsOfType(parser, Edk2SymbolType.dscModuleDefinition); + const complex = modules.find(s => /ComplexModule/i.test(s.name)); + assert.ok(complex, 'ComplexModule.inf should be parsed as a module definition'); + assert.ok(complex!.children.length > 0, 'ComplexModule should have child sub-sections'); + }); + + test('Component sub-sections are parsed (, , )', () => { + const subSections = symbolsOfType(parser, Edk2SymbolType.dscComponentSubSection); + assert.ok(subSections.length >= 2, `Expected >=2 component sub-sections, got ${subSections.length}`); + }); + }); + + suite('PCDs', () => { + test('Parses PCD definitions', () => { + const pcds = symbolsOfType(parser, Edk2SymbolType.dscPcdDefinition); + assert.ok(pcds.length >= 3, `Expected >=3 PCD definitions, got ${pcds.length}`); + }); + }); + + suite('Build Options', () => { + test('Parses build option entries', () => { + const opts = symbolsOfType(parser, Edk2SymbolType.dscBuildOption); + assert.ok(opts.length >= 2, `Expected >=2 build options, got ${opts.length}`); + }); + }); + + suite('Includes', () => { + test('Parses !include directive', () => { + const includes = symbolsOfType(parser, Edk2SymbolType.dscInclude); + assert.ok(includes.length >= 1, 'Expected at least one !include directive'); + const inc = includes.find(s => /TestInclude/i.test(s.name)); + assert.ok(inc, 'TestInclude.dsc.inc should be found'); + }); + }); + + suite('Tree Structure', () => { + test('symbolsTree contains top-level section nodes', () => { + assert.ok(parser.symbolsTree.length > 0, 'symbolsTree should not be empty'); + for (const root of parser.symbolsTree) { + assert.ok( + root.type === Edk2SymbolType.dscSection || + root.type === Edk2SymbolType.dscBuildOptionsSection || + root.type === Edk2SymbolType.dscDefine || + root.type === Edk2SymbolType.dscInclude || + root.type === Edk2SymbolType.dscModuleDefinition || + root.type === Edk2SymbolType.dscLibraryDefinition || + root.type === Edk2SymbolType.dscPcdDefinition || + root.type === Edk2SymbolType.dscBuildOption || + root.type === Edk2SymbolType.unknown, + `Unexpected root symbol type: ${root.type}` + ); + } + }); + + test('Library definitions are children of their parent section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.dscSection); + const libSection = sections.find(s => /^\[?\s*libraryclasses\s*\]?$/i.test(s.name)); + if (libSection) { + const childLibs = libSection.children.filter( + c => (c as any).type === Edk2SymbolType.dscLibraryDefinition + ); + assert.ok(childLibs.length >= 3, `Expected >=3 library children in [LibraryClasses], got ${childLibs.length}`); + } + }); + }); + + suite('Consistency', () => { + test('symbolsList length equals total nodes across the tree', () => { + function countNodes(nodes: vscode.DocumentSymbol[]): number { + let count = 0; + for (const n of nodes) { + count++; + count += countNodes(n.children); + } + return count; + } + const treeCount = countNodes(parser.symbolsTree); + assert.strictEqual(parser.symbolsList.length, treeCount, + 'Flat symbolsList length should equal recursive tree node count'); + }); + + test('Comments are not parsed as symbols', () => { + const allNames = parser.symbolsList.map(s => s.name); + for (const name of allNames) { + assert.ok(!name.startsWith('#'), `Symbol name should not start with #: "${name}"`); + assert.ok(!name.startsWith('/*'), `Symbol name should not start with /*: "${name}"`); + } + }); + }); +}); diff --git a/src/test/suite/edkWorkspace.test.ts b/src/test/suite/edkWorkspace.test.ts new file mode 100644 index 0000000..043c1bd --- /dev/null +++ b/src/test/suite/edkWorkspace.test.ts @@ -0,0 +1,762 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { DebugLog } from '../../debugLog'; +import { + SectionProperty, + SectionProperties, + InfDsc, + EdkWorkspaces, + EdkWorkspace, +} from '../../index/edkWorkspace'; + +/** + * Ensure gDebugLog is initialised even when the extension does not activate. + */ +function ensureGlobals() { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } +} + +function makeLoc(line: number = 0): vscode.Location { + return new vscode.Location( + vscode.Uri.file('d:/fake/test.dsc'), + new vscode.Position(line, 0) + ); +} + +// ─── SectionProperty ────────────────────────────────────────── + +suite('SectionProperty', () => { + test('constructor lowercases all fields', () => { + const prop = new SectionProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + assert.strictEqual(prop.sectionType, 'libraryclasses'); + assert.strictEqual(prop.arch, 'x64'); + assert.strictEqual(prop.moduleType, 'dxe_driver'); + }); + + test('constructor with already lowercase values', () => { + const prop = new SectionProperty('components', 'ia32', 'peim'); + assert.strictEqual(prop.sectionType, 'components'); + assert.strictEqual(prop.arch, 'ia32'); + assert.strictEqual(prop.moduleType, 'peim'); + }); +}); + +// ─── SectionProperties ──────────────────────────────────────── + +suite('SectionProperties', () => { + test('starts with empty properties', () => { + const sp = new SectionProperties(); + assert.strictEqual(sp.properties.length, 0); + }); + + test('addProperty adds correctly', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + assert.strictEqual(sp.properties.length, 1); + assert.strictEqual(sp.properties[0].sectionType, 'libraryclasses'); + }); + + test('addProperty multiple', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + sp.addProperty('Components', 'IA32', 'PEIM'); + assert.strictEqual(sp.properties.length, 2); + }); + + // ── compareArch ── + + test('compareArch returns true for matching arch', () => { + const sp1 = new SectionProperties(); + sp1.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + + const sp2 = new SectionProperties(); + sp2.addProperty('Components', 'X64', 'PEIM'); + + assert.strictEqual(sp1.compareArch(sp2), true); + }); + + test('compareArch returns false for different archs', () => { + const sp1 = new SectionProperties(); + sp1.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + + const sp2 = new SectionProperties(); + sp2.addProperty('Components', 'IA32', 'PEIM'); + + assert.strictEqual(sp1.compareArch(sp2), false); + }); + + // ── compareArchStr ── + + test('compareArchStr returns true for matching arch string', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + assert.strictEqual(sp.compareArchStr('X64'), true); + assert.strictEqual(sp.compareArchStr('x64'), true); + }); + + test('compareArchStr returns false for non-matching arch', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + assert.strictEqual(sp.compareArchStr('IA32'), false); + }); + + // ── compareLibSectionType ── + + test('compareLibSectionType returns true for matching section type', () => { + const sp1 = new SectionProperties(); + sp1.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + + const sp2 = new SectionProperties(); + sp2.addProperty('LibraryClasses', 'IA32', 'PEIM'); + + assert.strictEqual(sp1.compareLibSectionType(sp2), true); + }); + + test('compareLibSectionType returns false for different section types', () => { + const sp1 = new SectionProperties(); + sp1.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + + const sp2 = new SectionProperties(); + sp2.addProperty('Components', 'X64', 'DXE_DRIVER'); + + assert.strictEqual(sp1.compareLibSectionType(sp2), false); + }); + + // ── compareLibSectionTypeStr ── + + test('compareLibSectionTypeStr case-insensitive match', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + assert.strictEqual(sp.compareLibSectionTypeStr('LibraryClasses'), true); + assert.strictEqual(sp.compareLibSectionTypeStr('libraryclasses'), true); + assert.strictEqual(sp.compareLibSectionTypeStr('LIBRARYCLASSES'), true); + }); + + test('compareLibSectionTypeStr returns false for non-matching', () => { + const sp = new SectionProperties(); + sp.addProperty('Components', 'X64', 'DXE_DRIVER'); + assert.strictEqual(sp.compareLibSectionTypeStr('LibraryClasses'), false); + }); + + // ── compareModuleType ── + + test('compareModuleType returns true for matching module type', () => { + const sp1 = new SectionProperties(); + sp1.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + + const sp2 = new SectionProperties(); + sp2.addProperty('Components', 'IA32', 'DXE_DRIVER'); + + assert.strictEqual(sp1.compareModuleType(sp2), true); + }); + + test('compareModuleType returns false for different module types', () => { + const sp1 = new SectionProperties(); + sp1.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + + const sp2 = new SectionProperties(); + sp2.addProperty('LibraryClasses', 'X64', 'PEIM'); + + assert.strictEqual(sp1.compareModuleType(sp2), false); + }); + + // ── compareModuleTypeStr ── + + test('compareModuleTypeStr case-insensitive', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + assert.strictEqual(sp.compareModuleTypeStr('DXE_DRIVER'), true); + assert.strictEqual(sp.compareModuleTypeStr('dxe_driver'), true); + }); + + test('compareModuleTypeStr returns false for non-matching', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + assert.strictEqual(sp.compareModuleTypeStr('PEIM'), false); + }); + + // ── toString ── + + test('toString returns comma-separated properties', () => { + const sp = new SectionProperties(); + sp.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + sp.addProperty('Components', 'IA32', 'PEIM'); + const str = sp.toString(); + assert.ok(str.includes(','), 'should contain comma separator'); + }); + + // ── multi-property matching ── + + test('compareArch matches any combination', () => { + const sp1 = new SectionProperties(); + sp1.addProperty('LibraryClasses', 'X64', 'DXE_DRIVER'); + sp1.addProperty('LibraryClasses', 'IA32', 'DXE_DRIVER'); + + const sp2 = new SectionProperties(); + sp2.addProperty('Components', 'IA32', 'PEIM'); + + assert.strictEqual(sp1.compareArch(sp2), true); + }); +}); + +// ─── InfDsc ─────────────────────────────────────────────────── + +suite('InfDsc', () => { + test('constructor with section parent sets sectionProperties', () => { + ensureGlobals(); + const loc = makeLoc(10); + const inf = new InfDsc( + 'MdePkg/Library/BaseLib/BaseLib.inf', + loc, + 'Components.X64', + 'MdePkg/Library/BaseLib/BaseLib.inf' + ); + assert.strictEqual(inf.parent, undefined, 'section parent should set parent to undefined'); + assert.ok(inf.sectionProperties.properties.length > 0, 'should have section properties'); + assert.strictEqual(inf.sectionProperties.properties[0].sectionType, 'components'); + assert.strictEqual(inf.sectionProperties.properties[0].arch, 'x64'); + }); + + test('constructor with INF parent sets parent path', () => { + ensureGlobals(); + const loc = makeLoc(5); + const inf = new InfDsc( + 'MdePkg/Library/BaseLib/BaseLib.inf', + loc, + 'SomeModule/Module.inf', + 'SomeLib|MdePkg/Library/BaseLib/BaseLib.inf' + ); + assert.ok(inf.parent !== undefined, 'INF parent should set parent path'); + assert.ok(inf.parent!.includes('Module.inf')); + assert.strictEqual(inf.sectionProperties.properties.length, 0, 'INF parent should not set section properties'); + }); + + test('constructor normalizes path separators', () => { + ensureGlobals(); + const loc = makeLoc(); + const inf = new InfDsc( + 'MdePkg/Library/BaseLib/BaseLib.inf', + loc, + 'Components.common', + 'line text' + ); + // path.sep on Windows is \\, on Linux / + assert.ok(!inf.path.includes('/') || path.sep === '/', 'forward slashes should be normalized to path.sep'); + }); + + test('constructor with multi-section parent', () => { + ensureGlobals(); + const loc = makeLoc(); + const inf = new InfDsc( + 'Pkg/Lib.inf', + loc, + 'LibraryClasses.X64.DXE_DRIVER,LibraryClasses.IA32.PEIM', + 'SomeLib|Pkg/Lib.inf' + ); + assert.strictEqual(inf.parent, undefined); + assert.strictEqual(inf.sectionProperties.properties.length, 2); + assert.strictEqual(inf.sectionProperties.properties[0].arch, 'x64'); + assert.strictEqual(inf.sectionProperties.properties[0].moduleType, 'dxe_driver'); + assert.strictEqual(inf.sectionProperties.properties[1].arch, 'ia32'); + assert.strictEqual(inf.sectionProperties.properties[1].moduleType, 'peim'); + }); + + test('constructor with section missing arch defaults to common', () => { + ensureGlobals(); + const loc = makeLoc(); + const inf = new InfDsc( + 'Pkg/Lib.inf', + loc, + 'libraryclasses', + 'SomeLib|Pkg/Lib.inf' + ); + assert.strictEqual(inf.sectionProperties.properties[0].sectionType, 'libraryclasses'); + assert.strictEqual(inf.sectionProperties.properties[0].arch, 'common'); + assert.strictEqual(inf.sectionProperties.properties[0].moduleType, 'common'); + }); + + test('getModuleTypeStr returns comma-separated module types', () => { + ensureGlobals(); + const loc = makeLoc(); + const inf = new InfDsc( + 'Pkg/Lib.inf', + loc, + 'LibraryClasses.X64.DXE_DRIVER,LibraryClasses.IA32.PEIM', + 'SomeLib|Pkg/Lib.inf' + ); + const moduleTypes = inf.getModuleTypeStr(); + assert.ok(moduleTypes.includes('dxe_driver')); + assert.ok(moduleTypes.includes('peim')); + assert.ok(moduleTypes.includes(',')); + }); + + test('getModuleTypeStr single property', () => { + ensureGlobals(); + const loc = makeLoc(); + const inf = new InfDsc( + 'Pkg/Mod.inf', + loc, + 'Components.X64', + 'Pkg/Mod.inf' + ); + const moduleTypes = inf.getModuleTypeStr(); + assert.strictEqual(moduleTypes, 'common'); + }); + + test('toString includes path and line number', () => { + ensureGlobals(); + const loc = makeLoc(42); + const inf = new InfDsc( + 'Pkg/Mod.inf', + loc, + 'Components.X64', + 'Pkg/Mod.inf' + ); + const str = inf.toString(); + assert.ok(str.includes('42'), 'toString should include line number'); + }); + + test('text property stores original line', () => { + ensureGlobals(); + const loc = makeLoc(); + const inf = new InfDsc( + 'Pkg/Lib.inf', + loc, + 'LibraryClasses.common', + 'BaseLib|MdePkg/Library/BaseLib/BaseLib.inf' + ); + assert.strictEqual(inf.text, 'BaseLib|MdePkg/Library/BaseLib/BaseLib.inf'); + }); + + test('location is preserved', () => { + ensureGlobals(); + const loc = makeLoc(99); + const inf = new InfDsc( + 'Pkg/Lib.inf', + loc, + 'Components', + 'line' + ); + assert.strictEqual(inf.location.range.start.line, 99); + }); +}); + +// ─── EdkWorkspaces ──────────────────────────────────────────── + +suite('EdkWorkspaces', () => { + test('isConfigured returns false when no workspaces', () => { + const ws = new EdkWorkspaces(); + assert.strictEqual(ws.isConfigured(), false); + }); + + test('isConfigured returns true after adding a workspace', async () => { + ensureGlobals(); + const ws = new EdkWorkspaces(); + // Create a minimal mock document for the constructor + const filePath = path.resolve(__dirname, '../../../test/testDscParsing.dsc'); + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath)); + const edkWs = new EdkWorkspace(doc); + ws.workspaces = [edkWs]; + assert.strictEqual(ws.isConfigured(), true); + }); + + test('getInstance returns singleton', () => { + const inst1 = EdkWorkspaces.getInstance(); + const inst2 = EdkWorkspaces.getInstance(); + assert.strictEqual(inst1, inst2); + }); + + test('isFileInUse returns undefined when no workspaces', async () => { + const ws = new EdkWorkspaces(); + const result = await ws.isFileInUse(vscode.Uri.file('d:/fake/file.dsc')); + assert.strictEqual(result, undefined); + }); + + test('getWorkspace returns empty array when no workspaces', async () => { + const ws = new EdkWorkspaces(); + const result = await ws.getWorkspace(vscode.Uri.file('d:/fake/file.dsc')); + assert.strictEqual(result.length, 0); + }); + + test('getDefinition returns undefined when no workspaces', async () => { + const ws = new EdkWorkspaces(); + const result = await ws.getDefinition(vscode.Uri.file('d:/fake/file.dsc'), 'SOME_VAR'); + assert.strictEqual(result, undefined); + }); + + test('replaceDefines returns original text when no workspaces', async () => { + const ws = new EdkWorkspaces(); + const result = await ws.replaceDefines(vscode.Uri.file('d:/fake/file.dsc'), '$(MY_VAR)/path'); + assert.strictEqual(result, '$(MY_VAR)/path'); + }); + + test('getLib returns empty array when no workspaces', async () => { + const ws = new EdkWorkspaces(); + const loc = new vscode.Location( + vscode.Uri.file('d:/fake/file.dsc'), + new vscode.Position(0, 0) + ); + const result = await ws.getLib(loc); + assert.strictEqual(result.length, 0); + }); +}); + +// ─── EdkWorkspace ───────────────────────────────────────────── + +suite('EdkWorkspace', () => { + let workspace: EdkWorkspace; + + suiteSetup(async () => { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test/testDscParsing.dsc'); + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath)); + workspace = new EdkWorkspace(doc); + }); + + // ── Constructor ── + + test('constructor sets mainDsc from document', () => { + assert.ok(workspace.mainDsc.fsPath.endsWith('testDscParsing.dsc')); + }); + + test('constructor generates a numeric id', () => { + assert.strictEqual(typeof workspace.id, 'number'); + assert.ok(workspace.id > 0); + }); + + test('platformName starts as undefined', () => { + assert.strictEqual(workspace.platformName, undefined); + }); + + test('flashDefinitionDocument starts as undefined', () => { + assert.strictEqual(workspace.flashDefinitionDocument, undefined); + }); + + // ── Lists ── + + test('filesLibraries starts empty', () => { + assert.strictEqual(workspace.filesLibraries.length, 0); + }); + + test('filesModules starts empty', () => { + assert.strictEqual(workspace.filesModules.length, 0); + }); + + test('dscList is initially empty', () => { + assert.strictEqual(workspace.dscList().length, 0); + }); + + test('fdfList is initially empty', () => { + assert.strictEqual(workspace.fdfList().length, 0); + }); + + test('getFilesList aggregates all file lists', () => { + assert.ok(Array.isArray(workspace.getFilesList())); + }); + + test('includeTree starts empty', () => { + assert.strictEqual(workspace.includeTree.length, 0); + }); + + // ── Definitions (no processing) ── + + test('getDefinitions returns a Map', () => { + const defs = workspace.getDefinitions(); + assert.ok(defs instanceof Map); + }); + + test('getDefinition returns undefined for unknown key', () => { + assert.strictEqual(workspace.getDefinition('NON_EXISTENT'), undefined); + }); + + test('getDefinitionLocation returns undefined for unknown key', () => { + assert.strictEqual(workspace.getDefinitionLocation('NON_EXISTENT'), undefined); + }); + + test('replaceDefine passes through text with no defines', () => { + const result = workspace.replaceDefine('$(UNKNOWN_VAR)/path'); + assert.strictEqual(result, '$(UNKNOWN_VAR)/path'); + }); + + // ── PCDs ── + + test('getPcds returns undefined for unknown namespace', () => { + assert.strictEqual(workspace.getPcds('gUnknownPkg'), undefined); + }); + + test('getAllPcds returns a Map', () => { + const pcds = workspace.getAllPcds(); + assert.ok(pcds instanceof Map); + }); + + // ── Library / Module management ── + + test('filesLibraries can be set', () => { + const loc = makeLoc(); + const lib = new InfDsc('Pkg/Lib.inf', loc, 'LibraryClasses.common', 'BaseLib|Pkg/Lib.inf'); + workspace.filesLibraries = [lib]; + assert.strictEqual(workspace.filesLibraries.length, 1); + workspace.filesLibraries = []; // reset + }); + + test('filesModules can be set', () => { + const loc = makeLoc(); + const mod = new InfDsc('Pkg/Mod.inf', loc, 'Components.X64', 'Pkg/Mod.inf'); + workspace.filesModules = [mod]; + assert.strictEqual(workspace.filesModules.length, 1); + workspace.filesModules = []; // reset + }); + + test('getFilesList includes libraries and modules', () => { + const loc = makeLoc(); + workspace.filesLibraries = [new InfDsc('Pkg/Lib.inf', loc, 'LibraryClasses', 'Lib|Pkg/Lib.inf')]; + workspace.filesModules = [new InfDsc('Pkg/Mod.inf', loc, 'Components', 'Pkg/Mod.inf')]; + const list = workspace.getFilesList(); + assert.ok(list.length >= 2, 'should include at least library and module paths'); + workspace.filesLibraries = []; + workspace.filesModules = []; + }); + + // ── filesDsc / filesFdf sets ── + + test('filesDsc can be set and read', async () => { + const filePath = path.resolve(__dirname, '../../../test/testDscParsing.dsc'); + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath)); + workspace.filesDsc = new Set([doc]); + assert.strictEqual(workspace.filesDsc.size, 1); + assert.strictEqual(workspace.dscList().length, 1); + workspace.filesDsc = new Set(); + }); + + test('filesFdf can be set and read', async () => { + const filePath = path.resolve(__dirname, '../../../test/testFdfParsing.fdf'); + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath)); + workspace.filesFdf = new Set([doc]); + assert.strictEqual(workspace.filesFdf.size, 1); + assert.strictEqual(workspace.fdfList().length, 1); + workspace.filesFdf = new Set(); + }); + + // ── getLib ── + + test('getLib returns undefined when no matching library', async () => { + const loc = new vscode.Location( + vscode.Uri.file('d:/nonexistent/file.dsc'), + new vscode.Position(999, 0) + ); + const result = await workspace.getLib(loc); + assert.strictEqual(result, undefined); + }); + + test('getLib finds matching library by location', async () => { + const uri = vscode.Uri.file('d:/fake/platform.dsc'); + const loc = new vscode.Location(uri, new vscode.Position(10, 0)); + const lib = new InfDsc('Pkg/Lib.inf', loc, 'LibraryClasses.common', 'BaseLib|Pkg/Lib.inf'); + workspace.filesLibraries = [lib]; + + const result = await workspace.getLib(loc); + assert.ok(result !== undefined, 'should find the library'); + assert.strictEqual(result!.path, lib.path); + workspace.filesLibraries = []; + }); +}); + +// ─── EdkWorkspace.evaluateExpression ────────────────────────── + +suite('EdkWorkspace.evaluateExpression', () => { + let workspace: EdkWorkspace; + + suiteSetup(async () => { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test/testDscParsing.dsc'); + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath)); + workspace = new EdkWorkspace(doc); + }); + + // ── Boolean literals ── + + test('TRUE evaluates to true', () => { + assert.strictEqual(workspace.evaluateExpression('TRUE'), true); + }); + + test('FALSE evaluates to false', () => { + assert.strictEqual(workspace.evaluateExpression('FALSE'), false); + }); + + test('true/false case-insensitive', () => { + assert.strictEqual(workspace.evaluateExpression('true'), true); + assert.strictEqual(workspace.evaluateExpression('True'), true); + assert.strictEqual(workspace.evaluateExpression('false'), false); + }); + + // ── Numeric literals ── + + test('numeric 1 is truthy', () => { + assert.strictEqual(workspace.evaluateExpression('1'), 1); + }); + + test('numeric 0 is falsy', () => { + assert.strictEqual(workspace.evaluateExpression('0'), 0); + }); + + // ── Equality operators ── + + test('== with matching strings', () => { + assert.strictEqual(workspace.evaluateExpression('"hello" == "hello"'), true); + }); + + test('== with non-matching strings', () => { + assert.strictEqual(workspace.evaluateExpression('"hello" == "world"'), false); + }); + + test('!= with different strings', () => { + assert.strictEqual(workspace.evaluateExpression('"hello" != "world"'), true); + }); + + test('!= with same strings', () => { + assert.strictEqual(workspace.evaluateExpression('"hello" != "hello"'), false); + }); + + test('EQ operator', () => { + assert.strictEqual(workspace.evaluateExpression('"a" EQ "a"'), true); + }); + + test('NE operator', () => { + assert.strictEqual(workspace.evaluateExpression('"a" NE "b"'), true); + }); + + // ── Logical operators ── + + test('AND with both true', () => { + assert.strictEqual(workspace.evaluateExpression('TRUE AND TRUE'), true); + }); + + test('AND with one false', () => { + assert.strictEqual(workspace.evaluateExpression('TRUE AND FALSE'), false); + }); + + test('OR with one true', () => { + assert.strictEqual(workspace.evaluateExpression('FALSE OR TRUE'), true); + }); + + test('OR with both false', () => { + assert.strictEqual(workspace.evaluateExpression('FALSE OR FALSE'), false); + }); + + test('&& operator', () => { + assert.strictEqual(workspace.evaluateExpression('TRUE && TRUE'), true); + }); + + test('|| operator', () => { + assert.strictEqual(workspace.evaluateExpression('FALSE || TRUE'), true); + }); + + // ── NOT operator ── + + test('NOT TRUE evaluates to false', () => { + // NOT is a unary operator that uses stack pop for y, x is undefined + const result = workspace.evaluateExpression('NOT TRUE'); + assert.strictEqual(result, false); + }); + + test('NOT FALSE evaluates to true', () => { + const result = workspace.evaluateExpression('NOT FALSE'); + // NOT inverts the value + assert.ok(result, 'NOT FALSE should be truthy'); + }); + + // ── Arithmetic ── + + test('addition', () => { + assert.strictEqual(workspace.evaluateExpression('3 + 2'), 5); + }); + + test('subtraction', () => { + assert.strictEqual(workspace.evaluateExpression('5 - 2'), 3); + }); + + test('multiplication', () => { + assert.strictEqual(workspace.evaluateExpression('3 * 4'), 12); + }); + + test('division', () => { + assert.strictEqual(workspace.evaluateExpression('10 / 2'), 5); + }); + + test('modulus', () => { + assert.strictEqual(workspace.evaluateExpression('10 % 3'), 1); + }); + + // ── Comparison ── + + test('greater than', () => { + assert.strictEqual(workspace.evaluateExpression('5 > 3'), true); + }); + + test('less than', () => { + assert.strictEqual(workspace.evaluateExpression('3 > 5'), false); + }); + + test('greater or equal', () => { + assert.strictEqual(workspace.evaluateExpression('5 >= 5'), true); + }); + + test('less or equal', () => { + assert.strictEqual(workspace.evaluateExpression('3 <= 5'), true); + }); + + // ── Parentheses ── + + test('parentheses group expressions', () => { + assert.strictEqual(workspace.evaluateExpression('(TRUE OR FALSE) AND TRUE'), true); + }); + + test('nested parentheses', () => { + assert.strictEqual(workspace.evaluateExpression('((1 + 2) * 3)'), 9); + }); + + test('unbalanced parentheses throw', () => { + assert.throws(() => { + workspace.evaluateExpression('(TRUE AND FALSE'); + }); + }); + + // ── IN operator ── + + test('IN operator with match', () => { + assert.strictEqual(workspace.evaluateExpression('"X64" IN "X64 IA32 ARM"'), true); + }); + + test('IN operator without match', () => { + assert.strictEqual(workspace.evaluateExpression('"AARCH64" IN "X64 IA32 ARM"'), false); + }); + + // ── Undefined variables (???) ── + + test('undefined variable evaluates to false', () => { + // The expression evaluator replaces "???" with FALSE + assert.strictEqual(workspace.evaluateExpression('"???"'), false); + }); + + // ── String without quotes treated as string ── + + test('bare word is treated as quoted string', () => { + const result = workspace.evaluateExpression('hello == "hello"'); + assert.strictEqual(result, true); + }); + + // ── Complex expressions ── + + test('complex: (1 + 2) > 2 AND TRUE', () => { + assert.strictEqual(workspace.evaluateExpression('(1 + 2) > 2 AND TRUE'), true); + }); + + test('complex: FALSE OR (5 == 5)', () => { + assert.strictEqual(workspace.evaluateExpression('FALSE OR (5 == 5)'), true); + }); +}); diff --git a/src/test/suite/edkWorkspaceProcess.test.ts b/src/test/suite/edkWorkspaceProcess.test.ts new file mode 100644 index 0000000..37f99f0 --- /dev/null +++ b/src/test/suite/edkWorkspaceProcess.test.ts @@ -0,0 +1,422 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { DebugLog } from '../../debugLog'; +import { ConfigAgent } from '../../configuration'; +import { DiagnosticManager } from '../../diagnostics'; +import { EdkWorkspace, InfDsc } from '../../index/edkWorkspace'; +import { PathFind } from '../../pathfind'; + +/** + * Ensure all globals required by EdkWorkspace processing are available. + * Stubs status bar, tree provider, path finder, config agent, and + * diagnostics so the private _processDocument / _doProccessWorkspace + * can run in a test environment. + */ +function ensureProcessingGlobals() { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } + if (!ext.gWorkspacePath) { + ext.gWorkspacePath = path.resolve(__dirname, '../../../'); + } + if (!ext.gConfigAgent) { + ext.gConfigAgent = new ConfigAgent(); + } + if (!ext.gPathFind) { + ext.gPathFind = new PathFind(); + } + // Stub edkWorkspaceTreeProvider.refresh() + if (!ext.edkWorkspaceTreeProvider) { + ext.edkWorkspaceTreeProvider = { refresh() {} }; + } + + // Initialize DiagnosticManager (creates diagnosticsCollection) + DiagnosticManager.getInstance(); + + // Stub edkStatusBar functions that reference myStatusBarItem + // eslint-disable-next-line @typescript-eslint/no-var-requires + const statusBar = require('../../statusBar'); + if (!statusBar.myStatusBarItem) { + statusBar.myStatusBarItem = { + text: '', + tooltip: '', + show() {}, + hide() {}, + backgroundColor: undefined, + command: undefined, + }; + } +} + +/** + * Open a DSC file from the test/ folder and create an EdkWorkspace from it. + */ +async function openDscDocument(filename: string): Promise { + const filePath = path.resolve(__dirname, '../../../test', filename); + const uri = vscode.Uri.file(filePath); + return vscode.workspace.openTextDocument(uri); +} + +// ═══════════════════════════════════════════════════════════════ +// _processDocument tests +// ═══════════════════════════════════════════════════════════════ + +suite('EdkWorkspace._processDocument', () => { + let workspace: EdkWorkspace; + let doc: vscode.TextDocument; + + suiteSetup(async function () { + this.timeout(15_000); + ensureProcessingGlobals(); + doc = await openDscDocument('testDscProcess.dsc'); + workspace = new EdkWorkspace(doc); + // Call private method directly + await (workspace as any)._processDocument(doc, 'DSC'); + }); + + suite('Document Registration', () => { + test('Adds document to filesDsc', () => { + const dscPaths = workspace.dscList(); + assert.ok(dscPaths.length >= 1, 'filesDsc should contain at least the main document'); + assert.ok( + dscPaths.some(p => p.endsWith('testDscProcess.dsc')), + 'filesDsc should include testDscProcess.dsc' + ); + }); + + test('isDocumentInIndex returns true after processing', () => { + assert.strictEqual( + (workspace as any).isDocumentInIndex(doc), + true, + 'Document should be in the index after processing' + ); + }); + + test('Processing same document twice is a no-op', async () => { + const sizeBefore = workspace.dscList().length; + await (workspace as any)._processDocument(doc, 'DSC'); + assert.strictEqual(workspace.dscList().length, sizeBefore, 'Should not re-add'); + }); + }); + + suite('Defines Extraction', () => { + test('Extracts PLATFORM_NAME from [Defines]', () => { + assert.strictEqual(workspace.getDefinition('PLATFORM_NAME'), 'TestProcess'); + }); + + test('Extracts PLATFORM_GUID', () => { + assert.strictEqual( + workspace.getDefinition('PLATFORM_GUID'), + '11111111-2222-3333-4444-555555555555' + ); + }); + + test('Extracts DEFINE MY_FLAG', () => { + assert.strictEqual(workspace.getDefinition('MY_FLAG'), 'TRUE'); + }); + + test('Extracts DEFINE with empty value', () => { + const val = workspace.getDefinition('EMPTY_DEF'); + assert.ok(val !== undefined, 'EMPTY_DEF should be defined'); + assert.strictEqual(val!.trim(), ''); + }); + + test('Extracts root-level DEFINE', () => { + assert.strictEqual(workspace.getDefinition('ROOT_DEF'), 'RootValue'); + }); + + test('getDefinitionLocation returns a Location for known define', () => { + const loc = workspace.getDefinitionLocation('PLATFORM_NAME'); + assert.ok(loc !== undefined, 'Should have a location'); + assert.ok(loc!.uri.fsPath.endsWith('testDscProcess.dsc')); + }); + + test('getDefinitions returns all defines as a Map', () => { + const defs = workspace.getDefinitions(); + assert.ok(defs instanceof Map); + assert.ok(defs.size >= 5, `Expected >=5 defines, got ${defs.size}`); + }); + }); + + suite('Define Variable Substitution', () => { + test('DERIVED define has $(BASE) resolved', () => { + const val = workspace.getDefinition('DERIVED'); + assert.strictEqual(val, 'HelloWorld', 'DERIVED should be resolved to HelloWorld'); + }); + + test('replaceDefine substitutes known variables', () => { + const result = workspace.replaceDefine('$(MY_FLAG)'); + assert.strictEqual(result, 'TRUE'); + }); + + test('replaceDefine leaves unknown variables untouched', () => { + const result = workspace.replaceDefine('$(TOTALLY_UNKNOWN)'); + assert.strictEqual(result, '$(TOTALLY_UNKNOWN)'); + }); + }); + + suite('PCD Extraction', () => { + test('Extracts PCDs in gTestPkg namespace', () => { + const pcds = workspace.getPcds('gTestPkg'); + assert.ok(pcds !== undefined, 'gTestPkg PCDs should exist'); + }); + + test('PcdTestMask has correct value', () => { + const pcds = workspace.getPcds('gTestPkg'); + const pcd = pcds?.get('PcdTestMask'); + assert.ok(pcd !== undefined, 'PcdTestMask should exist'); + assert.strictEqual(pcd!.value, '0x2F'); + }); + + test('PcdBootTimeout from DynamicDefault', () => { + const pcds = workspace.getPcds('gTestPkg'); + const pcd = pcds?.get('PcdBootTimeout'); + assert.ok(pcd !== undefined, 'PcdBootTimeout should exist'); + assert.strictEqual(pcd!.value, '5'); + }); + + test('PCD with L"string" value strips L prefix', () => { + const pcds = workspace.getPcds('gTestPkg'); + const pcd = pcds?.get('PcdStringVal'); + assert.ok(pcd !== undefined, 'PcdStringVal should exist'); + assert.ok(pcd!.value.startsWith('"'), 'Value should start with " after L is stripped'); + }); + + test('PCD has location information', () => { + const pcds = workspace.getPcds('gTestPkg'); + const pcd = pcds?.get('PcdTestMask'); + assert.ok(pcd!.position.uri.fsPath.endsWith('testDscProcess.dsc')); + }); + + test('getAllPcds returns all namespaces', () => { + const all = workspace.getAllPcds(); + assert.ok(all.has('gTestPkg'), 'Should have gTestPkg namespace'); + }); + }); + + suite('Conditional Processing', () => { + test('!if TRUE branch: COND_TAKEN is defined', () => { + assert.strictEqual(workspace.getDefinition('COND_TAKEN'), 'IfTrueValue'); + }); + + test('!if TRUE branch: else branch not taken', () => { + // COND_TAKEN should NOT be IfFalseValue + assert.notStrictEqual(workspace.getDefinition('COND_TAKEN'), 'IfFalseValue'); + }); + + test('!if FALSE: else branch taken, COND_ELSE is defined', () => { + assert.strictEqual(workspace.getDefinition('COND_ELSE'), 'ElseValue'); + }); + + test('!if FALSE: if branch not taken, COND_FALSE_IF not defined', () => { + assert.strictEqual(workspace.getDefinition('COND_FALSE_IF'), undefined); + }); + + test('Nested conditionals: outer TRUE inner FALSE -> INNER_ELSE defined', () => { + assert.strictEqual(workspace.getDefinition('OUTER_TRUE'), 'OuterOk'); + assert.strictEqual(workspace.getDefinition('INNER_ELSE'), 'InnerElseOk'); + }); + + test('Nested conditionals: INNER_FALSE not defined', () => { + assert.strictEqual(workspace.getDefinition('INNER_FALSE'), undefined); + }); + + test('!ifdef on existing variable takes the branch', () => { + assert.strictEqual(workspace.getDefinition('IFDEF_TAKEN'), 'yes'); + }); + + test('!ifndef on undefined variable takes the branch', () => { + assert.strictEqual(workspace.getDefinition('IFNDEF_TAKEN'), 'yes'); + }); + + test('COND_A before conditional is still defined', () => { + assert.strictEqual(workspace.getDefinition('COND_A'), 'BeforeIf'); + }); + }); + + suite('Library and Module References', () => { + test('Libraries are collected (even if paths unresolved)', () => { + // Libraries are added even when gPathFind.findPath returns empty + assert.ok(workspace.filesLibraries.length >= 2, + `Expected >=2 library refs, got ${workspace.filesLibraries.length}`); + }); + + test('Modules are collected (even if paths unresolved)', () => { + assert.ok(workspace.filesModules.length >= 2, + `Expected >=2 module refs, got ${workspace.filesModules.length}`); + }); + + test('Library InfDsc has correct section properties', () => { + const lib = workspace.filesLibraries.find(l => l.path.includes('BaseLib')); + assert.ok(lib !== undefined, 'BaseLib should be in libraries'); + assert.ok(lib!.sectionProperties.properties.length > 0); + }); + + test('Module InfDsc preserves location', () => { + const mod = workspace.filesModules[0]; + assert.ok(mod.location.uri.fsPath.endsWith('testDscProcess.dsc')); + }); + }); + + suite('Grayout Ranges', () => { + test('parsedDocuments has entry for processed document', () => { + const ranges = (workspace as any).parsedDocuments.get(doc.uri.fsPath); + assert.ok(ranges !== undefined, 'Should have grayout entry'); + }); + + test('Grayout ranges exist for inactive conditional blocks', () => { + const ranges: vscode.Range[] = (workspace as any).parsedDocuments.get(doc.uri.fsPath); + // The !if FALSE ... !else block should generate at least one grayout range + assert.ok(ranges.length >= 1, + `Expected >=1 grayout ranges, got ${ranges.length}`); + }); + }); + + suite('Comment Stripping', () => { + test('stripComment removes line comments', () => { + const result = (workspace as any).stripComment('DEFINE X = 1 # comment'); + assert.strictEqual(result, 'DEFINE X = 1'); + }); + + test('stripComment preserves hash inside quotes', () => { + const result = (workspace as any).stripComment('DEFINE X = "value#with#hash"'); + assert.strictEqual(result, 'DEFINE X = "value#with#hash"'); + }); + + test('stripComment trims whitespace', () => { + const result = (workspace as any).stripComment(' some text '); + assert.strictEqual(result, 'some text'); + }); + + test('stripComment returns empty for comment-only line', () => { + const result = (workspace as any).stripComment('# just a comment'); + assert.strictEqual(result, ''); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════════ +// _doProccessWorkspace tests +// ═══════════════════════════════════════════════════════════════ + +suite('EdkWorkspace._doProccessWorkspace', () => { + let workspace: EdkWorkspace; + + suiteSetup(async function () { + this.timeout(15_000); + ensureProcessingGlobals(); + const doc = await openDscDocument('testDscProcess.dsc'); + workspace = new EdkWorkspace(doc); + const result = await (workspace as any)._doProccessWorkspace(); + assert.ok(result === true, '_doProccessWorkspace should return true'); + }); + + suite('Workspace Initialization', () => { + test('platformName is populated from PLATFORM_NAME define', () => { + assert.strictEqual(workspace.platformName, 'TestProcess'); + }); + + test('workInProgress is false after completion', () => { + assert.strictEqual((workspace as any).workInProgress, false); + }); + + test('processComplete is true after completion', () => { + assert.strictEqual((workspace as any).processComplete, true); + }); + }); + + suite('State Reset', () => { + test('Running again resets and re-processes', async () => { + // First run already done in suiteSetup. Reset workInProgress flag + // by accessing the internal state directly (it was set to false). + const result = await (workspace as any)._doProccessWorkspace(); + assert.ok(result === true, 'Second run should succeed'); + assert.strictEqual(workspace.platformName, 'TestProcess'); + }); + + test('Returns false if already in progress', async () => { + (workspace as any).workInProgress = true; + const result = await (workspace as any)._doProccessWorkspace(); + assert.strictEqual(result, false, 'Should return false when workInProgress'); + (workspace as any).workInProgress = false; + }); + }); + + suite('Defines After Full Processing', () => { + test('All defines from [Defines] section are available', () => { + const defs = workspace.getDefinitions(); + assert.ok(defs.has('PLATFORM_NAME')); + assert.ok(defs.has('MY_FLAG')); + assert.ok(defs.has('MY_PATH')); + }); + + test('Conditional defines are correctly resolved', () => { + assert.strictEqual(workspace.getDefinition('COND_TAKEN'), 'IfTrueValue'); + assert.strictEqual(workspace.getDefinition('COND_ELSE'), 'ElseValue'); + }); + + test('replaceDefine works after processing', () => { + const result = workspace.replaceDefine('$(PLATFORM_NAME)'); + assert.strictEqual(result, 'TestProcess'); + }); + }); + + suite('PCDs After Full Processing', () => { + test('PCDs are available after processing', () => { + const pcds = workspace.getPcds('gTestPkg'); + assert.ok(pcds !== undefined); + assert.ok(pcds!.size >= 3, `Expected >=3 PCDs, got ${pcds!.size}`); + }); + }); + + suite('File Lists After Full Processing', () => { + test('dscList contains the main document', () => { + const list = workspace.dscList(); + assert.ok(list.length >= 1, 'dscList should have at least 1 entry'); + assert.ok(list.some(p => p.endsWith('testDscProcess.dsc'))); + }); + + test('Libraries are populated', () => { + assert.ok(workspace.filesLibraries.length >= 2); + }); + + test('Modules are populated', () => { + assert.ok(workspace.filesModules.length >= 2); + }); + + test('getFilesList aggregates all lists', () => { + const all = workspace.getFilesList(); + assert.ok(all.length >= 4, + `Expected >=4 total files (dsc+libs+mods), got ${all.length}`); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════════ +// proccessWorkspace (public API) tests +// ═══════════════════════════════════════════════════════════════ + +suite('EdkWorkspace.proccessWorkspace', () => { + test('proccessWorkspace runs without errors', async function () { + this.timeout(15_000); + ensureProcessingGlobals(); + const doc = await openDscDocument('testDscProcess.dsc'); + const workspace = new EdkWorkspace(doc); + const result = await workspace.proccessWorkspace(); + assert.ok(result === true, 'proccessWorkspace should return true'); + assert.strictEqual(workspace.platformName, 'TestProcess'); + }); + + test('proccessWorkspace populates defines and PCDs', async function () { + this.timeout(15_000); + ensureProcessingGlobals(); + const doc = await openDscDocument('testDscProcess.dsc'); + const workspace = new EdkWorkspace(doc); + await workspace.proccessWorkspace(); + assert.ok(workspace.getDefinitions().size > 0, 'Should have defines'); + assert.ok(workspace.getAllPcds().size > 0, 'Should have PCDs'); + }); +}); diff --git a/src/test/suite/fdfParser.test.ts b/src/test/suite/fdfParser.test.ts new file mode 100644 index 0000000..ef633bf --- /dev/null +++ b/src/test/suite/fdfParser.test.ts @@ -0,0 +1,101 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { FdfParser } from '../../edkParser/fdfParser'; +import { Edk2SymbolType } from '../../symbols/symbolsType'; +import { DebugLog } from '../../debugLog'; + +function ensureGlobals() { + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } +} + +async function parseFdfFile(filename: string): Promise { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test', filename); + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + const parser = new FdfParser(document); + await parser.parseFile(); + return parser; +} + +function symbolsOfType(parser: FdfParser, type: Edk2SymbolType) { + return parser.symbolsList.filter(s => s.type === type); +} + +suite('FDF Parser – Symbol Extraction', () => { + + let parser: FdfParser; + + suiteSetup(async function () { + this.timeout(10_000); + parser = await parseFdfFile('testFdfParsing.fdf'); + }); + + suite('Sections', () => { + test('Parses [FD.*] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.fdfSection); + const fd = sections.filter(s => /FD\./i.test(s.name)); + assert.ok(fd.length >= 1, `Expected >=1 FD section, got ${fd.length}`); + }); + + test('Parses [FV.*] sections', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.fdfSection); + const fv = sections.filter(s => /FV\./i.test(s.name)); + assert.ok(fv.length >= 2, `Expected >=2 FV sections, got ${fv.length}`); + }); + + test('Parses [Rule.*] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.fdfSection); + const rules = sections.filter(s => /Rule\./i.test(s.name)); + assert.ok(rules.length >= 1, `Expected >=1 Rule section, got ${rules.length}`); + }); + }); + + suite('INF References', () => { + test('Parses INF module references', () => { + const infs = symbolsOfType(parser, Edk2SymbolType.fdfInf); + assert.ok(infs.length >= 3, `Expected >=3 INF refs, got ${infs.length}`); + }); + }); + + suite('Defines', () => { + test('Parses DEFINE statements', () => { + const defs = symbolsOfType(parser, Edk2SymbolType.fdfDefinition); + assert.ok(defs.length >= 2, `Expected >=2 DEFINE statements, got ${defs.length}`); + }); + }); + + suite('Includes', () => { + test('Parses !include directive', () => { + const includes = symbolsOfType(parser, Edk2SymbolType.fdfInclude); + assert.ok(includes.length >= 1, 'Expected at least one !include'); + const inc = includes.find(s => /TestFdfInclude/i.test(s.name)); + assert.ok(inc, 'TestFdfInclude.fdf.inc should be found'); + }); + }); + + suite('Consistency', () => { + test('symbolsTree is not empty', () => { + assert.ok(parser.symbolsTree.length > 0, 'symbolsTree should not be empty'); + }); + + test('symbolsList equals recursive tree count', () => { + function countNodes(nodes: vscode.DocumentSymbol[]): number { + let c = 0; + for (const n of nodes) { c++; c += countNodes(n.children); } + return c; + } + assert.strictEqual(parser.symbolsList.length, countNodes(parser.symbolsTree)); + }); + + test('Comments are not parsed as symbols', () => { + for (const s of parser.symbolsList) { + assert.ok(!s.name.startsWith('#'), `Symbol should not start with #: "${s.name}"`); + } + }); + }); +}); diff --git a/src/test/suite/infParser.test.ts b/src/test/suite/infParser.test.ts new file mode 100644 index 0000000..095c038 --- /dev/null +++ b/src/test/suite/infParser.test.ts @@ -0,0 +1,187 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { InfParser } from '../../edkParser/infParser'; +import { Edk2SymbolType } from '../../symbols/symbolsType'; +import { DebugLog } from '../../debugLog'; + +function ensureGlobals() { + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } +} + +async function parseInfFile(filename: string): Promise { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test', filename); + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + const parser = new InfParser(document); + await parser.parseFile(); + return parser; +} + +function symbolsOfType(parser: InfParser, type: Edk2SymbolType) { + return parser.symbolsList.filter(s => s.type === type); +} + +suite('INF Parser – Symbol Extraction', () => { + + let parser: InfParser; + + suiteSetup(async function () { + this.timeout(10_000); + parser = await parseInfFile('testInfParsing.inf'); + }); + + suite('Sections', () => { + test('Parses [Defines] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSection); + const defines = sections.filter(s => /defines/i.test(s.name)); + assert.ok(defines.length >= 1, 'Expected at least one [Defines] section'); + }); + + test('Parses [Sources] section(s)', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionSource); + assert.ok(sections.length >= 1, `Expected >=1 Sources section, got ${sections.length}`); + }); + + test('Parses [Packages] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionPackages); + assert.ok(sections.length >= 1, 'Expected at least one [Packages] section'); + }); + + test('Parses [LibraryClasses] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionLibraries); + assert.ok(sections.length >= 1, 'Expected at least one [LibraryClasses] section'); + }); + + test('Parses [Protocols] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionProtocols); + assert.ok(sections.length >= 1, 'Expected at least one [Protocols] section'); + }); + + test('Parses [Ppis] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionPpis); + assert.ok(sections.length >= 1, 'Expected at least one [Ppis] section'); + }); + + test('Parses [Guids] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionGuids); + assert.ok(sections.length >= 1, 'Expected at least one [Guids] section'); + }); + + test('Parses [Pcd] / [FixedPcd] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionPcds); + assert.ok(sections.length >= 1, 'Expected at least one PCD section'); + }); + + test('Parses [Depex] section', () => { + const sections = symbolsOfType(parser, Edk2SymbolType.infSectionDepex); + assert.ok(sections.length >= 1, 'Expected at least one [Depex] section'); + }); + }); + + suite('Defines', () => { + test('Parses INF defines (MODULE_TYPE, BASE_NAME, etc.)', () => { + const defines = symbolsOfType(parser, Edk2SymbolType.infDefine); + assert.ok(defines.length >= 7, `Expected >=7 defines, got ${defines.length}`); + }); + + test('Parses ENTRY_POINT define', () => { + const defines = symbolsOfType(parser, Edk2SymbolType.infDefine); + const ep = defines.find(s => /ENTRY_POINT/i.test(s.name)); + assert.ok(ep, 'ENTRY_POINT should be parsed'); + }); + + test('Parses CONSTRUCTOR define', () => { + const defines = symbolsOfType(parser, Edk2SymbolType.infDefine); + const ctor = defines.find(s => /CONSTRUCTOR/i.test(s.name)); + assert.ok(ctor, 'CONSTRUCTOR should be parsed'); + }); + + test('Parses DESTRUCTOR define', () => { + const defines = symbolsOfType(parser, Edk2SymbolType.infDefine); + const dtor = defines.find(s => /DESTRUCTOR/i.test(s.name)); + assert.ok(dtor, 'DESTRUCTOR should be parsed'); + }); + }); + + suite('Sources', () => { + test('Parses source file entries', () => { + const sources = symbolsOfType(parser, Edk2SymbolType.infSource); + assert.ok(sources.length >= 3, `Expected >=3 source entries, got ${sources.length}`); + }); + }); + + suite('Packages', () => { + test('Parses package references', () => { + const packages = symbolsOfType(parser, Edk2SymbolType.infPackage); + assert.ok(packages.length >= 2, `Expected >=2 packages, got ${packages.length}`); + }); + }); + + suite('Libraries', () => { + test('Parses library class references', () => { + const libs = symbolsOfType(parser, Edk2SymbolType.infLibrary); + assert.ok(libs.length >= 3, `Expected >=3 library references, got ${libs.length}`); + }); + }); + + suite('Protocols', () => { + test('Parses protocol entries', () => { + const protocols = symbolsOfType(parser, Edk2SymbolType.infProtocol); + assert.ok(protocols.length >= 2, `Expected >=2 protocols, got ${protocols.length}`); + }); + }); + + suite('PPIs', () => { + test('Parses PPI entries', () => { + const ppis = symbolsOfType(parser, Edk2SymbolType.infPpi); + assert.ok(ppis.length >= 1, `Expected >=1 PPI, got ${ppis.length}`); + }); + }); + + suite('GUIDs', () => { + test('Parses GUID entries', () => { + const guids = symbolsOfType(parser, Edk2SymbolType.infGuid); + assert.ok(guids.length >= 2, `Expected >=2 GUIDs, got ${guids.length}`); + }); + }); + + suite('PCDs', () => { + test('Parses PCD entries', () => { + const pcds = symbolsOfType(parser, Edk2SymbolType.infPcd); + assert.ok(pcds.length >= 1, `Expected >=1 PCD, got ${pcds.length}`); + }); + }); + + suite('Depex', () => { + test('Parses dependency expression entries', () => { + const depex = symbolsOfType(parser, Edk2SymbolType.infDepex); + assert.ok(depex.length >= 1, `Expected >=1 depex entry, got ${depex.length}`); + }); + }); + + suite('Consistency', () => { + test('symbolsTree is not empty', () => { + assert.ok(parser.symbolsTree.length > 0, 'symbolsTree should not be empty'); + }); + + test('symbolsList equals recursive tree count', () => { + function countNodes(nodes: vscode.DocumentSymbol[]): number { + let c = 0; + for (const n of nodes) { c++; c += countNodes(n.children); } + return c; + } + assert.strictEqual(parser.symbolsList.length, countNodes(parser.symbolsTree)); + }); + + test('Comments are not parsed as symbols', () => { + for (const s of parser.symbolsList) { + assert.ok(!s.name.startsWith('#'), `Symbol should not start with #: "${s.name}"`); + } + }); + }); +}); diff --git a/src/test/suite/vfrParser.test.ts b/src/test/suite/vfrParser.test.ts new file mode 100644 index 0000000..714f7e1 --- /dev/null +++ b/src/test/suite/vfrParser.test.ts @@ -0,0 +1,116 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { VfrParser } from '../../edkParser/vfrParser'; +import { Edk2SymbolType } from '../../symbols/symbolsType'; +import { DebugLog } from '../../debugLog'; + +function ensureGlobals() { + const ext = require('../../extension'); + if (!ext.gDebugLog) { + ext.gDebugLog = new DebugLog(); + } +} + +async function parseVfrFile(filename: string): Promise { + ensureGlobals(); + const filePath = path.resolve(__dirname, '../../../test', filename); + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + const parser = new VfrParser(document); + await parser.parseFile(); + return parser; +} + +function symbolsOfType(parser: VfrParser, type: Edk2SymbolType) { + return parser.symbolsList.filter(s => s.type === type); +} + +suite('VFR Parser – Symbol Extraction', () => { + + let parser: VfrParser; + + suiteSetup(async function () { + this.timeout(10_000); + parser = await parseVfrFile('testVfrParsing.vfr'); + }); + + suite('Formset', () => { + test('Parses formset', () => { + const formsets = symbolsOfType(parser, Edk2SymbolType.vfrFormset); + assert.ok(formsets.length >= 1, 'Expected at least one formset'); + }); + + test('Parses form blocks', () => { + const forms = symbolsOfType(parser, Edk2SymbolType.vfrForm); + assert.ok(forms.length >= 0, `Unexpected negative form count: ${forms.length}`); + }); + }); + + suite('Controls', () => { + test('Parses oneof controls', () => { + const oneofs = symbolsOfType(parser, Edk2SymbolType.vfrOneof); + assert.ok(oneofs.length >= 2, `Expected >=2 oneofs, got ${oneofs.length}`); + }); + + test('Parses checkbox controls', () => { + const checkboxes = symbolsOfType(parser, Edk2SymbolType.vfrCheckbox); + assert.ok(checkboxes.length >= 1, `Expected >=1 checkbox, got ${checkboxes.length}`); + }); + + test('Parses numeric controls', () => { + const numerics = symbolsOfType(parser, Edk2SymbolType.vfrNumeric); + assert.ok(numerics.length >= 1, `Expected >=1 numeric, got ${numerics.length}`); + }); + + test('Parses string controls', () => { + const strings = symbolsOfType(parser, Edk2SymbolType.vfrString); + assert.ok(strings.length >= 1, `Expected >=1 string control, got ${strings.length}`); + }); + + test('Parses password controls', () => { + const passwords = symbolsOfType(parser, Edk2SymbolType.vfrPassword); + assert.ok(passwords.length >= 1, `Expected >=1 password control, got ${passwords.length}`); + }); + + test('Parses goto references', () => { + const gotos = symbolsOfType(parser, Edk2SymbolType.vfrGoto); + assert.ok(gotos.length >= 2, `Expected >=2 goto refs, got ${gotos.length}`); + }); + + test('Parses prompt entries inside controls', () => { + const prompts = symbolsOfType(parser, Edk2SymbolType.vfrString); + assert.ok(prompts.length >= 2, `Expected >=2 prompt/string entries, got ${prompts.length}`); + }); + }); + + suite('Tree Structure', () => { + test('symbolsTree is not empty', () => { + assert.ok(parser.symbolsTree.length > 0); + }); + + test('Formset or form has children', () => { + const formsets = symbolsOfType(parser, Edk2SymbolType.vfrFormset); + const forms = symbolsOfType(parser, Edk2SymbolType.vfrForm); + const withChildren = [...formsets, ...forms].filter(s => s.children.length > 0); + assert.ok(withChildren.length > 0, 'At least one formset or form should have children'); + }); + }); + + suite('Consistency', () => { + test('symbolsList equals recursive tree count', () => { + function countNodes(nodes: vscode.DocumentSymbol[]): number { + let c = 0; + for (const n of nodes) { c++; c += countNodes(n.children); } + return c; + } + assert.strictEqual(parser.symbolsList.length, countNodes(parser.symbolsTree)); + }); + + test('Comments are not parsed as symbols', () => { + for (const s of parser.symbolsList) { + assert.ok(!s.name.startsWith('//'), `Symbol should not start with //: "${s.name}"`); + } + }); + }); +}); diff --git a/src/treeElements/DefinesTreeItem.ts b/src/treeElements/DefinesTreeItem.ts new file mode 100644 index 0000000..73283d5 --- /dev/null +++ b/src/treeElements/DefinesTreeItem.ts @@ -0,0 +1,70 @@ +import * as vscode from 'vscode'; +import { gEdkWorkspaces } from '../extension'; +import { TreeItem } from './TreeItem'; + +export class DefinesRootItem extends TreeItem { + constructor( label: string) { + super(label, vscode.TreeItemCollapsibleState.Collapsed); + } + + // Override to populate children dynamically + async expand() { + this.children = []; // Clear current children + + if (this.label === "DEFINES") { + this.populateDefines(); + } else if (this.label === "PCDs") { + this.populatePcds(); + } + } + + private populateDefines() { + if (!gEdkWorkspaces || !gEdkWorkspaces.workspaces) { return; } + + for (const wp of gEdkWorkspaces.workspaces) { + let defs = wp.getDefinitions(); + for (const [key, value] of defs.entries()) { + const label = `${key}`; + const item = new TreeItem(label, vscode.TreeItemCollapsibleState.None); + item.description = value.value; + if (value.location) { + item.command = { + command: 'vscode.open', + title: 'Open', + arguments: [value.location.uri, { selection: value.location.range }] + }; + item.tooltip = label; + } + this.addChildren(item); + } + } + } + + private populatePcds() { + if (!gEdkWorkspaces || !gEdkWorkspaces.workspaces) { return; } + + for (const wp of gEdkWorkspaces.workspaces) { + let pcds = wp.getAllPcds(); + for (const [namespace, pcdMap] of pcds) { + for (const [name, pcd] of pcdMap) { + const label = `${namespace}.${name} = ${pcd.value}`; + const item = new TreeItem(label, vscode.TreeItemCollapsibleState.None); + item.command = { + command: 'vscode.open', + title: 'Open', + arguments: [pcd.position.uri, { selection: pcd.position.range }] + }; + item.tooltip = label; + this.addChildren(item); + } + } + } + } + + // Needed because TreeItem implementation uses it + addChildren(node: TreeItem) { + node.parent = this; + // this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; // Don't auto expand when adding children internally + this.children.push(node); + } +} diff --git a/src/treeElements/Library.ts b/src/treeElements/Library.ts index 0a600c1..60d557a 100644 --- a/src/treeElements/Library.ts +++ b/src/treeElements/Library.ts @@ -132,7 +132,7 @@ export class EdkInfNodeLibrary extends EdkInfNode{ this.setDuplicatedLibrary(); }else{ librarySet.add(uri.fsPath); - gDebugLog.verbose(`${uri.fsPath}`); + gDebugLog.trace(`${uri.fsPath}`); } } } diff --git a/src/treeElements/Source.ts b/src/treeElements/Source.ts index 855ffd4..7e27b9c 100644 --- a/src/treeElements/Source.ts +++ b/src/treeElements/Source.ts @@ -2,12 +2,11 @@ import path = require("path"); import { CompileCommandsEntry } from "../compileCommands"; import { InfParser } from "../edkParser/infParser"; import { getParser } from "../edkParser/parserFactory"; -import { edkLensTreeDetailProvider, gCompileCommands, gDebugLog, gMapFileManager, gPathFind } from "../extension"; +import { gCompileCommands, gDebugLog, gMapFileManager, gPathFind } from "../extension"; import { EdkWorkspace } from "../index/edkWorkspace"; import { EdkSymbol } from "../symbols/edkSymbols"; import { EdkSymbolInfLibrary, EdkSymbolInfSource } from "../symbols/infSymbols"; import { Edk2SymbolType } from "../symbols/symbolsType"; -import { findHeaderIncludes, HeaderFileTreeItemLibraryTree } from "../TreeDataProvider"; import { getAllSymbols, openTextDocument } from "../utils"; import { EdkNode } from "./EdkObject"; import * as vscode from 'vscode'; diff --git a/src/treeElements/Symbol.ts b/src/treeElements/Symbol.ts index 4aae153..012c307 100644 --- a/src/treeElements/Symbol.ts +++ b/src/treeElements/Symbol.ts @@ -29,65 +29,6 @@ var baseTypeSet = new Set([ ]); -function getIconForSymbolKind(kind: vscode.SymbolKind): string { - switch(kind) { - case vscode.SymbolKind.File: - return "symbol-file"; - case vscode.SymbolKind.Module: - return "symbol-module"; - case vscode.SymbolKind.Namespace: - return "symbol-namespace"; - case vscode.SymbolKind.Package: - return "symbol-package"; - case vscode.SymbolKind.Class: - return "symbol-class"; - case vscode.SymbolKind.Method: - return "symbol-method"; - case vscode.SymbolKind.Property: - return "symbol-property"; - case vscode.SymbolKind.Field: - return "symbol-field"; - case vscode.SymbolKind.Constructor: - return "symbol-constructor"; - case vscode.SymbolKind.Enum: - return "symbol-enum"; - case vscode.SymbolKind.Interface: - return "symbol-interface"; - case vscode.SymbolKind.Function: - return "symbol-function"; - case vscode.SymbolKind.Variable: - return "symbol-variable"; - case vscode.SymbolKind.Constant: - return "symbol-constant"; - case vscode.SymbolKind.String: - return "symbol-string"; - case vscode.SymbolKind.Number: - return "symbol-number"; - case vscode.SymbolKind.Boolean: - return "symbol-boolean"; - case vscode.SymbolKind.Array: - return "symbol-array"; - case vscode.SymbolKind.Object: - return "symbol-object"; - case vscode.SymbolKind.Key: - return "symbol-key"; - case vscode.SymbolKind.Null: - return "symbol-null"; - case vscode.SymbolKind.EnumMember: - return "symbol-enum-member"; - case vscode.SymbolKind.Struct: - return "symbol-struct"; - case vscode.SymbolKind.Event: - return "symbol-event"; - case vscode.SymbolKind.Operator: - return "symbol-operator"; - case vscode.SymbolKind.TypeParameter: - return "symbol-type-parameter"; - default: - return "symbol-misc"; - } - } - export class EdkSymbolNode extends EdkNode{ uri:vscode.Uri; range:vscode.Range; @@ -99,7 +40,7 @@ export class EdkSymbolNode extends EdkNode{ this.label = symbol.name; this.name = symbol.name; this.description = symbol.detail.length?symbol.detail:vscode.SymbolKind[symbol.kind]; - this.iconPath = new vscode.ThemeIcon(getIconForSymbolKind(symbol.kind)); + this.iconPath = EdkSymbol.iconForKind(symbol.kind); this.collapsibleState = vscode.TreeItemCollapsibleState.None; this.range = symbol.range; this.uri = uri; diff --git a/src/treeElements/TreeItem.ts b/src/treeElements/TreeItem.ts index 8bca7e9..f1d3cc9 100644 --- a/src/treeElements/TreeItem.ts +++ b/src/treeElements/TreeItem.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import { edkLensTreeDetailProvider } from '../extension'; export class TreeItem extends vscode.TreeItem { children: TreeItem[] = []; @@ -36,13 +35,13 @@ export class TreeItem extends vscode.TreeItem { toString(){ let result = ""; if(typeof this.label === 'string'){ - result = this.label; + result = `**${this.label}**`; } if(typeof this.label === 'object' && 'label' in this.label){ - result = this.label.label; + result = `**${this.label.label}**`; } - result += this.description ? ` *${this.description}*` : ''; + result += this.description ? ` - *${this.description}*` : ''; return result; } @@ -51,12 +50,10 @@ export class TreeItem extends vscode.TreeItem { setLoading(){ this.tempIcon = this.iconPath; this.iconPath = new vscode.ThemeIcon("loading~spin"); - edkLensTreeDetailProvider.refresh(); } clearLoading(){ this.iconPath = this.tempIcon; - edkLensTreeDetailProvider.refresh(); } needsExpandProcess(){ diff --git a/src/ui/messages.ts b/src/ui/messages.ts index ed45ad8..88b69ad 100644 --- a/src/ui/messages.ts +++ b/src/ui/messages.ts @@ -17,7 +17,7 @@ export function askReloadFiles(){ export function infoMissingCompileInfo(){ void vscode.window.showInformationMessage("EDK2 Compile Information folder is missing.", "How to enable?").then(async selection => { if (selection === "How to enable?"){ - void vscode.env.openExternal(vscode.Uri.parse("https://github.com/intel/Edk2Code/wiki/Index-source-code#enable-compile-information")); + void vscode.env.openExternal(vscode.Uri.parse("https://intel.github.io/Edk2Code/advance_features/#how-to-enable-compile-information")); } }); } diff --git a/src/usedFileTracker.ts b/src/usedFileTracker.ts index c542a1f..e3b6f3f 100644 --- a/src/usedFileTracker.ts +++ b/src/usedFileTracker.ts @@ -28,7 +28,7 @@ export class FileUseWarning { if(langId===undefined){return;} if(!["c","cpp","edk2_dsc","edk2_inf","edk2_dec","asl","edk2_vfr","edk2_fdf", "edk2_uni"].includes(langId)){ edkStatusBar.setColor('statusBarItem.activeBackground'); - edkStatusBar.setHelpUrl("https://github.com/intel/Edk2Code/wiki"); + edkStatusBar.setHelpUrl("https://intel.github.io/Edk2Code/"); edkStatusBar.setText(`No EDK file`); return; } @@ -42,11 +42,6 @@ export class FileUseWarning { // // Notify user if file is in use // - - // edkStatusBar.setHelpUrl("https://github.com/intel/Edk2Code/wiki"); - // edkStatusBar.setText(""); - // edkStatusBar.setText(`$(check) ${path.basename(document.fileName)}`); - if(gEdkWorkspaces.isConfigured()){ let wps = await gEdkWorkspaces.getWorkspace(editor.document.uri); let wpFound = []; @@ -58,7 +53,7 @@ export class FileUseWarning { } if(wpFound.length){ - edkStatusBar.setHelpUrl("https://github.com/intel/Edk2Code/wiki"); + edkStatusBar.setHelpUrl("https://intel.github.io/Edk2Code/"); edkStatusBar.setColor('statusBarItem.activeBackground'); edkStatusBar.setText(`${wpFound.join("|")}`); edkStatusBar.setToolTip(""); @@ -67,20 +62,19 @@ export class FileUseWarning { // If file is an H file, then check in cscope.files if(gCscope.includesFile(editor.document.uri)){ - edkStatusBar.setHelpUrl("https://github.com/intel/Edk2Code/wiki"); + edkStatusBar.setHelpUrl("https://intel.github.io/Edk2Code/"); edkStatusBar.setColor('statusBarItem.activeBackground'); edkStatusBar.setText(`(Included Path) ${path.basename(document.fileName)}`); edkStatusBar.setToolTip(""); return; } - edkStatusBar.setColor('statusBarItem.warningBackground'); - edkStatusBar.setHelpUrl("https://github.com/intel/Edk2Code/wiki/Functionality#status-bar"); + edkStatusBar.setHelpUrl("https://intel.github.io/Edk2Code/getting_started/#status-bar"); edkStatusBar.setText(`$(warning) ${path.basename(document.fileName)}`); edkStatusBar.setToolTip("This file is not used by any loaded workspace"); return; }else{ - edkStatusBar.setHelpUrl("https://github.com/intel/Edk2Code/wiki/Index-source-code"); + edkStatusBar.setHelpUrl("https://intel.github.io/Edk2Code/advance_features/#indexing-source-code"); edkStatusBar.setColor('statusBarItem.activeBackground'); edkStatusBar.setText(`Workspace not found`); edkStatusBar.setToolTip("No workspace is configured"); diff --git a/src/utils.ts b/src/utils.ts index d2bda4b..dfd71a5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,10 +4,8 @@ import * as fs from 'fs'; import * as vscode from 'vscode'; import { cwd } from "process"; import { gCompileCommands, gDebugLog, gEdkWorkspaces, gExtensionContext, gWorkspacePath } from "./extension"; -import { TreeDetailsDataProvider } from "./TreeDataProvider"; import { rejects } from "assert"; import { REGEX_PCD } from "./edkParser/commonParser"; -import { TreeItem } from "./treeElements/TreeItem"; var normalizeCache = new Map(); @@ -190,9 +188,6 @@ export function getRealPath(inputPath: string) { return ""; } let fullRealPath = fs.realpathSync.native(fullPath); - - // In case user is using subst. Replace workspace path - // fullRealPath = path.join(gWorkspacePath, fullRealPath.slice(fullRealPath.length - path.relative(gWorkspacePath, fullPath).length)); return fullRealPath; } @@ -282,22 +277,7 @@ export async function openTextDocument(uri: vscode.Uri) { -function _copyTreeProviderToClipboardRecursive(item: TreeItem, deep: number, result: any) { - result["content"] += `${" ".repeat(deep)}${item.toString()}\n`; - for (const nextItem of item.children) { - _copyTreeProviderToClipboardRecursive(nextItem, deep + 1, result); - } -} - -export async function copyTreeProviderToClipboard(treeProvider: TreeDetailsDataProvider) { - let result = { "content": "" }; - for (const items of treeProvider.getChildren()) { - _copyTreeProviderToClipboardRecursive(items, 0, result); - } - await copyToClipboard(result["content"]); - -} export async function copyToClipboard(data:string, message:string="Data copied to clipboard"){ await vscode.env.clipboard.writeText(data); @@ -309,9 +289,15 @@ export async function copyToClipboard(data:string, message:string="Data copied t export function isWorkspacePath(p: string) { - let x = gWorkspacePath; - const relativePath = path.relative(gWorkspacePath, p); - return !relativePath.includes("..") && relativePath !== p; + for (const folder of vscode.workspace.workspaceFolders!) { + const relativePath = path.relative(folder.uri.fsPath, p); + + if(!relativePath.includes("..") && relativePath !== p){ + return true; + } + } + return false; + } export function trimSpaces(text: string) { @@ -516,43 +502,58 @@ export async function listFiles(dir: string): Promise { } /** - * Recursively list all files in a directory. + * Recursively list all files in a directory using ripgrep. * * @param {string} dir - The directory to start listing files from. - * @returns {Promise} - An array of file paths. + * @returns {Promise} - An array of file paths relative to `dir`. */ export async function listFilesRecursive(dir: string): Promise { - const files = fs.readdirSync(dir); - let filelist: string[] = []; - let baseDir = ""; - - for (const file of files) { - const filepath = path.join(dir, file); - const stat = fs.statSync(filepath); + const pattern = new vscode.RelativePattern(dir, '**/*'); + const uris = await vscode.workspace.findFiles(pattern); + gDebugLog.debug(`listFilesRecursive: ${uris.length} files found in ${dir}`); + return uris.map(uri => path.relative(dir, uri.fsPath)); +} - if (stat.isDirectory()) { - filelist = await _walk(filepath, path.basename(filepath), filelist); - } else { - filelist.push(path.join(baseDir, file)); - } - } +export default function getCurrentVersion(): string { + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return packageJson.version; +} - return filelist; +export function getDocsUrl():string{ + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return packageJson.homepage; } -async function _walk(dir: string, baseDir: string, filelist: string[] = []) { - const files = fs.readdirSync(dir); - for (const file of files) { - const filepath = path.join(dir, file); - const stat = fs.statSync(filepath); +export function findClosestCommonDirectory(paths: string[]): string { + if (paths.length === 0){ + return ""; + } - if (stat.isDirectory()) { - filelist = await _walk(filepath, path.basename(filepath), filelist); - } else { - filelist.push(path.join(baseDir, file)); + let commonPath = paths[0]; + for (let i = 1; i < paths.length; i++) { + commonPath = getCommonPath(commonPath, paths[i]); + if (commonPath === ''){ + break; } } - return filelist; + return commonPath; } +export function getCommonPath(path1: string, path2: string): string { + const path1Parts = path1.split(path.sep); + const path2Parts = path2.split(path.sep); + const length = Math.min(path1Parts.length, path2Parts.length); + + let commonParts = []; + for (let i = 0; i < length; i++) { + if (path1Parts[i] === path2Parts[i]) { + commonParts.push(path1Parts[i]); + } else { + break; + } + } + return commonParts.join(path.sep); +} \ No newline at end of file diff --git a/src/workspaceTree/WorkspaceTreeProvider.ts b/src/workspaceTree/WorkspaceTreeProvider.ts new file mode 100644 index 0000000..4e90c56 --- /dev/null +++ b/src/workspaceTree/WorkspaceTreeProvider.ts @@ -0,0 +1,861 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { EdkWorkspace, IncludeNode, InfDsc } from '../index/edkWorkspace'; +import { getParser } from '../edkParser/parserFactory'; +import { EdkSymbol } from '../symbols/edkSymbols'; +import { Edk2SymbolType } from '../symbols/symbolsType'; +import { edkWorkspaceTreeView, gConfigAgent, gEdkWorkspaces } from '../extension'; +import { DiagnosticManager, EdkDiagnosticCodes } from '../diagnostics'; + +// ─── DSC symbol types available for filtering ───────────────────────────────── + +export const DSC_FILTER_TYPES: { type: Edk2SymbolType; label: string; description: string }[] = [ + { type: Edk2SymbolType.dscDefine, label: 'Defines', description: 'dscDefine' }, + { type: Edk2SymbolType.dscLibraryDefinition, label: 'Library classes', description: 'dscLibraryDefinition' }, + { type: Edk2SymbolType.dscModuleDefinition, label: 'Components', description: 'dscModuleDefinition' }, + { type: Edk2SymbolType.dscSection, label: 'Sections', description: 'dscSection' }, + { type: Edk2SymbolType.dscBuildOptionsSection, label: 'Build options', description: 'dscBuildOptionsSection' }, + { type: Edk2SymbolType.dscBuildOption, label: 'Build option entries', description: 'dscBuildOption' }, + { type: Edk2SymbolType.dscPcdDefinition, label: 'PCDs', description: 'dscPcdDefinition' }, + { type: Edk2SymbolType.dscInclude, label: 'Include directives', description: 'dscInclude' }, + { type: Edk2SymbolType.showInactiveNodes, label: 'Inactive elements', description: 'Show elements inside inactive !if/!else blocks' }, +]; + +// Structural / container types that are always visible in the tree regardless of +// the user's filter selection. These types exist only to group children and +// filtering them out would hide all their descendants. +const STRUCTURAL_TYPES = new Set([ + Edk2SymbolType.dscComponentSubSection, +]); + +/** Returns true when a symbol type should be shown in the tree. */ +function isTypeVisible(type: Edk2SymbolType, activeFilters: Set): boolean { + return activeFilters.has(type) || STRUCTURAL_TYPES.has(type); +} + +/** + * Returns true when the symbol's selection range falls inside any of the + * grayout (inactive conditional) ranges for its file. + */ +function isSymbolInactive(symbol: EdkSymbol, fileUri: vscode.Uri, workspace: EdkWorkspace | undefined): boolean { + if (!workspace) { return false; } + const grayoutRanges = workspace.getGrayoutRangeByUri(fileUri); + const symLine = symbol.selectionRange.start.line; + return grayoutRanges.some(r => symLine >= r.start.line && symLine <= r.end.line); +} + +/** + * If the symbol has a duplicateDefine or duplicateStatement diagnostic, + * returns the location of the definition that overwrites it (from relatedInformation). + */ +function getOverwriteLocation(symbol: EdkSymbol, fileUri: vscode.Uri): vscode.Location | undefined { + const diag = DiagnosticManager.findDiagnosticAt( + fileUri, + symbol.selectionRange.start.line, + [EdkDiagnosticCodes.duplicateDefine, EdkDiagnosticCodes.duplicateStatement] + ); + if (!diag?.relatedInformation?.length) { return undefined; } + return diag.relatedInformation[0].location; +} + +// ─── Helper: load symbols for a URI via the parser ─────────────────────────── + +async function loadSymbols(uri: vscode.Uri): Promise { + try { + const parser = await getParser(uri); + return (parser?.symbolsTree ?? []) as EdkSymbol[]; + } catch { + return []; + } +} + +// ─── Helper: collect all file URIs reachable in an include tree ─────────────── + +function collectIncludeUris(nodes: IncludeNode[], out: Set): void { + for (const node of nodes) { + out.add(node.uri.fsPath); + collectIncludeUris(node.children, out); + } +} + +/** + * Returns true when the given URI is the main DSC or any file included + * (directly or transitively) in at least one workspace of the provided list. + */ +export function isFileInWorkspaceTree(uri: vscode.Uri, workspaces: EdkWorkspace[]): boolean { + for (const ws of workspaces) { + if (ws.mainDsc.fsPath === uri.fsPath) { return true; } + const uris = new Set(); + collectIncludeUris(ws.includeTree, uris); + if (uris.has(uri.fsPath)) { return true; } + } + return false; +} + +/** + * Returns true when the given INF URI is referenced as a module or library + * in at least one loaded workspace. + * Uses the same path-suffix check that EdkWorkspace.isFileInUse() performs. + */ +export function isInfInWorkspaces(uri: vscode.Uri, workspaces: EdkWorkspace[]): boolean { + for (const ws of workspaces) { + for (const mod of ws.filesModules) { + if (uri.fsPath.includes(mod.path)) { return true; } + } + for (const lib of ws.filesLibraries) { + if (uri.fsPath.includes(lib.path)) { return true; } + } + } + return false; +} + +// ─── Tree Item: Workspace root (the main DSC) ──────────────────────────────── + +export class WorkspaceRootItem extends vscode.TreeItem { + public readonly treePath: string[]; + public readonly nodePath: string; + + constructor(public readonly workspace: EdkWorkspace, public readonly wsIndex: number) { + const label = path.basename(workspace.mainDsc.fsPath); + super(label, vscode.TreeItemCollapsibleState.Expanded); + this.id = `wsr:${wsIndex}`; + this.treePath = [label]; + this.nodePath = label; + this.description = workspace.platformName ?? ''; + this.tooltip = new vscode.MarkdownString( + `**${label}**\n\n${this.description}\n\n\`${this.nodePath}\`` + ); + this.iconPath = new vscode.ThemeIcon('file-code'); + this.contextValue = 'workspaceRoot'; + this.command = { + command: 'vscode.open', + title: 'Open DSC', + arguments: [workspace.mainDsc] + }; + } +} + +// ─── Tree Item: An !include node inside the tree ────────────────────────────── + +export class IncludeTreeItem extends vscode.TreeItem { + public readonly treePath: string[]; + public readonly nodePath: string; + public readonly inactive: boolean; + + constructor(public readonly node: IncludeNode, parentPath: string[], parentNodePath: string, inactive: boolean = false) { + const label = path.basename(node.uri.fsPath); + super(label, vscode.TreeItemCollapsibleState.Collapsed); + this.inactive = inactive; + this.treePath = [...parentPath, vscode.workspace.asRelativePath(node.uri, false)]; + this.nodePath = parentNodePath + '/' + label; + this.description = inactive + ? `${vscode.workspace.asRelativePath(node.uri, false)} (inactive)` + : vscode.workspace.asRelativePath(node.uri, false); + this.tooltip = new vscode.MarkdownString( + `**${label}**\n\n${this.description}\n\n${this.nodePath}` + ); + this.iconPath = inactive + ? new vscode.ThemeIcon('file', new vscode.ThemeColor('disabledForeground')) + : new vscode.ThemeIcon('file'); + this.contextValue = inactive ? 'includeNodeInactive' : 'includeNode'; + // Clicking jumps to the !include directive in the parent file + this.command = { + command: 'edk2code.gotoFile', + title: 'Go to !include directive', + arguments: [node.location.uri, node.location.range] + }; + } +} + +// ─── Tree Item: A document symbol (outline entry) inside a file ────────────── + +export class DocumentSymbolItem extends vscode.TreeItem { + public readonly symbolType: Edk2SymbolType; + public readonly treePath: string[]; + public readonly nodePath: string; + public readonly inactive: boolean; + public readonly overwrittenBy: vscode.Location | undefined; + + constructor( + public readonly symbol: EdkSymbol, + public readonly fileUri: vscode.Uri, + activeFilters: Set, + parentPath: string[], + public readonly parent: WorkspaceRootItem | DocumentSymbolItem | undefined, + parentNodePath: string, + inactive: boolean = false, + overwrittenBy?: vscode.Location + ) { + const visibleChildren = symbol.children.filter( + c => isTypeVisible((c as EdkSymbol).type, activeFilters) + ); + const isDscInclude = symbol.type === Edk2SymbolType.dscInclude; + super( + symbol.name, + visibleChildren.length > 0 || isDscInclude + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ); + this.inactive = inactive; + this.overwrittenBy = overwrittenBy; + this.treePath = [...parentPath, symbol.name]; + this.nodePath = parentNodePath + ' / ' + symbol.name; + // Include the parent path in the id so the same file/symbol included from + // multiple places in the tree gets a unique id for each occurrence. + const parentKey = parentPath.join('/'); + this.id = `dsi:${parentKey}:${fileUri.fsPath}:${symbol.selectionRange.start.line}:${symbol.selectionRange.start.character}`; + this.symbolType = symbol.type; + + // Build description and context based on state + const isOverwritten = !!overwrittenBy; + const isBuildable = (symbol.type === Edk2SymbolType.dscModuleDefinition); + let desc = symbol.detail || ''; + let ctx = 'symbolNode'; + if (inactive && isOverwritten) { + desc = `${this.label} (inactive, overwritten) ${desc}`.trim(); + ctx = 'symbolNodeInactiveOverwritten'; + } else if (inactive) { + desc = `${this.label} (inactive) ${desc}`.trim(); + ctx = 'symbolNodeInactive'; + } else if (isOverwritten) { + desc = `${this.label} (overwritten) ${desc}`.trim(); + ctx = 'symbolNodeOverwritten'; + } else if (isBuildable) { + ctx = 'symbolNodeBuildable'; + } + + if(ctx !== 'symbolNode' && ctx !== 'symbolNodeBuildable') { + // The label is shown with strikethrough in the tree when overwritten, so we move the original label to the description and show the overwrite status in the label instead. This keeps the label text fully visible without truncation, while still indicating the symbol's name and status. + this.label = ""; + } + + this.description = desc || undefined; + + this.tooltip = new vscode.MarkdownString( + `**${symbol.name}**\n\n${this.description ?? ''}\n\n${this.nodePath}` + ); + + if (inactive || isOverwritten) { + this.iconPath = new vscode.ThemeIcon( + EdkSymbol.iconForKind(symbol.kind).id, + new vscode.ThemeColor('disabledForeground') + ); + } else { + this.iconPath = EdkSymbol.iconForKind(symbol.kind); + } + + this.contextValue = ctx; + // Clicking navigates to the symbol's location in its file + this.command = { + command: 'edk2code.gotoFile', + title: 'Go to symbol', + arguments: [fileUri, symbol.selectionRange] + }; + } +} + +export type WorkspaceTreeNode = WorkspaceRootItem | IncludeTreeItem | DocumentSymbolItem; + +// ─── Helper: find an IncludeNode by the location of its !include directive ─── + +function findIncludeNode(nodes: IncludeNode[], location: vscode.Location): IncludeNode | undefined { + for (const node of nodes) { + if (node.location.uri.fsPath === location.uri.fsPath && + node.location.range.start.line === location.range.start.line) { + return node; + } + const found = findIncludeNode(node.children, location); + if (found) { return found; } + } + return undefined; +} + +// ─── Recursive text serializer ─────────────────────────────────────────────── + +async function resolveIncludedUri(symbol: EdkSymbol): Promise { + if (symbol.type !== Edk2SymbolType.dscInclude || !symbol.onDefinition) { + return undefined; + } + const locations = await Promise.resolve(symbol.onDefinition(symbol.parser)) as vscode.Location[] | undefined; + return locations?.[0]?.uri; +} + +async function serializeFileSymbols( + fileUri: vscode.Uri, + indent: string, + filter: Set, + expandedFiles: Set +): Promise { + const symbols = await loadSymbols(fileUri); + let out = ''; + for (const sym of symbols) { + if (isTypeVisible(sym.type, filter)) { + out += await serializeSymbol(sym, indent, filter, expandedFiles); + } + } + return out; +} + +async function serializeSymbol( + symbol: EdkSymbol, + indent: string, + filter: Set, + expandedFiles: Set +): Promise { + const line = `${indent}${symbol.name}${symbol.detail ? ' - ' + symbol.detail : ''}\n`; + let out = line; + if (symbol.type === Edk2SymbolType.dscInclude) { + const includeUri = await resolveIncludedUri(symbol); + if (!includeUri || expandedFiles.has(includeUri.fsPath)) { + return out; + } + const nextExpandedFiles = new Set(expandedFiles); + nextExpandedFiles.add(includeUri.fsPath); + out += await serializeFileSymbols(includeUri, indent + ' ', filter, nextExpandedFiles); + return out; + } + for (const child of symbol.children) { + const edkChild = child as EdkSymbol; + if (isTypeVisible(edkChild.type, filter)) { + out += await serializeSymbol(edkChild, indent + ' ', filter, expandedFiles); + } + } + return out; +} + +// ─── Helper: find the deepest filtered symbol that contains a position ──────── + +function findDeepestSymbolAt( + symbols: EdkSymbol[], + position: vscode.Position, + filter: Set +): EdkSymbol | undefined { + let best: EdkSymbol | undefined; + for (const sym of symbols) { + if (!isTypeVisible(sym.type, filter)) { continue; } + const inRange = sym.range.contains(position) || sym.selectionRange.contains(position); + if (inRange) { + best = sym; + const deeper = findDeepestSymbolAt(sym.children as EdkSymbol[], position, filter); + if (deeper) { best = deeper; } + } + } + // Fallback: if no symbol contains the position, return the one closest by line + if (!best) { + let closestDist = Infinity; + for (const sym of symbols) { + if (!isTypeVisible(sym.type, filter)) { continue; } + const dist = Math.abs(sym.selectionRange.start.line - position.line); + if (dist < closestDist) { + closestDist = dist; + best = sym; + } + } + } + return best; +} + +// ─── Tree data provider ─────────────────────────────────────────────────────── + +export class WorkspaceTreeProvider implements vscode.TreeDataProvider, vscode.TreeDragAndDropController { + private _onDidChangeTreeData = + new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + // ─── Drag & Drop ────────────────────────────────────────────────────────── + readonly dropMimeTypes: string[] = ['application/vnd.code.tree.workspaceview']; + readonly dragMimeTypes: string[] = ['application/vnd.code.tree.workspaceview']; + + /** Stashed source info from the last drag operation. */ + private _draggedSource: { fileUri: vscode.Uri; range: vscode.Range } | undefined; + + handleDrag( + source: readonly WorkspaceTreeNode[], + _dataTransfer: vscode.DataTransfer, + _token: vscode.CancellationToken + ): void { + const item = source.find( + (s): s is DocumentSymbolItem => s instanceof DocumentSymbolItem + ); + if (!item) { + this._draggedSource = undefined; + return; + } + this._draggedSource = { fileUri: item.fileUri, range: item.symbol.range }; + } + + async handleDrop( + target: WorkspaceTreeNode | undefined, + _dataTransfer: vscode.DataTransfer, + _token: vscode.CancellationToken + ): Promise { + const source = this._draggedSource; + this._draggedSource = undefined; + if (!source || !target) { return; } + if (!(target instanceof DocumentSymbolItem)) { return; } + + const targetRange = target.symbol.range; + + // Skip dropping onto itself + if ( + source.fileUri.fsPath === target.fileUri.fsPath && + source.range.isEqual(targetRange) + ) { + return; + } + + const sourceDoc = await vscode.workspace.openTextDocument(source.fileUri); + + // Read full lines of the source symbol + const srcStart = source.range.start.line; + const srcEnd = source.range.end.line; + let textToMove = ''; + for (let i = srcStart; i <= srcEnd; i++) { + textToMove += sourceDoc.lineAt(i).text + '\n'; + } + + // Delete range: whole lines + const deleteRange = srcEnd + 1 < sourceDoc.lineCount + ? new vscode.Range(srcStart, 0, srcEnd + 1, 0) + : new vscode.Range( + srcStart === 0 ? 0 : srcStart - 1, + srcStart === 0 ? 0 : sourceDoc.lineAt(srcStart - 1).text.length, + srcEnd, + sourceDoc.lineAt(srcEnd).text.length + ); + + // Insert right after the target symbol's last line + const targetDoc = await vscode.workspace.openTextDocument(target.fileUri); + const tgtEnd = targetRange.end.line; + let insertPos: vscode.Position; + let insertText: string; + + if (tgtEnd + 1 < targetDoc.lineCount) { + insertPos = new vscode.Position(tgtEnd + 1, 0); + insertText = textToMove; + } else { + insertPos = new vscode.Position(tgtEnd, targetDoc.lineAt(tgtEnd).text.length); + insertText = '\n' + textToMove.replace(/\n$/, ''); + } + + const edit = new vscode.WorkspaceEdit(); + edit.delete(source.fileUri, deleteRange); + edit.insert(target.fileUri, insertPos, insertText); + await vscode.workspace.applyEdit(edit); + + // Refresh tree and reveal the target symbol + this.refresh(); + const tgtPosition = target.symbol.selectionRange.start; + await this.revealLocation(target.fileUri, tgtPosition, edkWorkspaceTreeView); + } + + private _activeIndex: number = 0; + + /** Which DSC symbol types are currently visible. Loaded from config; defaults to all. */ + private _activeFilters: Set = (() => { + const saved = gConfigAgent.getWorkspaceTreeFilters(); + if (saved && saved.length > 0) { + return new Set(saved as Edk2SymbolType[]); + } + return new Set(DSC_FILTER_TYPES.map(f => f.type)); + })(); + + /** + * When set, the tree only shows nodes whose `nodePath` is in this set. + * Used by the "Search workspace tree" command to display only the path + * leading to a selected node. `undefined` means no path filter is active. + */ + private _searchFilter: Set | undefined; + + get activeIndex(): number { + return this._activeIndex; + } + + get isSearchFilterActive(): boolean { + return this._searchFilter !== undefined; + } + + /** Filter the children list against the active search filter (if any). */ + private _applySearchFilter(items: T[]): T[] { + if (!this._searchFilter) { return items; } + const filter = this._searchFilter; + const filtered = items.filter(i => filter.has(i.nodePath)); + // Force-expand surviving nodes so the path to the target is visible. + for (const item of filtered) { + if (item.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) { + item.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; + } + } + return filtered; + } + + /** Clear the search-path filter and refresh the tree. */ + clearSearchFilter(): void { + if (!this._searchFilter) { return; } + this._searchFilter = undefined; + void vscode.commands.executeCommand('setContext', 'edk2code.workspaceTreeSearchActive', false); + this._onDidChangeTreeData.fire(); + } + + refresh(): void { + // Clamp index in case workspaces were removed + const max = Math.max(0, gEdkWorkspaces.workspaces.length - 1); + this._activeIndex = Math.min(this._activeIndex, max); + this._onDidChangeTreeData.fire(); + } + + /** + * Switch the displayed workspace and refresh the view. + */ + selectWorkspace(index: number): void { + this._activeIndex = index; + this._onDidChangeTreeData.fire(); + } + + /** + * Show a multi-select Quick Pick to toggle which DSC symbol types are shown. + */ + async showFilterPicker(): Promise { + type FilterPickItem = vscode.QuickPickItem & { type: Edk2SymbolType }; + const items: FilterPickItem[] = DSC_FILTER_TYPES.map(f => ({ + label: f.label, + description: f.description, + picked: this._activeFilters.has(f.type), + type: f.type + })); + + const picked = await vscode.window.showQuickPick(items, { + canPickMany: true, + placeHolder: 'Select symbol types to show', + title: 'EDK2: Filter workspace symbols' + }); + + // Cancelled → leave filter unchanged + if (picked === undefined) { return; } + + this._activeFilters = new Set(picked.map(p => p.type)); + gConfigAgent.setWorkspaceTreeFilters([...this._activeFilters]); + this._onDidChangeTreeData.fire(); + } + + /** + * Serialize the whole displayed workspace tree to an indented string. + */ + async serializeTree(): Promise { + const ws = gEdkWorkspaces.workspaces[this._activeIndex]; + if (!ws) { return ''; } + + const rootLabel = path.basename(ws.mainDsc.fsPath); + const rel = vscode.workspace.asRelativePath(ws.mainDsc, false); + let out = `${rootLabel} (${rel})\n`; + out += await serializeFileSymbols(ws.mainDsc, ' ', this._activeFilters, new Set([ws.mainDsc.fsPath])); + return out; + } + + getTreeItem(element: WorkspaceTreeNode): vscode.TreeItem { + return element; + } + + async getChildren(element?: WorkspaceTreeNode): Promise { + const workspaces = gEdkWorkspaces.workspaces; + if (workspaces.length === 0) { + return []; + } + + const ws = workspaces[this._activeIndex]; + + // Root level: the single WorkspaceRootItem for the active workspace + if (!element) { + if (!ws) { return []; } + return this._applySearchFilter([new WorkspaceRootItem(ws, this._activeIndex)]); + } + + const showInactive = this._activeFilters.has(Edk2SymbolType.showInactiveNodes); + + // Under the workspace root: symbols of the main DSC + if (element instanceof WorkspaceRootItem) { + const symbols = await loadSymbols(element.workspace.mainDsc); + return this._applySearchFilter(symbols + .filter(s => isTypeVisible(s.type, this._activeFilters)) + .map(s => new DocumentSymbolItem(s, element.workspace.mainDsc, this._activeFilters, element.treePath, element, element.nodePath, + isSymbolInactive(s, element.workspace.mainDsc, ws), + getOverwriteLocation(s, element.workspace.mainDsc))) + .filter(s => showInactive || !s.inactive)); + } + + // Under an include node: symbols of that file only + if (element instanceof IncludeTreeItem) { + const inactive = element.inactive; + const symbols = await loadSymbols(element.node.uri); + return this._applySearchFilter(symbols + .filter(s => isTypeVisible(s.type, this._activeFilters)) + .map(s => new DocumentSymbolItem(s, element.node.uri, this._activeFilters, element.treePath, undefined, element.nodePath, + inactive || isSymbolInactive(s, element.node.uri, ws), + getOverwriteLocation(s, element.node.uri))) + .filter(s => showInactive || !s.inactive)); + } + + // Under a symbol: for !include directives expand into the included file; + // for all other symbols expand their parsed children. + if (element instanceof DocumentSymbolItem) { + if (element.symbolType === Edk2SymbolType.dscInclude) { + if (ws) { + const node = findIncludeNode(ws.includeTree, element.symbol.location); + if (node) { + const symbols = await loadSymbols(node.uri); + return this._applySearchFilter(symbols + .filter(s => isTypeVisible(s.type, this._activeFilters)) + .map(s => new DocumentSymbolItem(s, node.uri, this._activeFilters, element.treePath, element, element.nodePath, + element.inactive || isSymbolInactive(s, node.uri, ws), + getOverwriteLocation(s, node.uri))) + .filter(s => showInactive || !s.inactive)); + } + } + } + return this._applySearchFilter((element.symbol.children as EdkSymbol[]) + .filter(c => isTypeVisible(c.type, this._activeFilters)) + .map(child => new DocumentSymbolItem(child, element.fileUri, this._activeFilters, element.treePath, element, element.nodePath, + element.inactive || isSymbolInactive(child, element.fileUri, ws), + getOverwriteLocation(child, element.fileUri))) + .filter(s => showInactive || !s.inactive)); + } + + return []; + } + + getParent(element: WorkspaceTreeNode): vscode.ProviderResult { + if (element instanceof WorkspaceRootItem) { return undefined; } + if (element instanceof DocumentSymbolItem) { return element.parent; } + return undefined; + } + + /** + * Reveal the tree node that best matches the symbol at the cursor in the active editor. + */ + async revealActiveEditor(treeView: vscode.TreeView): Promise { + const editor = vscode.window.activeTextEditor; + if (!editor) { + void vscode.window.showInformationMessage('No active editor.'); + return; + } + await this.revealLocation(editor.document.uri, editor.selection.active, treeView); + } + + /** + * Reveal the tree node that best matches the symbol at a specific location. + */ + async revealLocation( + uri: vscode.Uri, + position: vscode.Position, + treeView: vscode.TreeView + ): Promise { + // Load symbols for the file and find the deepest one at the position + const symbols = await loadSymbols(uri); + if (symbols.length === 0) { + void vscode.window.showInformationMessage('No EDK2 symbols found in the active file.'); + return; + } + + const target = findDeepestSymbolAt(symbols, position, this._activeFilters); + if (!target) { + void vscode.window.showInformationMessage('No EDK2 symbol found at the cursor position.'); + return; + } + + // Traverse the tree to find the matching DocumentSymbolItem + const rootItems = await this.getChildren(undefined); + for (const root of rootItems) { + const found = await this._findItemForSymbol(root, uri, target); + if (found) { + await treeView.reveal(found, { select: true, focus: false, expand: true }); + return; + } + } + + void vscode.window.showInformationMessage('Symbol not found in the workspace tree.'); + } + + /** + * Reveal an INF file's DSC declaration in the workspace tree. + */ + async revealInfInTree( + infUri: vscode.Uri, + treeView: vscode.TreeView + ): Promise { + const wps = await gEdkWorkspaces.getWorkspace(infUri); + let declarations: InfDsc[] = []; + for (const wp of wps) { + declarations = declarations.concat(await wp.getDscDeclaration(infUri)); + } + if (declarations.length) { + const decl = declarations[0]; + await this.revealLocation(decl.location.uri, decl.location.range.start, treeView); + } + } + + /** + * Open an input box where the user types a query. The workspace tree is + * filtered live so that only paths leading to nodes matching the query + * remain visible. A toggle button switches between case-insensitive + * substring matching and regular expression matching. The filter can + * later be cleared via `clearSearchFilter()`. + */ + async searchTree(treeView: vscode.TreeView): Promise { + type CollectedNode = { node: WorkspaceTreeNode; chain: string[]; haystack: string }; + + // Always start the search against the unfiltered tree. + const previousFilter = this._searchFilter; + this._searchFilter = undefined; + // Refresh the tree so the input-box-driven filter starts from the full tree. + if (previousFilter) { + this._onDidChangeTreeData.fire(); + } + + // Collect every node and its ancestor chain (inclusive of itself). + const allNodes: CollectedNode[] = []; + const collect = async (parent: WorkspaceTreeNode | undefined, chain: string[]): Promise => { + const children = await this.getChildren(parent); + for (const child of children) { + const label = child instanceof DocumentSymbolItem ? child.symbol.name + : child instanceof IncludeTreeItem ? path.basename(child.node.uri.fsPath) + : (child as WorkspaceRootItem).label as string; + const description = child.description as string | undefined; + const childChain = [...chain, child.nodePath]; + const haystack = `${label}\n${description ?? ''}`; + allNodes.push({ node: child, chain: childChain, haystack }); + await collect(child, childChain); + } + }; + + await vscode.window.withProgress( + { location: { viewId: 'workspaceView' } }, + async () => { await collect(undefined, []); } + ); + + if (allNodes.length === 0) { + this._searchFilter = previousFilter; + void vscode.window.showInformationMessage('No nodes in the workspace tree.'); + return; + } + + const input = vscode.window.createInputBox(); + input.title = 'EDK2: Search workspace tree'; + input.placeholder = 'Type to filter (case-insensitive). Toggle .* to use regex.'; + input.prompt = 'Tree updates live. Press Enter to keep the filter, Esc to cancel.'; + + const regexButtonOff: vscode.QuickInputButton = { + iconPath: new vscode.ThemeIcon('regex'), + tooltip: 'Use Regular Expression (off)' + }; + const regexButtonOn: vscode.QuickInputButton = { + iconPath: new vscode.ThemeIcon('regex'), + tooltip: 'Use Regular Expression (on)' + }; + let useRegex = false; + input.buttons = [regexButtonOff]; + + // Track whether the user accepted (Enter) so onDidHide knows whether to revert. + let accepted = false; + + const applyFilter = (value: string): void => { + if (!value) { + // Empty input → no filter while typing. + this._searchFilter = undefined; + input.validationMessage = undefined; + void vscode.commands.executeCommand('setContext', 'edk2code.workspaceTreeSearchActive', false); + this._onDidChangeTreeData.fire(); + return; + } + + let predicate: (s: string) => boolean; + if (useRegex) { + let rx: RegExp; + try { + rx = new RegExp(value, 'i'); + } catch (e) { + input.validationMessage = `Invalid regex: ${(e as Error).message}`; + return; + } + input.validationMessage = undefined; + predicate = (s) => rx.test(s); + } else { + input.validationMessage = undefined; + const needle = value.toLowerCase(); + predicate = (s) => s.toLowerCase().includes(needle); + } + + // Keep every nodePath that's part of an ancestor chain leading to a match. + const keep = new Set(); + for (const item of allNodes) { + if (predicate(item.haystack)) { + for (const p of item.chain) { keep.add(p); } + } + } + + this._searchFilter = keep.size > 0 ? keep : new Set(['__no_match__']); + void vscode.commands.executeCommand('setContext', 'edk2code.workspaceTreeSearchActive', true); + this._onDidChangeTreeData.fire(); + }; + + input.onDidChangeValue(applyFilter); + input.onDidTriggerButton(btn => { + if (btn === regexButtonOff || btn === regexButtonOn) { + useRegex = !useRegex; + input.buttons = [useRegex ? regexButtonOn : regexButtonOff]; + applyFilter(input.value); + } + }); + + await new Promise(resolve => { + input.onDidAccept(() => { + accepted = true; + input.hide(); + }); + input.onDidHide(() => { + resolve(); + }); + input.show(); + }); + input.dispose(); + + if (!accepted) { + // User cancelled (Esc) → restore the previous filter state. + this._searchFilter = previousFilter; + void vscode.commands.executeCommand( + 'setContext', + 'edk2code.workspaceTreeSearchActive', + this._searchFilter !== undefined + ); + this._onDidChangeTreeData.fire(); + return; + } + + // Accepted: keep whatever filter the live preview produced. + if (!this._searchFilter) { + // Empty query at acceptance → no filter active. + void vscode.commands.executeCommand('setContext', 'edk2code.workspaceTreeSearchActive', false); + } + } + + /** Recursively walk the tree to find a DocumentSymbolItem matching (fileUri, targetSymbol). */ + private async _findItemForSymbol( + parent: WorkspaceTreeNode, + fileUri: vscode.Uri, + targetSymbol: EdkSymbol + ): Promise { + const children = await this.getChildren(parent); + for (const child of children) { + if ( + child instanceof DocumentSymbolItem && + child.fileUri.fsPath === fileUri.fsPath && + child.symbol.selectionRange.start.line === targetSymbol.selectionRange.start.line && + child.symbol.selectionRange.start.character === targetSymbol.selectionRange.start.character + ) { + return child; + } + const found = await this._findItemForSymbol(child, fileUri, targetSymbol); + if (found) { return found; } + } + return undefined; + } +} diff --git a/static/buildForm.html b/static/buildForm.html new file mode 100644 index 0000000..3e4cddb --- /dev/null +++ b/static/buildForm.html @@ -0,0 +1,681 @@ + + + + + +EDK2 Build Configuration + + + + + +
+ + + + + +
+

+ EDK2 Build Configuration +

+

Configure build arguments, then click Build. Settings for individual modules are saved automatically.

+ +
+ ⚠️ Beta Feature: Build arguments were automatically calculated from your workspace configuration but may need manual adjustments. Please review before building. +
+ + +

Target

+
+ + + + + + + + + + + + + + + + + + + +
+ + +

Common Options

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

Flags

+
+ + + + + + + + + + + + + + +
+ + +

Report

+
+ + + + +
+
+ + +

Binary Cache

+
+ + + + + +
+ + +

Defines (-D)

+

Macro definitions passed as -D Name=Value.

+
+ + + +

PCDs (--pcd)

+

PCD overrides passed as --pcd=PcdName=Value.

+
+ + + +

Extra Arguments

+

Additional arguments passed verbatim to build.

+
+ + + +

Environment

+
+ +
+
+ + + +

Global Configuration

+

These settings are shared across all builds and saved globally in .edkCode/edk2_build_configuration.json.

+
+ + + + + + + + + + + + + + +
+ +
+ + +
+
+
+

Building...

+
+
+ + + + diff --git a/test/testAslParsing.asl b/test/testAslParsing.asl new file mode 100644 index 0000000..be251af --- /dev/null +++ b/test/testAslParsing.asl @@ -0,0 +1,60 @@ +// Corner-case ASL file for parser tests. + +DefinitionBlock ("test.aml", "DSDT", 2, "TEST", "TESTDSDT", 0x00000001) +{ + External (\_SB.PCI0, DeviceObj) + External (\_SB.PCI0.LPCB, DeviceObj) + + Scope (\_SB) + { + Name (TVAR, 0x1234) + + Device (TPM0) + { + Name (_HID, "MSFT0101") + Name (_STR, Unicode("TPM 2.0 Device")) + + OperationRegion (TPMR, SystemMemory, 0xFED40000, 0x5000) + Field (TPMR, AnyAcc, NoLock, Preserve) + { + ACC0, 8, + } + + Method (_STA, 0, Serialized) + { + Return (0x0F) + } + + Method (_CRS, 0, Serialized) + { + Name (RBUF, ResourceTemplate () + { + Memory32Fixed (ReadWrite, 0xFED40000, 0x5000) + }) + Return (RBUF) + } + } + + Device (EC0) + { + Name (_HID, "PNP0C09") + + OperationRegion (ECOR, EmbeddedControl, 0x00, 0xFF) + Field (ECOR, ByteAcc, Lock, Preserve) + { + TEMP, 8, + FAN0, 8, + } + + Method (RFAN, 0, NotSerialized) + { + Return (FAN0) + } + } + } + + Scope (\_SB.PCI0) + { + Name (PVAR, "PCI") + } +} diff --git a/test/testDecParsing.dec b/test/testDecParsing.dec new file mode 100644 index 0000000..d557810 --- /dev/null +++ b/test/testDecParsing.dec @@ -0,0 +1,36 @@ +## Corner-case DEC file for parser tests. + +[Defines] + DEC_SPECIFICATION = 0x00010017 + PACKAGE_NAME = TestPkg + PACKAGE_GUID = 11223344-5566-7788-99AA-BBCCDDEEFF00 + PACKAGE_VERSION = 1.0 + +[Includes] + Include + Include/Library + +[Includes.X64] + Include/X64 + +[LibraryClasses] + BaseLib|Include/Library/BaseLib.h + DebugLib|Include/Library/DebugLib.h + +[Guids] + gTestTokenSpaceGuid = { 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 } } + +[Protocols] + gTestProtocolGuid = { 0xAABBCCDD, 0x1122, 0x3344, { 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC } } + gTestProtocol2Guid = { 0x11111111, 0x2222, 0x3333, { 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB } } + +[Ppis] + gTestPpiGuid = { 0xDDDDDDDD, 0xEEEE, 0xFFFF, { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 } } + +[PcdsFixedAtBuild] + gTestTokenSpaceGuid.PcdTestFixed|0x00000001|UINT32|0x00000001 + +[PcdsPatchableInModule] + gTestTokenSpaceGuid.PcdTestPatchable|0x00|UINT8|0x00000002 + +# End of DEC diff --git a/test/testDscOperations.dsc b/test/testDscOperations.dsc new file mode 100644 index 0000000..e2a602a --- /dev/null +++ b/test/testDscOperations.dsc @@ -0,0 +1,138 @@ +[Defines] + PLATFORM_NAME = TestOperations + PLATFORM_GUID = 12345678-1234-1234-1234-123456789abc + PLATFORM_VERSION = 1.0 + DSC_SPECIFICATION = 0x00010017 + + DEFINE VAR1 = TRUE + DEFINE VAR2 = TRUE + DEFINE VAR3 = FALSE + DEFINE VAR4 = TRUE + DEFINE VAR5 = 5 + DEFINE VAR6 = 10 + DEFINE VAR7 = 15 + DEFINE VAR8 = 10 + DEFINE VAR9 = 5 + DEFINE VAR10 = 5 + DEFINE VAR11 = 10 + DEFINE VAR12 = 10 + DEFINE VAR13 = 3 + DEFINE VAR14 = 2 + DEFINE VAR15 = 5 + DEFINE VAR16 = 0 + DEFINE VAR17 = 6 + DEFINE VAR18 = 1 + DEFINE VAR19 = 6 + DEFINE VAR20 = 1 + DEFINE VAR21 = 10 + DEFINE VAR22 = 2 + DEFINE VAR23 = 0xFF + DEFINE VAR24 = 0x00 + DEFINE VAR25 = 2 + DEFINE VAR26 = 4 + DEFINE VAR27 = 1 + DEFINE VAR29 = TRUE + DEFINE VAR30 = FALSE + DEFINE VAR31 = TRUE + DEFINE VAR32 = "TEST" + DEFINE VAR33 = TEST + + +[Components] +!if $(VAR33) == TEST + # !error "VAR33 is equal to TEST" +!else + !error "VAR33 is not equal to TEST" +!endif + +!if $(VAR33) == "TEST" + # !error "VAR33 is equal to string TEST" +!else + !error "VAR33 is not equal to string TEST" +!endif + +!if $(VAR32) == TEST + # !error "VAR32 is equal to TEST" +!else + !error "VAR32 is not equal to TEST" +!endif + +!if $(VAR32) == "TEST" + # !error "VAR32 is equal to string TEST" +!else + !error "VAR32 is not equal to string TEST" +!endif + +!if TEST == "TEST" + # !error "Literal TEST is equal to string TEST" +!else + !error "Literal TEST is not equal to string TEST" +!endif + +!if "2" in "2 3 4 5" + # !error "String '2' is found in '2 3 4 5'" +!else + !error "String '2' is not found in '2 3 4 5'" +!endif + +!if FALSE ^ 5 + # !error "Bitwise XOR between FALSE and 5 is non-zero" +!else + !error "Bitwise XOR between FALSE and 5 is zero" +!endif + +!if $(VAR13) + $(VAR14) == $(VAR15) - $(VAR16) + # !error "Addition and subtraction result in equality" +!else + !error "Addition and subtraction do not result in equality" +!endif + +!if $(UNDEFINED_VAR) == FALSE + # !error "UNDEFINED_VAR is equal to FALSE" +!else + !error "UNDEFINED_VAR is not equal to FALSE" +!endif + +!if $(VAR1) == $(VAR2) && $(VAR3) != $(VAR4) + # !error "VAR1 equals VAR2 and VAR3 does not equal VAR4" +!else + !error "VAR1 does not equal VAR2 or VAR3 equals VAR4" +!endif + +!if $(VAR5) < $(VAR6) || $(VAR7) > $(VAR8) + # !error "VAR5 is less than VAR6 or VAR7 is greater than VAR8" +!else + !error "VAR5 is not less than VAR6 and VAR7 is not greater than VAR8" +!endif + +!if $(VAR9) <= $(VAR10) && $(VAR11) >= $(VAR12) + # !error "VAR9 is less than or equal to VAR10 and VAR11 is greater than or equal to VAR12" +!else + !error "VAR9 is not less than or equal to VAR10 or VAR11 is not greater than or equal to VAR12" +!endif + +!if $(VAR17) * $(VAR18) == $(VAR19) / $(VAR20) + # !error "Multiplication and division result in equality" +!else + !error "Multiplication and division do not result in equality" +!endif + +!if $(VAR21) % $(VAR22) == 0 + # !error "VAR21 is divisible by VAR22" +!else + !error "VAR21 is not divisible by VAR22" +!endif + +!if ~$(VAR23) == $(VAR24) + !error "Bitwise NOT of VAR23 equals VAR24" +!else + # !error "Bitwise NOT of VAR23 does not equal VAR24" +!endif + +!if $(VAR25) << 1 == $(VAR26) >> 1 + !error "Left shift of VAR25 equals right shift of VAR26" +!else + # !error "Left shift of VAR25 does not equal right shift of VAR26" +!endif + +!error "ALL Test Passed" \ No newline at end of file diff --git a/test/testDscParsing.dsc b/test/testDscParsing.dsc new file mode 100644 index 0000000..b3139dc --- /dev/null +++ b/test/testDscParsing.dsc @@ -0,0 +1,63 @@ +## Corner-case DSC file used by the parser unit tests. +## Every section exercises a different parsing path. + +[Defines] + PLATFORM_NAME = TestParsing + PLATFORM_GUID = AABBCCDD-1122-3344-5566-778899AABBCC + PLATFORM_VERSION = 0.1 + DSC_SPECIFICATION = 0x00010017 + OUTPUT_DIRECTORY = Build/TestParsing + SUPPORTED_ARCHITECTURES = IA32|X64|AARCH64 + BUILD_TARGETS = DEBUG|RELEASE + DEFINE MY_VAR = SomeValue + DEFINE EMPTY_VAR = + +# Root-level define (outside any section) +DEFINE ROOT_DEFINE = OutsideSection + +[SkuIds] + 0|DEFAULT + +[LibraryClasses] + BaseLib | MdePkg/Library/BaseLib/BaseLib.inf + DebugLib|MdePkg/Library/DebugLib/DebugLib.inf + # Library with spaces around pipe + PrintLib | MdePkg/Library/PrintLib/PrintLib.inf + +[LibraryClasses.X64] + TimerLib|MdePkg/Library/TimerLib/TimerLib.inf + +[Components] + MdePkg/Test/SimpleModule.inf + MdePkg/Test/AnotherModule.inf + +[Components.X64] + # Module with sub-sections + MdePkg/Test/ComplexModule.inf { + + BaseLib | MdePkg/Library/OverrideLib/Override.inf + + gTokenSpace.PcdFoo|0x1 + + MSFT:*_*_*_CC_FLAGS = /DTEST + } + + # Module without braces + MdePkg/Test/PlainModule.inf + +[PcdsFixedAtBuild] + gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x2F + gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000040 + +[PcdsDynamicDefault] + gEfiMdePkgTokenSpaceGuid.PcdPlatformBootTimeOut|5 + +[BuildOptions] + GCC:*_*_*_CC_FLAGS = -DMDEPKG_NDEBUG + MSFT:*_*_*_CC_FLAGS = /DMDEPKG_NDEBUG + # Comment inside build options + +!include TestInclude.dsc.inc + +/* Block comment test */ +# End of file diff --git a/test/testDscProcess.dsc b/test/testDscProcess.dsc new file mode 100644 index 0000000..b9b7926 --- /dev/null +++ b/test/testDscProcess.dsc @@ -0,0 +1,85 @@ +[Defines] + PLATFORM_NAME = TestProcess + PLATFORM_GUID = 11111111-2222-3333-4444-555555555555 + PLATFORM_VERSION = 2.0 + DSC_SPECIFICATION = 0x00010017 + DEFINE MY_FLAG = TRUE + DEFINE MY_PATH = SomePath + DEFINE EMPTY_DEF = + +# Root-level define +DEFINE ROOT_DEF = RootValue + +# Test that comments are stripped +# This line is a comment and should not appear + +[SkuIds] + 0|DEFAULT + +[LibraryClasses] + BaseLib|MdePkg/Library/BaseLib/BaseLib.inf + DebugLib|MdePkg/Library/DebugLib/DebugLib.inf + +[LibraryClasses.X64] + TimerLib|MdePkg/Library/TimerLib/TimerLib.inf + +[Components] + MdePkg/Test/ModuleA.inf + MdePkg/Test/ModuleB.inf + +[Components.X64] + MdePkg/Test/ModuleC.inf + +[PcdsFixedAtBuild] + gTestPkg.PcdTestMask|0x2F + gTestPkg.PcdTestLevel|0x80000040 + +[PcdsDynamicDefault] + gTestPkg.PcdBootTimeout|5 + +[BuildOptions] + GCC:*_*_*_CC_FLAGS = -DTEST + MSFT:*_*_*_CC_FLAGS = /DTEST + +# Conditional block test: TRUE branch taken +DEFINE COND_A = BeforeIf +!if TRUE + DEFINE COND_TAKEN = IfTrueValue +!else + DEFINE COND_TAKEN = IfFalseValue +!endif + +# Conditional block test: FALSE branch -> else taken +!if FALSE + DEFINE COND_FALSE_IF = ShouldNotExist +!else + DEFINE COND_ELSE = ElseValue +!endif + +# Nested conditional +!if TRUE + DEFINE OUTER_TRUE = OuterOk + !if FALSE + DEFINE INNER_FALSE = ShouldNotExist + !else + DEFINE INNER_ELSE = InnerElseOk + !endif +!endif + +# !ifdef test +DEFINE EXISTING_VAR = Exists +!ifdef EXISTING_VAR + DEFINE IFDEF_TAKEN = yes +!endif + +# !ifndef test on undefined variable +!ifndef TOTALLY_UNDEFINED_XYZ + DEFINE IFNDEF_TAKEN = yes +!endif + +# Define with variable reference +DEFINE BASE = Hello +DEFINE DERIVED = $(BASE)World + +# PCD in string value + gTestPkg.PcdStringVal|L"TestString" diff --git a/test/testFdfParsing.fdf b/test/testFdfParsing.fdf new file mode 100644 index 0000000..a846a5a --- /dev/null +++ b/test/testFdfParsing.fdf @@ -0,0 +1,38 @@ +## Corner-case FDF file for parser tests. + +[FD.TestFd] + BaseAddress = 0xFF000000 + Size = 0x01000000 + ErasePolarity = 1 + BlockSize = 0x00010000 + NumBlocks = 0x100 + + DEFINE FD_VAR = SomeValue + +[FV.FVMAIN] + FvAlignment = 16 + ERASE_POLARITY = 1 + MEMORY_MAPPED = TRUE + + INF MdePkg/Test/SimpleModule.inf + INF MdePkg/Test/AnotherModule.inf + + DEFINE FV_VAR = AnotherValue + +[FV.FVRECOVERY] + FvAlignment = 16 + + APRIORI DXE { + INF MdePkg/Test/AprioriModule.inf + } + + INF MdePkg/Test/RecoveryModule.inf + +[Rule.Common.SEC] + FILE SEC = $(NAMED_GUID) { + PE32 PE32 $(INF_OUTPUT)/$(MODULE_NAME).efi + } + +!include TestFdfInclude.fdf.inc + +# End of FDF diff --git a/test/testInfParsing.inf b/test/testInfParsing.inf new file mode 100644 index 0000000..ba2c3e5 --- /dev/null +++ b/test/testInfParsing.inf @@ -0,0 +1,48 @@ +## Corner-case INF file for parser tests. + +[Defines] + INF_VERSION = 0x00010017 + BASE_NAME = TestModule + MODULE_TYPE = DXE_DRIVER + ENTRY_POINT = TestEntryPoint + LIBRARY_CLASS = TestLib + CONSTRUCTOR = TestConstructor + DESTRUCTOR = TestDestructor + DEFINE MY_FLAG = TRUE + +[Sources] + TestModule.c + TestModule.h + Subfolder/Helper.c + +[Sources.X64] + X64/Arch.c + +[Packages] + MdePkg/MdePkg.dec + MdeModulePkg/MdeModulePkg.dec + +[LibraryClasses] + BaseLib + DebugLib + UefiBootServicesTableLib + +[Protocols] + gEfiSimpleTextOutProtocolGuid + gEfiDevicePathProtocolGuid + +[Ppis] + gEfiPeiMemoryDiscoveredPpiGuid + +[Guids] + gEfiGlobalVariableGuid + gEfiHobListGuid + +[FixedPcd] + gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask + +[Depex] + gEfiVariableArchProtocolGuid AND + gEfiVariableWriteArchProtocolGuid + +# End of INF diff --git a/test/testVfrParsing.vfr b/test/testVfrParsing.vfr new file mode 100644 index 0000000..11ee5e8 --- /dev/null +++ b/test/testVfrParsing.vfr @@ -0,0 +1,78 @@ +// Corner-case VFR file for parser tests. + +#include "TestVfrStrDefs.h" + +formset + guid = TEST_FORMSET_GUID, + title = STRING_TOKEN(STR_FORM_SET_TITLE), + help = STRING_TOKEN(STR_FORM_SET_HELP), + + form formid = 1, + title = STRING_TOKEN(STR_FORM1_TITLE); + + oneof + varid = TestData.Option1, + prompt = STRING_TOKEN(STR_OPTION1_PROMPT), + help = STRING_TOKEN(STR_OPTION1_HELP), + option text = STRING_TOKEN(STR_DISABLED), value = 0, flags = DEFAULT; + option text = STRING_TOKEN(STR_ENABLED), value = 1, flags = 0; + endoneof; + + checkbox + varid = TestData.Flag1, + prompt = STRING_TOKEN(STR_FLAG1_PROMPT), + help = STRING_TOKEN(STR_FLAG1_HELP), + flags = CHECKBOX_DEFAULT, + endcheckbox; + + numeric + varid = TestData.Value1, + prompt = STRING_TOKEN(STR_VALUE1_PROMPT), + help = STRING_TOKEN(STR_VALUE1_HELP), + minimum = 0, + maximum = 255, + step = 1, + default = 100, + endnumeric; + + string + varid = TestData.Name1, + prompt = STRING_TOKEN(STR_NAME1_PROMPT), + help = STRING_TOKEN(STR_NAME1_HELP), + minsize = 0, + maxsize = 64, + endstring; + + password + varid = TestData.Pass1, + prompt = STRING_TOKEN(STR_PASS1_PROMPT), + help = STRING_TOKEN(STR_PASS1_HELP), + minsize = 6, + maxsize = 20, + endpassword; + + goto 2, + prompt = STRING_TOKEN(STR_GOTO_FORM2), + help = STRING_TOKEN(STR_GOTO_FORM2_HELP); + + endform; + + form formid = 2, + title = STRING_TOKEN(STR_FORM2_TITLE); + + oneof + varid = TestData.Option2, + prompt = STRING_TOKEN(STR_OPTION2_PROMPT), + help = STRING_TOKEN(STR_OPTION2_HELP), + option text = STRING_TOKEN(STR_LOW), value = 0, flags = DEFAULT; + option text = STRING_TOKEN(STR_MEDIUM), value = 1, flags = 0; + option text = STRING_TOKEN(STR_HIGH), value = 2, flags = 0; + endoneof; + + goto 1, + prompt = STRING_TOKEN(STR_GOTO_FORM1), + help = STRING_TOKEN(STR_GOTO_FORM1_HELP); + + endform; + +endformset; diff --git a/tsconfig.json b/tsconfig.json index 892bd18..b584063 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,12 +12,17 @@ "allowJs": true, "rootDir": "src", "strict": true /* enable all strict type-checking options */, - + "skipLibCheck": true, /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + }, + "exclude": [ + "scripts/**/*", + "out/**/*", + "node_modules/**/*" + ] }