From 3924803138366c09bb2d90d7211ecea3b8298395 Mon Sep 17 00:00:00 2001 From: eyza Date: Sat, 21 Sep 2024 12:34:30 +0200 Subject: [PATCH] Fixed unnecessary file parsing, improved exceptions handling, logging data into 'Output' tab, internal notifying of GscFiles, internal storage of diagnostics for files --- src/Gsc.ts | 25 ++- src/GscCodeActionProvider.ts | 138 ++++++++----- src/GscCompletionItemProvider.ts | 19 +- src/GscConfig.ts | 54 +++++ src/GscDefinitionProvider.ts | 14 +- src/GscDiagnosticsCollection.ts | 185 +++++++++--------- src/GscFiles.ts | 151 +++++++------- src/GscHoverProvider.ts | 30 ++- src/GscSemanticTokensProvider.ts | 15 +- src/GscStatusBar.ts | 15 +- src/Issues.ts | 118 +++++------ src/LoggerOutput.ts | 85 +++++++- src/test/Tests.test.ts | 59 ++---- .../GscCompletionItemProvider.test.ts | 20 +- src/test/workspace/GscComplex.test.ts | 4 +- .../GscDiagnosticsCollection.test.ts | 112 +++++------ src/test/workspace/GscQuickFix.test.ts | 65 +++--- 17 files changed, 657 insertions(+), 452 deletions(-) diff --git a/src/Gsc.ts b/src/Gsc.ts index a4fd1bb..08f2a7c 100644 --- a/src/Gsc.ts +++ b/src/Gsc.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { GscFiles } from './GscFiles'; -import { GscCompletionItemProvider } from './GscCompletionItemProvider'; +import { GscCompletionItemProvider } from './GscCompletionItemProvider'; import { GscSemanticTokensProvider } from './GscSemanticTokensProvider'; import { GscDiagnosticsCollection } from './GscDiagnosticsCollection'; import { GscDefinitionProvider } from './GscDefinitionProvider'; @@ -8,6 +8,7 @@ import { GscHoverProvider } from './GscHoverProvider'; import { GscStatusBar } from './GscStatusBar'; import { GscConfig } from './GscConfig'; import { GscCodeActionProvider } from './GscCodeActionProvider'; +import { Issues } from './Issues'; export class Gsc { @@ -19,15 +20,19 @@ export class Gsc { console.log("------------------------------------------------------------"); // Register events - await GscConfig.activate(context); - await GscStatusBar.activate(context); - await GscFiles.activate(context); - await GscDiagnosticsCollection.activate(context); - await GscCodeActionProvider.activate(context); - await GscSemanticTokensProvider.activate(context); - await GscCompletionItemProvider.activate(context); - await GscDefinitionProvider.activate(context); - await GscHoverProvider.activate(context); + try { + await GscConfig.activate(context); + await GscStatusBar.activate(context); + await GscFiles.activate(context); + await GscDiagnosticsCollection.activate(context); + await GscCodeActionProvider.activate(context); + await GscSemanticTokensProvider.activate(context); + await GscCompletionItemProvider.activate(context); + await GscDefinitionProvider.activate(context); + await GscHoverProvider.activate(context); + } catch (error) { + Issues.handleError(error); + } } static deactivate() { diff --git a/src/GscCodeActionProvider.ts b/src/GscCodeActionProvider.ts index 28c6276..fa51c31 100644 --- a/src/GscCodeActionProvider.ts +++ b/src/GscCodeActionProvider.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { ConfigErrorDiagnostics, GscConfig } from './GscConfig'; import * as path from 'path'; +import { Issues } from './Issues'; export class GscCodeActionProvider implements vscode.CodeActionProvider { @@ -10,84 +11,117 @@ export class GscCodeActionProvider implements vscode.CodeActionProvider { }); context.subscriptions.push(vscode.commands.registerCommand('gsc.addFolderForReferences', async (workspaceUri: vscode.Uri) => { - // Prompt the user to select a folder - const folderUri = await vscode.window.showOpenDialog({ - canSelectFolders: true, - canSelectFiles: false, - canSelectMany: false, - openLabel: 'Add selected folder to Workspace' - }); - - if (folderUri && folderUri[0]) { - const uri = folderUri[0]; - - // Get folder name from the uri - const folderName = path.basename(uri.fsPath); - await GscConfig.addIncludedWorkspaceFolders(workspaceUri, folderName); - - // Add the selected folder to the workspace - vscode.workspace.updateWorkspaceFolders( - 0, //vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, - null, - { uri } - ); - - void vscode.window.showInformationMessage(`Added folder to workspace: ${uri.fsPath}`); + try { + // Prompt the user to select a folder + const folderUri = await vscode.window.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + openLabel: 'Add selected folder to Workspace' + }); + + if (folderUri && folderUri[0]) { + const uri = folderUri[0]; + + // Get folder name from the uri + const folderName = path.basename(uri.fsPath); + await GscConfig.addIncludedWorkspaceFolders(workspaceUri, folderName); + + // Add the selected folder to the workspace + vscode.workspace.updateWorkspaceFolders( + 0, //vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, + null, + { uri } + ); + + void vscode.window.showInformationMessage(`Added folder to workspace: ${uri.fsPath}`); + } + } catch (error) { + Issues.handleError(error); } })); context.subscriptions.push(vscode.commands.registerCommand('gsc.disableErrorDiagnostics', async (workspaceUri: vscode.Uri, workspaceFolderName: string) => { - await GscConfig.updateErrorDiagnostics(workspaceUri, ConfigErrorDiagnostics.Disable); - void vscode.window.showInformationMessage(`Disabled error diagnostic for workspace folder '${workspaceFolderName}'`); + try { + await GscConfig.updateErrorDiagnostics(workspaceUri, ConfigErrorDiagnostics.Disable); + void vscode.window.showInformationMessage(`Disabled error diagnostic for workspace folder '${workspaceFolderName}'`); + } catch (error) { + Issues.handleError(error); + } })); context.subscriptions.push(vscode.commands.registerCommand('gsc.addFunctionNameIntoIgnored', async (workspaceUri: vscode.Uri, functionName: string) => { - await GscConfig.addIgnoredFunctionName(workspaceUri, functionName); - void vscode.window.showInformationMessage(`Added '${functionName}' to ignored function names.`); + try { + await GscConfig.addIgnoredFunctionName(workspaceUri, functionName); + void vscode.window.showInformationMessage(`Added '${functionName}' to ignored function names.`); + } catch (error) { + Issues.handleError(error); + } })); context.subscriptions.push(vscode.commands.registerCommand('gsc.addFilePathIntoIgnored', async (workspaceUri: vscode.Uri, filePath: string) => { - await GscConfig.addIgnoredFilePath(workspaceUri, filePath); - void vscode.window.showInformationMessage(`Added '${filePath}' to ignored file paths.`); + try { + await GscConfig.addIgnoredFilePath(workspaceUri, filePath); + void vscode.window.showInformationMessage(`Added '${filePath}' to ignored file paths.`); + } catch (error) { + Issues.handleError(error); + } })); context.subscriptions.push(vscode.commands.registerCommand('gsc.addAllMissingFilePathsIntoIgnored', async (workspaceUri: vscode.Uri) => { - const allDiagnostics = vscode.languages.getDiagnostics(); - const paths = new Set(); - for (const [uri, diagnostics] of allDiagnostics) { - for (const diagnostic of diagnostics) { - if (typeof diagnostic.code !== "string") { - continue; - } - const code = diagnostic.code.toString(); - if (code.startsWith("unknown_file_path_")) { - const filePath = diagnostic.code.substring("unknown_file_path_".length); - paths.add(filePath); + try { + const allDiagnostics = vscode.languages.getDiagnostics(); + const paths = new Set(); + for (const [uri, diagnostics] of allDiagnostics) { + for (const diagnostic of diagnostics) { + if (typeof diagnostic.code !== "string") { + continue; + } + const code = diagnostic.code.toString(); + if (code.startsWith("unknown_file_path_")) { + const filePath = diagnostic.code.substring("unknown_file_path_".length); + paths.add(filePath); + } } - } - }; - await GscConfig.addIgnoredFilePath(workspaceUri, [...paths]); - void vscode.window.showInformationMessage(`Added all missing files to ignored file paths.`); + }; + await GscConfig.addIgnoredFilePath(workspaceUri, [...paths]); + void vscode.window.showInformationMessage(`Added all missing files to ignored file paths.`); + } catch (error) { + Issues.handleError(error); + } })); context.subscriptions.push(vscode.commands.registerCommand('gsc.changeRootFolder', async (workspaceUri: vscode.Uri, rootFolder: string) => { - await GscConfig.changeRootFolder(workspaceUri, rootFolder); - void vscode.window.showInformationMessage(`Changed root folder to '${rootFolder}'.`); + try { + await GscConfig.changeRootFolder(workspaceUri, rootFolder); + void vscode.window.showInformationMessage(`Changed root folder to '${rootFolder}'.`); + } catch (error) { + Issues.handleError(error); + } })); context.subscriptions.push(vscode.commands.registerCommand('gsc.addIncludedWorkspaceFolders', async (workspace: vscode.WorkspaceFolder, otherWorkspace: vscode.WorkspaceFolder, rootFolder?: string) => { - if (rootFolder) { - await GscConfig.changeRootFolder(otherWorkspace.uri, rootFolder); - void vscode.window.showInformationMessage(`Changed root folder to '${rootFolder}'.`); + try { + if (rootFolder) { + await GscConfig.changeRootFolder(otherWorkspace.uri, rootFolder); + void vscode.window.showInformationMessage(`Changed root folder to '${rootFolder}'.`); + } + await GscConfig.addIncludedWorkspaceFolders(workspace.uri, otherWorkspace.name); + void vscode.window.showInformationMessage(`Added workspace folder '${otherWorkspace.name}' for file references.`); + } catch (error) { + Issues.handleError(error); } - await GscConfig.addIncludedWorkspaceFolders(workspace.uri, otherWorkspace.name); - void vscode.window.showInformationMessage(`Added workspace folder '${otherWorkspace.name}' for file references.`); })); } async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { - return GscCodeActionProvider.getCodeActions(document.uri, context.diagnostics); + try { + return GscCodeActionProvider.getCodeActions(document.uri, context.diagnostics); + } catch (error) { + Issues.handleError(error); + return []; + } } diff --git a/src/GscCompletionItemProvider.ts b/src/GscCompletionItemProvider.ts index c9d11f2..7d2cbe3 100644 --- a/src/GscCompletionItemProvider.ts +++ b/src/GscCompletionItemProvider.ts @@ -5,6 +5,7 @@ import { CodFunctions } from './CodFunctions'; import { GscConfig, GscGame } from './GscConfig'; import { GscFunctions, GscVariableDefinition } from './GscFunctions'; import { LoggerOutput } from './LoggerOutput'; +import { Issues } from './Issues'; export interface CompletionConfig { variableItems: boolean; @@ -23,17 +24,21 @@ export class GscCompletionItemProvider implements vscode.CompletionItemProvider async provideCompletionItems( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken ): Promise { - // This function is called when user types a character or presses ctrl+space + try { + // This function is called when user types a character or presses ctrl+space + // Get parsed file + const gscFile = await GscFiles.getFileData(document.uri); - // Get parsed file - const gscFile = await GscFiles.getFileData(document.uri); + const currentGame = GscConfig.getSelectedGame(document.uri); - const currentGame = GscConfig.getSelectedGame(document.uri); + const items = await GscCompletionItemProvider.getCompletionItems(gscFile, position, currentGame, undefined, document.uri); - const items = await GscCompletionItemProvider.getCompletionItems(gscFile, position, currentGame, undefined, document.uri); - - return items; + return items; + } catch (error) { + Issues.handleError(error); + return undefined; + } } diff --git a/src/GscConfig.ts b/src/GscConfig.ts index f228566..8362d70 100644 --- a/src/GscConfig.ts +++ b/src/GscConfig.ts @@ -1,4 +1,6 @@ import * as vscode from 'vscode'; +import { LoggerOutput } from './LoggerOutput'; +import { Issues } from './Issues'; // These must match with package.json settings export enum GscGame { @@ -22,15 +24,60 @@ export type GscGameRootFolder = { } +type ConfigChangeHandler = () => Promise | void; + + export class GscConfig { public static game: GscGame = GscGame.UniversalGame; + private static configChangeSubscribers: ConfigChangeHandler[] = []; + static async activate(context: vscode.ExtensionContext) { + vscode.workspace.onDidChangeConfiguration((e) => this.onDidChangeConfiguration(e), null, context.subscriptions); + } + + + /** + * Handle vscode configuration change event + */ + private static async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) { + if (e.affectsConfiguration('gsc')) { + LoggerOutput.log("[GscConfig] GSC configuration changed."); + await GscConfig.emitConfigChange(); + } } + /** + * Subscribe to configuration changes. The handler will be called whenever the configuration changes. Subscribers are called in the order they were added. + * @param handler + */ + public static onDidConfigChange(handler: ConfigChangeHandler): void { + this.configChangeSubscribers.push(handler); + } + + + + /** + * Emit a configuration change event. This will call all subscribers in the order they were added. + */ + private static async emitConfigChange(): Promise { + for (const handler of this.configChangeSubscribers) { + try { + const result = handler(); + if (result instanceof Promise) { + await result; + } + } catch (error) { + Issues.handleError(error); + } + } + } + + + /** * Get path to game root folder. By default the game root folder is the workspace path. It can be changed in settings. Each folder can also have custom settings. */ @@ -54,6 +101,7 @@ export class GscConfig { public static changeRootFolder(uri: vscode.Uri, rootFolder: string) { + LoggerOutput.log("[GscConfig] Changing game root folder to: " + rootFolder, vscode.workspace.asRelativePath(uri)); const config = vscode.workspace.getConfiguration('gsc', uri); return config.update('gameRootFolder', rootFolder, vscode.ConfigurationTarget.WorkspaceFolder); } @@ -69,6 +117,7 @@ export class GscConfig { return ignoredFunctionNames; } public static addIgnoredFunctionName(uri: vscode.Uri, value: string) { + LoggerOutput.log("[GscConfig] Adding ignored function name: " + value, vscode.workspace.asRelativePath(uri)); const config = vscode.workspace.getConfiguration('gsc', uri); const ignoredFunctionNames: string[] = config.get('ignoredFunctionNames', []); ignoredFunctionNames.push(value); @@ -86,6 +135,7 @@ export class GscConfig { return ignoredFunctionNames; } public static addIgnoredFilePath(uri: vscode.Uri, value: string | string[]) { + LoggerOutput.log("[GscConfig] Adding ignored file path: " + value, vscode.workspace.asRelativePath(uri)); const config = vscode.workspace.getConfiguration('gsc', uri); const ignoredFilePaths: string[] = config.get('ignoredFilePaths', []); if (typeof value === 'string') { @@ -105,6 +155,7 @@ export class GscConfig { return GscConfig.errorDiagnosticsStringToEnum(selectedOption, ConfigErrorDiagnostics.Enable); } public static async updateErrorDiagnostics(uri: vscode.Uri, value: ConfigErrorDiagnostics) { + LoggerOutput.log("[GscConfig] Changing error diagnostics to: " + value, vscode.workspace.asRelativePath(uri)); // Load ignored function names const config = vscode.workspace.getConfiguration('gsc', uri); await config.update("errorDiagnostics", value, vscode.ConfigurationTarget.WorkspaceFolder); @@ -131,6 +182,8 @@ export class GscConfig { return includedWorkspaceFolders; } public static addIncludedWorkspaceFolders(uri: vscode.Uri, value: string | string[]) { + const valueStr = (typeof value === 'string') ? value : value.join(", "); + LoggerOutput.log("[GscConfig] Adding included workspace folder: " + valueStr, vscode.workspace.asRelativePath(uri)); const config = vscode.workspace.getConfiguration('gsc', uri); const includedWorkspaceFolders: string[] = config.get('includedWorkspaceFolders', []); if (typeof value === 'string') { @@ -168,6 +221,7 @@ export class GscConfig { } public static async updateSelectedGame(uri: vscode.Uri, game: GscGame) { + LoggerOutput.log("[GscConfig] Changing selected game to: " + game, vscode.workspace.asRelativePath(uri)); // Check if the URI is part of the workspace if (!vscode.workspace.getWorkspaceFolder(uri)) { throw new Error("The file is not part of the workspace."); diff --git a/src/GscDefinitionProvider.ts b/src/GscDefinitionProvider.ts index 0b8e54f..3b1b817 100644 --- a/src/GscDefinitionProvider.ts +++ b/src/GscDefinitionProvider.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { GscFile, GscFiles } from './GscFiles'; import { GroupType, GscData, GscFileParser } from './GscFileParser'; import { GscFunctions } from './GscFunctions'; +import { Issues } from './Issues'; export class GscDefinitionProvider implements vscode.DefinitionProvider { @@ -15,12 +16,17 @@ export class GscDefinitionProvider implements vscode.DefinitionProvider { token: vscode.CancellationToken ): Promise { - // Get parsed file - const gscFile = await GscFiles.getFileData(document.uri); + try { + // Get parsed file + const gscFile = await GscFiles.getFileData(document.uri); - const locations = await GscDefinitionProvider.getFunctionDefinitionLocations(gscFile, position); + const locations = await GscDefinitionProvider.getFunctionDefinitionLocations(gscFile, position); - return locations; + return locations; + } catch (error) { + Issues.handleError(error); + return null; + } } diff --git a/src/GscDiagnosticsCollection.ts b/src/GscDiagnosticsCollection.ts index c47fbbe..f57d26f 100644 --- a/src/GscDiagnosticsCollection.ts +++ b/src/GscDiagnosticsCollection.ts @@ -6,9 +6,10 @@ import { ConfigErrorDiagnostics, GscConfig, GscGame, GscGameRootFolder } from '. import { GscFunctions, GscFunctionState } from './GscFunctions'; import { assert } from 'console'; import { LoggerOutput } from './LoggerOutput'; +import { Issues } from './Issues'; export class GscDiagnosticsCollection { - private static diagnosticCollection: vscode.DiagnosticCollection | undefined; + public static diagnosticCollection: vscode.DiagnosticCollection | undefined; private static statusBarItem: vscode.StatusBarItem | undefined; static async activate(context: vscode.ExtensionContext) { @@ -28,11 +29,7 @@ export class GscDiagnosticsCollection { context.subscriptions.push(vscode.commands.registerCommand('gsc.refreshDiagnosticsCollection', () => this.refreshDiagnosticsCollection())); // Settings changed, handle it... - vscode.workspace.onDidChangeConfiguration((e) => this.onDidChangeConfiguration(e), null, context.subscriptions); - - GscFiles.onDidParseAllDocuments((files) => this.updateDiagnosticsAll("all files parsed")); - GscFiles.onDidParseDocument(data => this.generateDiagnostics(data)); - GscFiles.onDidDeleteDocument(uri => this.deleteDiagnostics(uri)); + GscConfig.onDidConfigChange(async () => await this.onDidConfigChange()); } @@ -43,19 +40,18 @@ export class GscDiagnosticsCollection { */ static async updateDiagnosticsAll(debugText: string) { - LoggerOutput.log("[GscDiagnosticsCollection] Creating diagnostics for all files because: " + debugText); + LoggerOutput.log("[GscDiagnosticsCollection] Creating diagnostics for all files", "because: " + debugText); // Get cached files for workspaces that have diagnostics enabled const workspaceFolders = vscode.workspace.workspaceFolders; if (workspaceFolders === undefined) { return; } - const workspaceFoldersWithEnabledErrors = workspaceFolders.filter(f => GscConfig.getErrorDiagnostics(f.uri) !== ConfigErrorDiagnostics.Disable); // Get cached files for workspaces that have diagnostics enabled - const files = GscFiles.getCachedFiles(workspaceFoldersWithEnabledErrors.map(f => f.uri)); + const files = GscFiles.getCachedFiles(); - LoggerOutput.log("[GscDiagnosticsCollection] files: " + files.length + ", workspaceFolders: " + workspaceFoldersWithEnabledErrors.map(f => f.name).join(", ")); + LoggerOutput.log("[GscDiagnosticsCollection] files: " + files.length + ", workspaceFolders: " + workspaceFolders.map(f => f.name).join(", ")); // Cancel the previous operation if it's still running @@ -114,115 +110,124 @@ export class GscDiagnosticsCollection { this.currentCancellationTokenSource = null; } - LoggerOutput.log("[GscDiagnosticsCollection] Done, diagnostics created: " + count); + LoggerOutput.log("[GscDiagnosticsCollection] Done all, diagnostics created: " + count); } /** - * This function is called when some gsc file is parsed. - * The parsed gsc file will be analyzed for commons errors like: - * - missing ; - * - unexpected tokens (bad syntax) + * Generate diagnostics for the given GSC file. This function is called when the file is parsed. + * @param gscFile The GSC file to generate diagnostics for. + * @returns The number of diagnostics created. */ static async generateDiagnostics(gscFile: GscFile): Promise { - //console.log("[DiagnosticsProvider]", "Document changed, creating diagnostics..."); - const uri = gscFile.uri; + try { + LoggerOutput.log("[GscDiagnosticsCollection] Creating diagnostics for file", vscode.workspace.asRelativePath(gscFile.uri)); + + const uri = gscFile.uri; - const diagnostics: vscode.Diagnostic[] = []; + // Clear array + gscFile.diagnostics.length = 0; - // Return empty diagnostics if diagnostics are disabled - if (gscFile.errorDiagnostics === ConfigErrorDiagnostics.Disable) { - this.diagnosticCollection?.set(uri, diagnostics); - return 0; - } + // Return empty diagnostics if diagnostics are disabled + if (gscFile.errorDiagnostics === ConfigErrorDiagnostics.Disable) { + this.diagnosticCollection?.set(uri, gscFile.diagnostics); + LoggerOutput.log("[GscDiagnosticsCollection] Done for file, diagnostics is disabled", vscode.workspace.asRelativePath(gscFile.uri)); + return 0; + } - // Load ignored function names - const isUniversalGame = GscConfig.isUniversalGame(gscFile.currentGame); - - const groupFunctionNames: {group: GscGroup, uri: vscode.Uri}[] = []; - const groupIncludedPaths: {group: GscGroup, uri: vscode.Uri}[] = []; + // Load ignored function names + const isUniversalGame = GscConfig.isUniversalGame(gscFile.currentGame); + + const groupFunctionNames: {group: GscGroup, uri: vscode.Uri}[] = []; + const groupIncludedPaths: {group: GscGroup, uri: vscode.Uri}[] = []; - // Process the file - walkGroupItems(gscFile.data.root, gscFile.data.root.items); + // Process the file + walkGroupItems(gscFile.data.root, gscFile.data.root.items); - // Create diagnostic for function names - for (const d of groupFunctionNames) { - const diag = await GscDiagnosticsCollection.createDiagnosticsForFunctionName(d.group, gscFile); - if (diag) { - diagnostics.push(diag); + // Create diagnostic for function names + for (const d of groupFunctionNames) { + const diag = await GscDiagnosticsCollection.createDiagnosticsForFunctionName(d.group, gscFile); + if (diag) { + gscFile.diagnostics.push(diag); + } } - } - // Create diagnostic for included files - for (const d of groupIncludedPaths) { - const diag = GscDiagnosticsCollection.createDiagnosticsForIncludedPaths(d.group, gscFile); - if (diag) { - diagnostics.push(diag); + // Create diagnostic for included files + for (const d of groupIncludedPaths) { + const diag = GscDiagnosticsCollection.createDiagnosticsForIncludedPaths(d.group, gscFile); + if (diag) { + gscFile.diagnostics.push(diag); + } } - } - // TODO check where this file is referenced to update particular files - // It will be crusual for #include files + // TODO check where this file is referenced to update particular files + // It will be crusual for #include files - this.diagnosticCollection?.set(uri, diagnostics); + this.diagnosticCollection?.set(uri, gscFile.diagnostics); - // ------------------------------------------------------------------------------------------------------------------------------------------ - // ------------------------------------------------------------------------------------------------------------------------------------------ + // ------------------------------------------------------------------------------------------------------------------------------------------ + // ------------------------------------------------------------------------------------------------------------------------------------------ - function walkGroupItems(parentGroup: GscGroup, items: GscGroup[]) { - // This object have child items, process them first - for (var i = 0; i < items.length; i++) { - const innerGroup = items[i]; - const nextGroup = items.at(i + 1); - - const diagnostic = action(parentGroup, innerGroup); - if (diagnostic === undefined) { - walkGroupItems(innerGroup, innerGroup.items); - } else { - diagnostics.push(diagnostic); - } + function walkGroupItems(parentGroup: GscGroup, items: GscGroup[]) { + // This object have child items, process them first + for (var i = 0; i < items.length; i++) { + const innerGroup = items[i]; + const nextGroup = items.at(i + 1); - function action(parentGroup: GscGroup, group: GscGroup): vscode.Diagnostic | undefined - { - if (group.type === GroupType.Unknown) { - return new vscode.Diagnostic(group.getRange(), "Unexpected token", vscode.DiagnosticSeverity.Error); - } - else if (group.solved === false) { - return GscDiagnosticsCollection.createDiagnosticsForUnsolved(group, parentGroup, nextGroup); - + const diagnostic = action(parentGroup, innerGroup); + if (diagnostic === undefined) { + walkGroupItems(innerGroup, innerGroup.items); } else { - switch (group.type as GroupType) { + gscFile.diagnostics.push(diagnostic); + } - // Function path or #include path - case GroupType.Path: - groupIncludedPaths.push({group, uri}); - break; + function action(parentGroup: GscGroup, group: GscGroup): vscode.Diagnostic | undefined + { + if (group.type === GroupType.Unknown) { + return new vscode.Diagnostic(group.getRange(), "Unexpected token", vscode.DiagnosticSeverity.Error); + } + else if (group.solved === false) { + return GscDiagnosticsCollection.createDiagnosticsForUnsolved(group, parentGroup, nextGroup); + + } else { + switch (group.type as GroupType) { + + // Function path or #include path + case GroupType.Path: + groupIncludedPaths.push({group, uri}); + break; - case GroupType.FunctionName: - groupFunctionNames.push({group, uri}); - break; + case GroupType.FunctionName: + groupFunctionNames.push({group, uri}); + break; - case GroupType.DataTypeKeyword: - if (!isUniversalGame && gscFile.currentGame !== GscGame.CoD1) { - return new vscode.Diagnostic(group.getRange(), "Casting to data type is not supported for " + gscFile.currentGame, vscode.DiagnosticSeverity.Error); - } - break; + case GroupType.DataTypeKeyword: + if (!isUniversalGame && gscFile.currentGame !== GscGame.CoD1) { + return new vscode.Diagnostic(group.getRange(), "Casting to data type is not supported for " + gscFile.currentGame, vscode.DiagnosticSeverity.Error); + } + break; - case GroupType.ExtraTerminator: - return new vscode.Diagnostic(group.getRange(), "Terminator ; is not needed", vscode.DiagnosticSeverity.Information); + case GroupType.ExtraTerminator: + return new vscode.Diagnostic(group.getRange(), "Terminator ; is not needed", vscode.DiagnosticSeverity.Information); + } } + return undefined; } - return undefined; } } - } - //console.log("[DiagnosticsProvider]", "Diagnostics done"); + LoggerOutput.log("[GscDiagnosticsCollection] Done for file, diagnostics created: " + gscFile.diagnostics.length, vscode.workspace.asRelativePath(gscFile.uri)); + - return diagnostics.length; + return gscFile.diagnostics.length; + } + catch (error) { + Issues.handleError(error); + return 0; + } } @@ -412,13 +417,11 @@ export class GscDiagnosticsCollection { } - static async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) { - if (e.affectsConfiguration('gsc')) { - await this.updateDiagnosticsAll("config changed"); - } + private static async onDidConfigChange() { + await this.updateDiagnosticsAll("config changed"); } - static async refreshDiagnosticsCollection() { + private static async refreshDiagnosticsCollection() { await this.updateDiagnosticsAll("manual refresh"); } diff --git a/src/GscFiles.ts b/src/GscFiles.ts index 738ecf3..07705ca 100644 --- a/src/GscFiles.ts +++ b/src/GscFiles.ts @@ -4,6 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { ConfigErrorDiagnostics, GscConfig, GscGame, GscGameRootFolder } from './GscConfig'; import { LoggerOutput } from './LoggerOutput'; +import { GscDiagnosticsCollection } from './GscDiagnosticsCollection'; /** * On startup scan every .gsc file, parse it, and save the result into memory. @@ -14,9 +15,6 @@ export class GscFiles { private static cachedFilesPerWorkspace: Map = new Map(); - private static _onDidParseDocument: vscode.EventEmitter = new vscode.EventEmitter(); - private static _onDidParseAllDocuments: vscode.EventEmitter = new vscode.EventEmitter(); - private static _onDidDeleteDocument: vscode.EventEmitter = new vscode.EventEmitter(); private static parseAllFiles = false; private static statusBarItem: vscode.StatusBarItem | undefined; @@ -35,9 +33,8 @@ export class GscFiles { context.subscriptions.push(vscode.workspace.onDidCreateFiles(this.onCreateFiles)); context.subscriptions.push(vscode.workspace.onDidDeleteFiles(this.onDeleteFiles)); - context.subscriptions.push(vscode.workspace.onDidRenameFiles(this.onRenameFiles)); - context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(e => this.onChangeWorkspaceFolders(e))); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => this.onDidChangeConfiguration(e))); + context.subscriptions.push(vscode.workspace.onDidRenameFiles(e => this.onRenameFiles)); + context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(e => this.onChangeWorkspaceFolders(e))); context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(e => this.onChangeEditorSelection(e))); context.subscriptions.push(vscode.commands.registerCommand('gsc.debugParsedGscFile', this.debugParsedGscFile)); @@ -46,9 +43,8 @@ export class GscFiles { context.subscriptions.push(vscode.commands.registerCommand('gsc.debugParsedUris', this.debugParsedUris)); context.subscriptions.push(vscode.commands.registerCommand('gsc.debugCachedFiles', () => this.showDebugWindow(context))); context.subscriptions.push(vscode.commands.registerCommand('gsc.parseAll', () => setTimeout(() => this.initialParse(), 1))); - - // Close debug window if it was open and workspace folders changed (activate is called again in this case without deactivate) - //this.closeDebugWindow(); + + GscConfig.onDidConfigChange(async () => { await this.onDidConfigChange(); }); // Restore the debug window if it was open in the last session if (context.globalState.get('debugWindowOpen')) { @@ -66,22 +62,34 @@ export class GscFiles { /** - * Get parsed file from cache, opened document or file on disk. If the file is part of workspace, file is cached for later use. - * @param fileUri Uri of the file to get. If the URI is not part of workspace, file is just parsed and not cached. - * @param workspaceUri Workspace URI can be specified to speed up the process (then its trusted that file is part of workspace and will be cached). + * Get parsed file data from cache, opened document or file on disk. + * If the file is in cache memory, the cached data is returned unless forceParsing is true. + * If the file is not cached or forceParsing is true, the file is parsed and saved into cache. + * If the file is not part of workspace, it is just parsed and not cached. + * @param fileUri Uri of the file to get. + * @param forceParsing If true, the file is parsed even if it is already in cache. + * @param doParseNotify * @returns */ - public static async getFileData(fileUri: vscode.Uri, ignoreNotify: boolean = false): Promise { + public static async getFileData(fileUri: vscode.Uri, forceParsing: boolean = false, doParseNotify: boolean = true): Promise { + const doLog = doParseNotify; + + if (doLog) { + LoggerOutput.log("[GscFiles] Getting file data", vscode.workspace.asRelativePath(fileUri)); + } + // This file is part of workspace, save it into cache; otherwise ignore it const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri); - // Parse file - var gsc = await this.parseFile(fileUri); - // This file is not part of workspace, return it and do not cache it if (workspaceFolder === undefined) { - return new GscFile(gsc, fileUri, workspaceFolder); + const gsc = await this.parseFile(fileUri); + const gscFile = new GscFile(gsc, fileUri, workspaceFolder); + if (doLog) { + LoggerOutput.log("[GscFiles] Done (not part of workspace)", fileUri.toString()); + } + return gscFile; } // Get data of workspace that contains cached files @@ -93,36 +101,55 @@ export class GscFiles { // Try to get cached file let fileData = dataOfWorkspace.getParsedFile(fileUri); + let bParsed = false; + + // If file is not found in cache if (fileData === undefined) { - fileData = new GscFile(gsc, fileUri, workspaceFolder); // Create new cached file + // Parse the file and save it into cache + const gsc = await this.parseFile(fileUri); + fileData = new GscFile(gsc, fileUri, workspaceFolder); dataOfWorkspace.addParsedFile(fileData); + bParsed = true; + + // If cached file was found } else { - fileData.updateData(gsc); // Update data of existing cached file + // If its forced to parse (so cache is ignored) + if (forceParsing) { + // Parse the file and update the cache file data + const gsc = await this.parseFile(fileUri); + fileData.updateData(gsc); + bParsed = true; + } else { + doParseNotify = false; // Do not notify because the files was not parsed + } } // If opened, update debug window this.updateDebugCachedFilesWindow(); - - // Run initial scan of all files - if (this.parseAllFiles) { - this.parseAllFiles = false; - - await this.initialParse(); - this._onDidParseDocument.fire(fileData); + // When all files are being parsed, ignoreNotify is true + if (doParseNotify) { + await GscDiagnosticsCollection.generateDiagnostics(fileData); + } - return fileData; + if (doLog) { + LoggerOutput.log("[GscFiles] Done, " + (bParsed ? "was parsed" : "loaded from cache"), vscode.workspace.asRelativePath(fileUri)); } - else if (ignoreNotify === false) { - // Notify all subscribers that the document has been parsed - this._onDidParseDocument.fire(fileData); + + // Run initial scan of all files + if (this.parseAllFiles) { + this.parseAllFiles = false; + await this.initialParse(); } return fileData; } + + + public static async initialParse() { if (GscFiles.statusBarItem) { GscFiles.statusBarItem.show(); @@ -131,25 +158,24 @@ export class GscFiles { this.removeAllCachedFiles(); // Parse all - await GscFiles.parseAndCacheAllFiles(true); // true = ignore notify + await GscFiles.parseAndCacheAllFiles(); - // Notify subscribers that all files has been parsed - const files = this.getCachedFiles(); - this._onDidParseAllDocuments.fire(files); - if (GscFiles.statusBarItem) { GscFiles.statusBarItem.hide(); GscFiles.statusBarItem.dispose(); GscFiles.statusBarItem = undefined; } + + // Update diagnostics for all files + await GscDiagnosticsCollection.updateDiagnosticsAll("all files parsed"); } /** * Load all .gsc files opened in editor or found in workspace file system, parse them and save them into memory */ - public static async parseAndCacheAllFiles(ignoreNotify: boolean = false) { - LoggerOutput.log("[GscFile] Parsing all GSC files..."); + public static async parseAndCacheAllFiles() { + LoggerOutput.log("[GscFiles] Parsing all GSC files..."); const start = performance.now(); // Find all GSC files in repository @@ -159,7 +185,7 @@ export class GscFiles { let i = 0; const parseFile = async (file: vscode.Uri, index: number) => { - const gsc = await this.getFileData(file, ignoreNotify); + const gsc = await this.getFileData(file, true, false); if (GscFiles.statusBarItem) { GscFiles.statusBarItem.text = `$(sync~spin) Parsing GSC file ${index + 1}/${files.length}...`; GscFiles.statusBarItem.tooltip = file.fsPath; @@ -182,7 +208,7 @@ export class GscFiles { //this.debugParsedFiles(true); //console.log(this, "Files:", this.parsedFiles.size, "Total time:", elapsed, "Errors:", errors); - LoggerOutput.log("[GscFile] All GSC files parsed, files: " + this.getCachedFiles().length + ", time: " + elapsed + "ms"); + LoggerOutput.log("[GscFiles] All GSC files parsed, files: " + this.getCachedFiles().length + ", time: " + elapsed + "ms"); } @@ -404,20 +430,7 @@ export class GscFiles { - /** - * Get referenced file in specified file. It first tries to find the file in the same workspace folder, then in included workspace folders. - */ - public static get onDidParseDocument(): vscode.Event { - return this._onDidParseDocument.event; - } - - public static get onDidParseAllDocuments(): vscode.Event { - return this._onDidParseAllDocuments.event; - } - public static get onDidDeleteDocument(): vscode.Event { - return this._onDidDeleteDocument.event; - } static isValidGscFile(filePath: string): boolean { @@ -431,36 +444,34 @@ export class GscFiles { continue; } void GscFiles.getFileData(file); - LoggerOutput.log("[GscFile] Added " + vscode.workspace.asRelativePath(file) + " for parsing, because new file is created"); + LoggerOutput.log("[GscFiles] Added " + vscode.workspace.asRelativePath(file) + " for parsing, because new file is created"); } } static onDeleteFiles(e: vscode.FileDeleteEvent) { // Refresh all to ensure correct validation - LoggerOutput.log("[GscFile] Re-parsing all because some file was deleted"); + LoggerOutput.log("[GscFiles] Re-parsing all because some file was deleted"); void GscFiles.initialParse(); } // Called when file or folder is renamed or moved static onRenameFiles(e: vscode.FileRenameEvent) { // Refresh all to ensure correct validation - LoggerOutput.log("[GscFile] Re-parsing all because some file was renamed"); + LoggerOutput.log("[GscFiles] Re-parsing all because some file was renamed"); void GscFiles.initialParse(); } static onChangeWorkspaceFolders(e: vscode.WorkspaceFoldersChangeEvent) { // Refresh all to ensure correct validation - LoggerOutput.log("[GscFile] Re-parsing all because workspace folders changed"); + LoggerOutput.log("[GscFiles] Re-parsing all because workspace folders changed"); void GscFiles.initialParse(); } - static onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) { - if (e.affectsConfiguration('gsc')) { - LoggerOutput.log("[GscFile] Configuration changed, updating cached files configuration."); - // Update configuration for GscFile before DiagnosticCollection is called - for (const [uriKey, workspaceData] of this.cachedFilesPerWorkspace) { - workspaceData.updateConfiguration(); - } - } + private static async onDidConfigChange() { + LoggerOutput.log("[GscFiles] Configuration changed, updating cached files configuration."); + // Update configuration for GscFile before DiagnosticCollection is called + for (const [uriKey, workspaceData] of this.cachedFilesPerWorkspace) { + workspaceData.updateConfiguration(); + } } static onChangeEditorSelection(e: vscode.TextEditorSelectionChangeEvent) { @@ -585,6 +596,7 @@ export class GscFiles { // If it is still defined, it means it was closed by user, otherwise it was closed by code if (this.debugWindow) { void context.globalState.update('debugWindowOpen', false); + this.debugWindow = undefined; } }, null, context.subscriptions); @@ -602,8 +614,8 @@ export class GscFiles { } private static updateDebugCachedFilesWindow() { - if (this.debugWindow) { - this.debugWindow.webview.html = this.getWebviewContent(); + if (GscFiles.debugWindow) { + GscFiles.debugWindow.webview.html = this.getWebviewContent(); } } @@ -629,6 +641,9 @@ export class GscFiles { if (gscFile === undefined) { html += "

File is not part of workspace: " + uri.fsPath + "

"; } else { + + + // Get group before cursor var groupAtCursor = gscFile.data.root.findGroupOnLeftAtPosition(cursor); @@ -693,6 +708,10 @@ export class GscFile { currentGame: GscGame; errorDiagnostics: ConfigErrorDiagnostics; + + diagnostics: vscode.Diagnostic[] = []; + + constructor( /** Parsed data */ public data: GscData, diff --git a/src/GscHoverProvider.ts b/src/GscHoverProvider.ts index 8d72d0c..16da6da 100644 --- a/src/GscHoverProvider.ts +++ b/src/GscHoverProvider.ts @@ -4,6 +4,8 @@ import { GroupType, GscData } from './GscFileParser'; import { CodFunctions } from './CodFunctions'; import { ConfigErrorDiagnostics, GscConfig } from './GscConfig'; import { GscFunctions, GscFunctionState } from './GscFunctions'; +import { Issues } from './Issues'; +import { LoggerOutput } from './LoggerOutput'; export class GscHoverProvider implements vscode.HoverProvider { @@ -15,17 +17,25 @@ export class GscHoverProvider implements vscode.HoverProvider { document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken - ): Promise - { - // Get parsed file - const gscData = await GscFiles.getFileData(document.uri); + ): Promise { + try { + LoggerOutput.log("[GscHoverProvider] Provide hover at " + position.line + ":" + position.character, vscode.workspace.asRelativePath(document.uri)); + + // Get parsed file + const gscData = await GscFiles.getFileData(document.uri); - const hover = await GscHoverProvider.getHover(gscData, position); + const hover = await GscHoverProvider.getHover(gscData, position); - return hover; + LoggerOutput.log("[GscHoverProvider] Done, hover: " + (hover !== undefined), vscode.workspace.asRelativePath(document.uri)); + + return hover; + } catch (error) { + Issues.handleError(error); + } } - public static async getHover(gscFile: GscFile, position: vscode.Position): Promise { + + public static async getHover(gscFile: GscFile, position: vscode.Position): Promise { let markdown = new vscode.MarkdownString(); markdown.isTrusted = true; // enable HTML tags @@ -132,7 +142,11 @@ export class GscHoverProvider implements vscode.HoverProvider { } } - return new vscode.Hover(markdown); + if (markdown.value === "") { + return undefined; + } else { + return new vscode.Hover(markdown); + } } public static markdownAppendFileWasNotFound(md: vscode.MarkdownString, funcName: string, path: string) { diff --git a/src/GscSemanticTokensProvider.ts b/src/GscSemanticTokensProvider.ts index c05410a..b43951f 100644 --- a/src/GscSemanticTokensProvider.ts +++ b/src/GscSemanticTokensProvider.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode'; import { GscFiles } from './GscFiles'; import { GroupType, GscGroup} from './GscFileParser'; +import { error } from 'console'; +import { Issues } from './Issues'; export class GscSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider { @@ -42,7 +44,16 @@ export class GscSemanticTokensProvider implements vscode.DocumentSemanticTokensP async provideDocumentSemanticTokens( document: vscode.TextDocument ): Promise { - + try { + return await this.getDocumentSemanticTokens(document); + } catch (error) { + Issues.handleError(error); + } + } + + async getDocumentSemanticTokens( + document: vscode.TextDocument + ): Promise { // This function is called when // - when the document's content has changed, or // - when the document is first opened, or @@ -53,7 +64,7 @@ export class GscSemanticTokensProvider implements vscode.DocumentSemanticTokensP const builder = new vscode.SemanticTokensBuilder(GscSemanticTokensProvider.legend); // Get the parsed file - var gscFile = await GscFiles.getFileData(document.uri); + var gscFile = await GscFiles.getFileData(document.uri, true); function walkGroupItems(parentGroup: GscGroup, items: GscGroup[], action: (parentGroup: GscGroup, group: GscGroup) => void) { diff --git a/src/GscStatusBar.ts b/src/GscStatusBar.ts index 5c0971a..95a9f2d 100644 --- a/src/GscStatusBar.ts +++ b/src/GscStatusBar.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { GscConfig } from './GscConfig'; import { EXTENSION_ID } from './extension'; import { LoggerOutput } from './LoggerOutput'; +import { GscFiles } from './GscFiles'; export class GscStatusBar { @@ -56,11 +57,11 @@ export class GscStatusBar { return; } - const selectedGame = GscConfig.getSelectedGame(activeEditor.document.uri); + const gscFile = await GscFiles.getFileData(activeEditor.document.uri); - LoggerOutput.log(`[GscFile] Updating status bar with game: ${selectedGame} because of: ${debugText}`); + LoggerOutput.log(`[GscStatusBar] Updating status bar with game: "${gscFile.currentGame}"`, `because: ${debugText}`); - gameBarItem.text = "$(notebook-open-as-text) " + (selectedGame); + gameBarItem.text = "$(notebook-open-as-text) " + (gscFile.currentGame); gameBarItem.show(); settingsBarItem.show(); @@ -75,11 +76,9 @@ export class GscStatusBar { //vscode.workspace.onDidOpenTextDocument(updateStatusBar, null, context.subscriptions); //vscode.workspace.onDidCloseTextDocument(updateStatusBar, null, context.subscriptions); - vscode.workspace.onDidChangeConfiguration(async (e) => { - if (e.affectsConfiguration('gsc')) { - await updateStatusBar("configChanged"); - } - }, null, context.subscriptions); + GscConfig.onDidConfigChange(async () => { + await updateStatusBar("configChanged"); + }); // Initial update of the status bar visibility await updateStatusBar("init"); diff --git a/src/Issues.ts b/src/Issues.ts index 72f145b..d5d8e33 100644 --- a/src/Issues.ts +++ b/src/Issues.ts @@ -1,85 +1,64 @@ import * as vscode from 'vscode'; import * as os from 'os'; import { DISCORD_URL, EMAIL, EXTENSION_ID, GITHUB_ISSUES_URL } from './extension'; +import { LoggerOutput } from './LoggerOutput'; export class Issues { - private static isDisposed = false; - static activate(context: vscode.ExtensionContext) { - - Issues.isDisposed = false; - context.subscriptions.push({ - dispose: () => { - Issues.isDisposed = true; - } - }); - + let disposable = vscode.commands.registerCommand('gsc.showErrorInfo', async (errorDetails: string) => { - const panel = vscode.window.createWebviewPanel( - 'errorInfo', // Identifies the type of the webview - 'Report an Issue', // Title of the panel - vscode.ViewColumn.One, // Editor column to show the new webview panel in - { - enableScripts: true, // Allow scripts in the webview - } // Webview options - ); - - // Set the webview's HTML content - panel.webview.html = Issues.getWebviewContent(errorDetails); + try { + const panel = vscode.window.createWebviewPanel( + 'errorInfo', // Identifies the type of the webview + 'Report an Issue', // Title of the panel + vscode.ViewColumn.One, // Editor column to show the new webview panel in + { + enableScripts: true, // Allow scripts in the webview + } // Webview options + ); + + // Set the webview's HTML content + panel.webview.html = Issues.getWebviewContent(errorDetails); + } catch (error) { + console.error(error); + } }); context.subscriptions.push(disposable); + } - // VSCode runs each extension in its own separate process (the extension host) - // It isolates the extension from the main VSCode application and other extensions. - // This means that the global error handlers you set up in this extension will not capture errors from other extensions or from VSCode itself. + static handleError(err: unknown) { - // Handle uncaught exceptions - process.on('uncaughtException', (error) => { - if (Issues.isDisposed) { - console.log("Extension has been disposed, ignoring unhandled exception. ", error.message); - return; // Ignore the error since the extension is disposed - } - - try { - Issues.handleGlobalError(error); - } catch (error) { } - }); - - // Handle unhandled promise rejections - process.on('unhandledRejection', (reason: any) => { - if (Issues.isDisposed) { - console.log("Extension has been disposed, ignoring unhandled rejection.", reason.message); - return; // Ignore the error since the extension is disposed - } - - try { - Issues.handleGlobalError(reason instanceof Error ? reason : new Error(reason)); - } catch (error) {} - }); - } + const error = Issues.normalizeError(err); - static handleGlobalError(error: Error) { const ver = vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.version; - const errorMessage = `Error in CoD GSC Extension v${ver}: ${error.message}`; - const errorDetails = ` + const errorMessage = `Error: ${error.message}`; + + + void vscode.window.showErrorMessage(errorMessage, 'Report Issue').then((selection) => { + if (selection === 'Report Issue') { + + const errorDetails = ` Extension Version: ${ver} VSCode Version: ${vscode.version} OS: ${os.type()} ${os.release()} ${error.stack} -`; - - void vscode.window.showErrorMessage(errorMessage, 'Report Issue').then((selection) => { - if (selection === 'Report Issue') { +Log (5min): +${LoggerOutput.getLogs().join('\n')} + + `; + void vscode.commands.executeCommand('gsc.showErrorInfo', errorDetails); } }); - - console.error('Unhandled error:', error); + + console.error(error); + + LoggerOutput.log("[Issues] " + error.message); } static getWebviewContent(errorDetails: string) { @@ -131,4 +110,29 @@ ${error.stack} `; } + + + /** + * Normalizes any unknown error into an Error object. + * @param error - The unknown error to normalize. + * @returns An Error object with a standardized message. + */ + public static normalizeError(error: unknown): Error { + if (error instanceof Error) { + return error; + } else if (typeof error === 'string') { + return new Error(error); + } else if (typeof error === 'object' && error !== null) { + // The error is an object, attempt to stringify it + try { + const errorString = JSON.stringify(error, Object.getOwnPropertyNames(error)); + return new Error(errorString); + } catch (stringifyError) { + return new Error('An unknown error occurred'); + } + } else { + // The error is of an unknown type (number, boolean, null, undefined, etc.) + return new Error(String(error) || 'An unknown error occurred'); + } + } } \ No newline at end of file diff --git a/src/LoggerOutput.ts b/src/LoggerOutput.ts index 0da481b..b8ef352 100644 --- a/src/LoggerOutput.ts +++ b/src/LoggerOutput.ts @@ -1,8 +1,14 @@ import * as vscode from 'vscode'; +interface LogEntry { + timestamp: Date; + message: string; +} + export class LoggerOutput { private static outputChannel: vscode.OutputChannel; private static isDisposed = false; + private static logBuffer: LogEntry[] = []; // Internal log buffer // Activates the logger and registers the necessary disposal function static activate(context: vscode.ExtensionContext) { @@ -17,22 +23,83 @@ export class LoggerOutput { } // Log a message to the custom output channel - static log(message: string) { + static log(message: string, rightMessage?: string, spaces?: number) { if (!this.outputChannel) { this.outputChannel = vscode.window.createOutputChannel('GSC Logs'); } - const timestamp = LoggerOutput.getFormattedTimestamp(); - this.outputChannel.appendLine(`${timestamp} ${message}`); - - //this.outputChannel.show(); // Bring the output panel to the front if necessary + + if (spaces === undefined) { + spaces = 70; + } + + // If there is a right message, align the left message to number of spaces and then add the right message + if (rightMessage !== undefined) { + message = message.padEnd(spaces) + "" + rightMessage; + } + + const now = new Date(); + const timestamp = LoggerOutput.getFormattedTimestamp(now); + const fullMessage = `${timestamp} ${message}`; + + // Append to OutputChannel + this.outputChannel.appendLine(fullMessage); + + // Store in internal buffer with timestamp + this.logBuffer.push({ + timestamp: now, + message: fullMessage, + }); + + // Clean up old logs beyond 5 minutes + this.cleanUpOldLogs(); + } + + // Method to retrieve the logs from the last 5 minutes, from latest to oldest + static getLogs(): string[] { + const now = new Date(); + const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago + + const recentLogs: string[] = []; + + // Iterate backwards since logs are stored in chronological order + for (let i = this.logBuffer.length - 1; i >= 0; i--) { + const logEntry = this.logBuffer[i]; + if (logEntry.timestamp >= fiveMinutesAgo) { + recentLogs.push(logEntry.message); + } else { + // Since logs are chronological, no need to check earlier logs + break; + } + } + + return recentLogs; } // Helper to format the current timestamp - private static getFormattedTimestamp(): string { + private static getFormattedTimestamp(date: Date): string { + const datePart = date.toISOString().slice(0, 10); + const timePart = date.toISOString().slice(11, 23); + return `${datePart} ${timePart}`; // Example: 2024-09-14 10:15:59.649 + } + + // Clean up old logs that are older than 5 minutes + private static cleanUpOldLogs() { const now = new Date(); - const date = now.toISOString().slice(0, 10); - const time = now.toISOString().slice(11, 23); - return `${date} ${time}`; // Example: 2024-09-14 10:15:59.649 + const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago + + // Find the index where logs are newer than five minutes ago + let firstRecentLogIndex = 0; + for (let i = 0; i < this.logBuffer.length; i++) { + if (this.logBuffer[i].timestamp >= fiveMinutesAgo) { + firstRecentLogIndex = i; + break; + } + } + + // Remove old logs from the buffer + if (firstRecentLogIndex > 0) { + this.logBuffer = this.logBuffer.slice(firstRecentLogIndex); + } } // Disposes of the output channel when no longer needed diff --git a/src/test/Tests.test.ts b/src/test/Tests.test.ts index 74b3039..6bf6a95 100644 --- a/src/test/Tests.test.ts +++ b/src/test/Tests.test.ts @@ -10,6 +10,7 @@ import { GscFile, GscFiles } from '../GscFiles'; import { GscCompletionItemProvider } from '../GscCompletionItemProvider'; import { GscCodeActionProvider } from '../GscCodeActionProvider'; import { GscFunction } from '../GscFunctions'; +import { LoggerOutput } from '../LoggerOutput'; export const testWorkspaceDir = path.join(os.tmpdir(), 'vscode-test-workspace'); @@ -24,6 +25,9 @@ export async function activateExtension() { await extension!.activate(); assert.ok(extension!.isActive, "Extension should be activated"); + + // Clear all editors + vscode.commands.executeCommand('workbench.action.closeAllEditors'); } @@ -34,34 +38,25 @@ export function sleep(ms: number): Promise { -export async function loadGscFile(paths: string[], doWaitForDiagnosticsUpdate: boolean = true): Promise<[gscFile: GscFile, diagnostics: vscode.Diagnostic[]]> { +export async function loadGscFile(paths: string[]): Promise { const filePath = path.join(testWorkspaceDir, ...paths); const fileUri = vscode.Uri.file(filePath); - - if (doWaitForDiagnosticsUpdate) { - const diagnosticsPromise = waitForDiagnosticsUpdate(fileUri); - - var gscFile = await GscFiles.getFileData(fileUri); - - const diagnostics = await diagnosticsPromise; - - return [gscFile, diagnostics]; - } else { - var gscFile = await GscFiles.getFileData(fileUri); - return [gscFile, []]; - } + LoggerOutput.log("[Tests] loadGscFile() " + vscode.workspace.asRelativePath(fileUri)); + + var gscFile = await GscFiles.getFileData(fileUri); + return gscFile; } -export function checkDiagnostic(diagnosticItems: vscode.Diagnostic[], index: number, errorText: string, severity: vscode.DiagnosticSeverity) { +export function checkDiagnostic(diagnosticItems: readonly vscode.Diagnostic[], index: number, errorText: string, severity: vscode.DiagnosticSeverity) { function message(message: string, current: string, expected: string) { var debugText = diagnosticItems.map((diagnostic, i) => " " + i + ": " + diagnostic.message + " [" + vscode.DiagnosticSeverity[diagnostic.severity] + "]").join('\n'); - return message + "\n\ndiagnostics[" + index + "] = \n'" + current + "'. \n\nExpected: \n'" + expected + "'. \n\nErrors:\n" + debugText; + return message + "\n\ndiagnostics[" + index + "] = \n'" + current + "'. \n\nExpected: \n'" + expected + "'. \n\nErrors:\n" + debugText + "\n\n" + LoggerOutput.getLogs().join('\n'); } var item = diagnosticItems.at(index); @@ -84,7 +79,7 @@ export function checkQuickFix(codeActions: vscode.CodeAction[], index: number, e function message(message: string, current: string, expected: string) { var debugText = codeActions.map((diagnostic, i) => " " + i + ": " + diagnostic.title + " [" + diagnostic.kind?.value + "]").join('\n'); - return "diagnostics[" + index + "] = \n'" + current + "'. \n\nExpected: \n'" + expected + "'. \n\nMessage: " + message + ")\n\nErrors:\n" + debugText; + return "diagnostics[" + index + "] = \n'" + current + "'. \n\nExpected: \n'" + expected + "'. \n\nMessage: " + message + ")\n\nErrors:\n" + debugText + "\n\n" + LoggerOutput.getLogs().join('\n'); } var item = codeActions.at(index); @@ -99,7 +94,7 @@ export function checkQuickFix(codeActions: vscode.CodeAction[], index: number, e -export function checkHover(hover: vscode.Hover, expected: string) { +export function checkHover(hover: vscode.Hover | undefined, expected: string) { assert.ok(hover !== undefined); assert.ok(hover.contents.length === 1); assert.ok(hover.contents[0] instanceof vscode.MarkdownString); @@ -141,7 +136,7 @@ export function checkCompletions(gscFile: GscFile, items: vscode.CompletionItem[ function message(message: string, current: string, expected: string) { var debugText = gscFile.data.content + "\n\n"; debugText += printCompletionItems(items); - return message + ". Current: '" + current + "'. Expected: '" + expected + "'. At: " + index + ")\n\n" + debugText + "\n\n"; + return message + ". Current: '" + current + "'. Expected: '" + expected + "'. At: " + index + ")\n\n" + debugText + "\n\n" + LoggerOutput.getLogs().join('\n') + "\n\n"; } function printCompletionItems(items: vscode.CompletionItem[]) { @@ -178,32 +173,6 @@ export function checkCompletions(gscFile: GscFile, items: vscode.CompletionItem[ - - - - - - - -export function waitForDiagnosticsUpdate(uri: vscode.Uri): Promise { - return new Promise((resolve, reject) => { - const disposable = vscode.languages.onDidChangeDiagnostics((e) => { - //console.log("onDidChangeDiagnostics event fired"); - if (e.uris.some(changedUri => changedUri.toString() === uri.toString())) { - const diagnostics = vscode.languages.getDiagnostics(uri); - disposable.dispose(); // Clean up the event listener - resolve(diagnostics); - } - }); - - // Optionally, add a timeout in case diagnostics don't update within a reasonable time - setTimeout(() => { - disposable.dispose(); - reject(new Error('Timeout waiting for diagnostics update')); - }, 5000); // Adjust the timeout as needed - }); -} - export function filePathToUri(relativePath: string): vscode.Uri { const filePath = path.join(testWorkspaceDir, relativePath); const fileUri = vscode.Uri.file(filePath); diff --git a/src/test/workspace/GscCompletionItemProvider.test.ts b/src/test/workspace/GscCompletionItemProvider.test.ts index 8bc0003..3f3df97 100644 --- a/src/test/workspace/GscCompletionItemProvider.test.ts +++ b/src/test/workspace/GscCompletionItemProvider.test.ts @@ -39,10 +39,10 @@ suite('GscCompletionItemProvider', () => { // Check if local functions appears in completion list test(`Local functions`, async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscCompletionItemProvider', 'localFunctions.gsc']); + const gsc = await tests.loadGscFile(['GscCompletionItemProvider', 'localFunctions.gsc']); // There should be no error - everything is case insensitive - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); const completions = await GscCompletionItemProvider.getCompletionItems(gsc, new vscode.Position(1, 0), GscGame.UniversalGame, cfgFunctionsOnly); const doc = tests.getFunctionDescription; @@ -58,10 +58,10 @@ suite('GscCompletionItemProvider', () => { // Check if included functions appears in completion list test(`Included functions`, async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscCompletionItemProvider', 'include.gsc']); + const gsc = await tests.loadGscFile(['GscCompletionItemProvider', 'include.gsc']); // There should be no error - everything is case insensitive - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); const completions = await GscCompletionItemProvider.getCompletionItems(gsc, new vscode.Position(3, 0), GscGame.UniversalGame, cfgFunctionsOnly); const doc = tests.getFunctionDescription; @@ -76,10 +76,10 @@ suite('GscCompletionItemProvider', () => { // Check if included functions appears in completion list test(`Included functions from included workspace folder`, async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscCompletionItemProvider', 'includeWorkspaceFolder.gsc']); + const gsc = await tests.loadGscFile(['GscCompletionItemProvider', 'includeWorkspaceFolder.gsc']); // There should be no error - everything is case insensitive - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); const completions = await GscCompletionItemProvider.getCompletionItems(gsc, new vscode.Position(3, 0), GscGame.UniversalGame, cfgFunctionsOnly); const doc = tests.getFunctionDescription; @@ -95,12 +95,12 @@ suite('GscCompletionItemProvider', () => { // Check if level. variable appears in completion list test(`Variables level.`, async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscCompletionItemProvider', 'variablesLevel.gsc']); + const gsc = await tests.loadGscFile(['GscCompletionItemProvider', 'variablesLevel.gsc']); // - tests.checkDiagnostic(diagnostics, 0, "Unexpected token level", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 1, "Unexpected token", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 2); + tests.checkDiagnostic(gsc.diagnostics, 0, "Unexpected token level", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 1, "Unexpected token", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 2); const completions = await GscCompletionItemProvider.getCompletionItems(gsc, new vscode.Position(3, 10), GscGame.UniversalGame, cfgVariablesOnly, gsc.uri); diff --git a/src/test/workspace/GscComplex.test.ts b/src/test/workspace/GscComplex.test.ts index 6adbc07..76cc3be 100644 --- a/src/test/workspace/GscComplex.test.ts +++ b/src/test/workspace/GscComplex.test.ts @@ -19,10 +19,10 @@ suite('GscComplex', () => { // Check case insensitivity of function calls (file paths, function names) test('GscComplex.FunctionReferences -> error + hover + definition', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscComplex', 'FunctionReferences.gsc']); + const gsc = await tests.loadGscFile(['GscComplex', 'FunctionReferences.gsc']); // There should be no error - everything is case insensitive - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); // Correct path // FunctionReferencesFolder\FunctionReferencesFile::funcName(); diff --git a/src/test/workspace/GscDiagnosticsCollection.test.ts b/src/test/workspace/GscDiagnosticsCollection.test.ts index 89525c7..cc19253 100644 --- a/src/test/workspace/GscDiagnosticsCollection.test.ts +++ b/src/test/workspace/GscDiagnosticsCollection.test.ts @@ -15,66 +15,66 @@ suite('GscDiagnosticsCollection.CoD2MP', () => { }); test('GscDiagnosticsCollection.CoD2MP.casting', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'casting.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'casting.gsc']); - tests.checkDiagnostic(diagnostics, 0, "Casting to data type is not supported for CoD2 MP", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 1); + tests.checkDiagnostic(gsc.diagnostics, 0, "Casting to data type is not supported for CoD2 MP", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 1); }); test('GscDiagnosticsCollection.CoD2MP.functionOverwriting', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionOverwriting.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionOverwriting.gsc']); - tests.checkDiagnostic(diagnostics, 0, "Function 'spawn' is overwriting build-in function", vscode.DiagnosticSeverity.Information); - tests.checkDiagnostic(diagnostics, 1, "Function 'hide' is overwriting build-in function", vscode.DiagnosticSeverity.Information); - assert.ok(diagnostics.length === 2); + tests.checkDiagnostic(gsc.diagnostics, 0, "Function 'spawn' is overwriting build-in function", vscode.DiagnosticSeverity.Information); + tests.checkDiagnostic(gsc.diagnostics, 1, "Function 'hide' is overwriting build-in function", vscode.DiagnosticSeverity.Information); + assert.ok(gsc.diagnostics.length === 2); }); test('GscDiagnosticsCollection.CoD2MP.functionParameters', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionParameters.gsc']); - - tests.checkDiagnostic(diagnostics, 0, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 1, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 2, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 3, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 4, "Function 'aCos' can not be called on object (does not support callon object)", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 5, "Function 'getAmmoCount' must be called on object (callon object is missing)", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 6, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 7, "Function 'spawn' expect min 2 parameters, got 1", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 8, "Function 'spawn' expect max 5 parameters, got 6", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 9, "Function 'spawn' expect max 5 parameters, got 7", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 10, "Function 'spawn' expect max 5 parameters, got 8", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 11); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionParameters.gsc']); + + tests.checkDiagnostic(gsc.diagnostics, 0, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 1, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 2, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 3, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 4, "Function 'aCos' can not be called on object (does not support callon object)", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 5, "Function 'getAmmoCount' must be called on object (callon object is missing)", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 6, "Function 'spawn' expect min 2 parameters, got 0", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 7, "Function 'spawn' expect min 2 parameters, got 1", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 8, "Function 'spawn' expect max 5 parameters, got 6", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 9, "Function 'spawn' expect max 5 parameters, got 7", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 10, "Function 'spawn' expect max 5 parameters, got 8", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 11); }); test('GscDiagnosticsCollection.CoD2MP.functionParametersLocal', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionParametersLocal.gsc']); - - tests.checkDiagnostic(diagnostics, 0, "Function 'p0' does not expect any parameters, got 1", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 1, "Function 'p0' does not expect any parameters, got 2", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 2, "Function 'p1' expect 1 parameter, got 2", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 3, "Function 'p2' expect 2 parameters, got 3", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 4, "Function 'p2' expect 2 parameters, got 5", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 5); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionParametersLocal.gsc']); + + tests.checkDiagnostic(gsc.diagnostics, 0, "Function 'p0' does not expect any parameters, got 1", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 1, "Function 'p0' does not expect any parameters, got 2", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 2, "Function 'p1' expect 1 parameter, got 2", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 3, "Function 'p2' expect 2 parameters, got 3", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 4, "Function 'p2' expect 2 parameters, got 5", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 5); }); test('GscDiagnosticsCollection.CoD2MP.functionUndefined', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionUndefined.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'functionUndefined.gsc']); - tests.checkDiagnostic(diagnostics, 0, "Function 'unknownLocalFunction' is not defined!", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 1, "Function 'func2' is not defined in 'functionUndefinedFuncs.gsc'!", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 2); + tests.checkDiagnostic(gsc.diagnostics, 0, "Function 'unknownLocalFunction' is not defined!", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 1, "Function 'func2' is not defined in 'functionUndefinedFuncs.gsc'!", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 2); }); test('GscDiagnosticsCollection.CoD2MP.invalidFile', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'invalidFile.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.CoD2MP', 'invalidFile.gsc']); - tests.checkDiagnostic(diagnostics, 0, "File 'unknown_path\\script.gsc' was not found in workspace folder 'GscDiagnosticsCollection.CoD2MP'", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 1); + tests.checkDiagnostic(gsc.diagnostics, 0, "File 'unknown_path\\script.gsc' was not found in workspace folder 'GscDiagnosticsCollection.CoD2MP'", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 1); }); }); @@ -90,49 +90,49 @@ suite('GscDiagnosticsCollection.UniversalGame', () => { }); test('GscDiagnosticsCollection.UniversalGame.casting', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'casting.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'casting.gsc']); - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); }); test('GscDiagnosticsCollection.UniversalGame.functionOverwriting', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionOverwriting.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionOverwriting.gsc']); - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); }); test('GscDiagnosticsCollection.UniversalGame.functionParameters', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionParameters.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionParameters.gsc']); - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); }); test('GscDiagnosticsCollection.CoD2MP.functionParametersLocal', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionParametersLocal.gsc']); - - tests.checkDiagnostic(diagnostics, 0, "Function 'p0' does not expect any parameters, got 1", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 1, "Function 'p0' does not expect any parameters, got 2", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 2, "Function 'p1' expect 1 parameter, got 2", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 3, "Function 'p2' expect 2 parameters, got 3", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 4, "Function 'p2' expect 2 parameters, got 5", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 5); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionParametersLocal.gsc']); + + tests.checkDiagnostic(gsc.diagnostics, 0, "Function 'p0' does not expect any parameters, got 1", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 1, "Function 'p0' does not expect any parameters, got 2", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 2, "Function 'p1' expect 1 parameter, got 2", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 3, "Function 'p2' expect 2 parameters, got 3", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 4, "Function 'p2' expect 2 parameters, got 5", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 5); }); test('GscDiagnosticsCollection.UniversalGame.functionUndefined', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionUndefined.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'functionUndefined.gsc']); - tests.checkDiagnostic(diagnostics, 0, "Function 'func2' is not defined in 'functionUndefinedFuncs.gsc'!", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 1); + tests.checkDiagnostic(gsc.diagnostics, 0, "Function 'func2' is not defined in 'functionUndefinedFuncs.gsc'!", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 1); }); test('GscDiagnosticsCollection.UniversalGame.invalidFile', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'invalidFile.gsc']); + const gsc = await tests.loadGscFile(['GscDiagnosticsCollection.UniversalGame', 'invalidFile.gsc']); - tests.checkDiagnostic(diagnostics, 0, "File 'unknown_path\\script.gsc' was not found in workspace folder 'GscDiagnosticsCollection.UniversalGame'", vscode.DiagnosticSeverity.Error); - assert.ok(diagnostics.length === 1); + tests.checkDiagnostic(gsc.diagnostics, 0, "File 'unknown_path\\script.gsc' was not found in workspace folder 'GscDiagnosticsCollection.UniversalGame'", vscode.DiagnosticSeverity.Error); + assert.ok(gsc.diagnostics.length === 1); }); }); diff --git a/src/test/workspace/GscQuickFix.test.ts b/src/test/workspace/GscQuickFix.test.ts index fee8002..2105c41 100644 --- a/src/test/workspace/GscQuickFix.test.ts +++ b/src/test/workspace/GscQuickFix.test.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode'; import assert from 'assert'; import * as tests from '../Tests.test'; +import { GscDiagnosticsCollection } from '../../GscDiagnosticsCollection'; +import { LoggerOutput } from '../../LoggerOutput'; @@ -14,10 +16,10 @@ suite('GscQuickFix', () => { test('func references', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscQuickFix', 'includedFolders.gsc']); + const gsc = await tests.loadGscFile(['GscQuickFix', 'includedFolders.gsc']); // There should be no error - assert.ok(diagnostics.length === 0); + assert.ok(gsc.diagnostics.length === 0); // file1::func1(); const pos1 = new vscode.Position(2, 14); @@ -47,14 +49,14 @@ suite('GscQuickFix', () => { test('command "add workspace folder" + "game root" + "ignore folder" + "ignore errors"', async () => { - const [gsc, diagnostics] = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); + const gsc = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); - tests.checkDiagnostic(diagnostics, 0, "File 'quickFix\\quickFixFile1.gsc' was not found in workspace folder 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 1, "File 'quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics, 2, "File 'quickFix\\quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); - assert.strictEqual(diagnostics.length, 3); + tests.checkDiagnostic(gsc.diagnostics, 0, "File 'quickFix\\quickFixFile1.gsc' was not found in workspace folder 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 1, "File 'quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc.diagnostics, 2, "File 'quickFix\\quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc.diagnostics.length, 3); - const fixes1 = await tests.getQuickFixesForDiagnostic(diagnostics, 0, gsc.uri); + const fixes1 = await tests.getQuickFixesForDiagnostic(gsc.diagnostics, 0, gsc.uri); tests.checkQuickFix(fixes1, 0, "Add workspace folder \"GscQuickFix.4\" for file references (workspace settings)"); tests.checkQuickFix(fixes1, 1, "Choose folder for file references..."); tests.checkQuickFix(fixes1, 2, "Ignore file \"quickFix\\quickFixFile1\" (workspace settings)"); @@ -63,7 +65,7 @@ suite('GscQuickFix', () => { tests.checkQuickFix(fixes1, 5, 'Disable all error diagnostics for workspace folder "GscQuickFix" (workspace settings)'); assert.strictEqual(fixes1.length, 6); - const fixes2 = await tests.getQuickFixesForDiagnostic(diagnostics, 1, gsc.uri); + const fixes2 = await tests.getQuickFixesForDiagnostic(gsc.diagnostics, 1, gsc.uri); tests.checkQuickFix(fixes2, 0, 'Add workspace folder "GscQuickFix.4" for file references and change game root folder to "quickfix" (workspace settings)'); tests.checkQuickFix(fixes2, 1, 'Add workspace folder "GscQuickFix.4" for file references and change game root folder to "subfolder/quickfix" (workspace settings)'); tests.checkQuickFix(fixes2, 2, "Choose folder for file references..."); @@ -72,7 +74,7 @@ suite('GscQuickFix', () => { tests.checkQuickFix(fixes2, 5, 'Disable all error diagnostics for workspace folder "GscQuickFix" (workspace settings)'); assert.strictEqual(fixes2.length, 6); - const fixes3 = await tests.getQuickFixesForDiagnostic(diagnostics, 2, gsc.uri); + const fixes3 = await tests.getQuickFixesForDiagnostic(gsc.diagnostics, 2, gsc.uri); tests.checkQuickFix(fixes3, 0, 'Add workspace folder "GscQuickFix.4" for file references (workspace settings)'); tests.checkQuickFix(fixes3, 1, 'Add workspace folder "GscQuickFix.4" for file references and change game root folder to "subfolder" (workspace settings)'); tests.checkQuickFix(fixes2, 2, "Choose folder for file references..."); @@ -87,15 +89,18 @@ suite('GscQuickFix', () => { // Execute: Add workspace folder \"GscQuickFix.4\" for file references (workspace settings) await vscode.commands.executeCommand(fixes1[0].command!.command, ...fixes1[0].command?.arguments || []); + LoggerOutput.log("1. Executed command: " + fixes1[0].command?.command); await tests.sleep(100); //////////////////////// - const [gsc2, diagnostics2] = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); + + const gsc2 = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); + - tests.checkDiagnostic(diagnostics2, 0, "File 'quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.4', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); - assert.strictEqual(diagnostics2.length, 1); + tests.checkDiagnostic(gsc2.diagnostics, 0, "File 'quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.4', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc2.diagnostics.length, 1); - const fixes1_2 = await tests.getQuickFixesForDiagnostic(diagnostics2, 0, gsc2.uri); + const fixes1_2 = await tests.getQuickFixesForDiagnostic(gsc2.diagnostics, 0, gsc2.uri); tests.checkQuickFix(fixes1_2, 0, 'Change game root folder to "quickfix" for workspace folder "GscQuickFix.4" (workspace settings)'); tests.checkQuickFix(fixes1_2, 1, 'Change game root folder to "subfolder/quickfix" for workspace folder "GscQuickFix.4" (workspace settings)'); tests.checkQuickFix(fixes1_2, 2, "Choose folder for file references..."); @@ -110,14 +115,15 @@ suite('GscQuickFix', () => { // Execute: Add workspace folder "Change game root folder to "subfolder/quickfix" for workspace folder "GscQuickFix.4" (workspace settings) await vscode.commands.executeCommand(fixes1_2[1].command!.command, ...fixes1_2[1].command?.arguments || []); + LoggerOutput.log("2. Executed command: " + fixes1_2[1].command?.command); await tests.sleep(100); //////////////////////// - const [gsc3, diagnostics3] = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); + const gsc3 = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); - tests.checkDiagnostic(diagnostics3, 0, "File 'quickFix\\quickFixFile1.gsc' was not found in workspace folder 'GscQuickFix.4/subfolder/quickfix', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); - tests.checkDiagnostic(diagnostics3, 1, "File 'quickFix\\quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.4/subfolder/quickfix', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); - assert.strictEqual(diagnostics3.length, 2); + tests.checkDiagnostic(gsc3.diagnostics, 0, "File 'quickFix\\quickFixFile1.gsc' was not found in workspace folder 'GscQuickFix.4/subfolder/quickfix', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + tests.checkDiagnostic(gsc3.diagnostics, 1, "File 'quickFix\\quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.4/subfolder/quickfix', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc3.diagnostics.length, 2); @@ -126,28 +132,37 @@ suite('GscQuickFix', () => { // Execute: "Ignore file \"quickFix\\quickFixFile1\" (workspace settings)" await vscode.commands.executeCommand(fixes1[2].command!.command, ...fixes1[2].command?.arguments || []); + LoggerOutput.log("3. Executed command: " + fixes1_2[2].command?.command); await tests.sleep(100); //////////////////////// - const [gsc4, diagnostics4] = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); + const gsc4 = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); - tests.checkDiagnostic(diagnostics4, 0, "File 'quickFix\\quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.4/subfolder/quickfix', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); - assert.strictEqual(diagnostics4.length, 1); + tests.checkDiagnostic(gsc4.diagnostics, 0, "File 'quickFix\\quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.4/subfolder/quickfix', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc4.diagnostics.length, 1); - + // Check also the real diagnostics array + const realDiagnostics4 = GscDiagnosticsCollection.diagnosticCollection?.get(gsc4.uri); + assert.ok(realDiagnostics4 !== undefined); + tests.checkDiagnostic(realDiagnostics4, 0, "File 'quickFix\\quickFixFile2.gsc' was not found in workspace folder 'GscQuickFix.4/subfolder/quickfix', 'GscQuickFix.3', 'GscQuickFix.2/subfolder', 'GscQuickFix.1', 'GscQuickFix'", vscode.DiagnosticSeverity.Error); + assert.strictEqual(realDiagnostics4.length, 1); //////////////////////// // Execute: Disable error diagnostics for workspace folder "GscQuickFix" (workspace settings) await vscode.commands.executeCommand(fixes3[6].command!.command, ...fixes3[6].command?.arguments || []); + LoggerOutput.log("4. Executed command: " + fixes3[6].command?.command); await tests.sleep(100); //////////////////////// - const [gsc5, diagnostics5] = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc'], false); - - assert.strictEqual(diagnostics5.length, 0); + const gsc5 = await tests.loadGscFile(['GscQuickFix', 'includedFoldersCommand.gsc']); + assert.strictEqual(gsc5.diagnostics.length, 0); + + const realDiagnostics = GscDiagnosticsCollection.diagnosticCollection?.get(gsc5.uri); + assert.ok(realDiagnostics !== undefined); + assert.strictEqual(realDiagnostics?.length, 0); //await tests.sleep(100000);