diff --git a/src/Events.ts b/src/Events.ts new file mode 100644 index 0000000..3fe4276 --- /dev/null +++ b/src/Events.ts @@ -0,0 +1,162 @@ +import * as vscode from 'vscode'; +import { GscConfig } from './GscConfig'; +import { GscFiles } from './GscFiles'; +import { GscStatusBar } from './GscStatusBar'; +import { GscFile } from './GscFile'; +import { LoggerOutput } from './LoggerOutput'; +import { Issues } from './Issues'; + +export class Events { + + static activate(context: vscode.ExtensionContext) { + + + // An event that is emitted when the configuration changed. + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async (e) => { + try { + LoggerOutput.log("[Events] Configuration changed."); + + await GscConfig.onDidChangeConfiguration(e); + + } catch (error) { + Issues.handleError(error); + } + })); + + + // An event that is emitted when a workspace folder is added or removed. + //**Note:** this event will not fire if the first workspace folder is added, removed or changed, + // because in that case the currently executing extensions (including the one that listens to this + // event) will be terminated and restarted so that the (deprecated) `rootPath` property is updated + // to point to the first workspace folder. + context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(async e => { + try { + LoggerOutput.log("[Events] Workspace folders changed."); + + await GscFiles.onChangeWorkspaceFolders(e); + + } catch (error) { + Issues.handleError(error); + } + })); + + + var debouncedTimerOnDidChangeActiveTextEditor: NodeJS.Timeout | undefined = undefined; + // An event which fires when the window.activeTextEditor active editor has changed. + // *Note* that the event also fires when the active editor changes to `undefined`. + context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(async (e) => { + try { + const uri = e?.document.uri; + const uriStr = uri ? vscode.workspace.asRelativePath(uri) : "undefined"; + + LoggerOutput.log("[Events] Active editor changed to " + uriStr + ", debouncing update..."); + + // Debounce the update + if (debouncedTimerOnDidChangeActiveTextEditor) { + clearTimeout(debouncedTimerOnDidChangeActiveTextEditor); + } + debouncedTimerOnDidChangeActiveTextEditor = setTimeout(async () => { + debouncedTimerOnDidChangeActiveTextEditor = undefined; + + LoggerOutput.log("[Events] Debounce done (250ms) - Active editor changed to " + e?.document.fileName); + + await GscStatusBar.updateStatusBar("activeEditorChanged"); + }, 250); + + } catch (error) { + Issues.handleError(error); + } + })); + + + // An event that is emitted when a text document is changed. This usually happens + // when the contents changes but also when other things like the dirty-state changes. + vscode.workspace.onDidChangeTextDocument(e => { + try { + //LoggerOutput.log("[Events] Text document changed."); + + GscFiles.onTextDocumentChange(e); + } catch (error) { + Issues.handleError(error); + } + }, this, context.subscriptions); + + + + // An event which fires when the selection in an editor has changed. + context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(e => { + try { + //LoggerOutput.log("[Events] Editor selection changed."); + + GscFiles.onChangeEditorSelection(e); + } catch (error) { + Issues.handleError(error); + } + })); + + + // File rename + context.subscriptions.push(vscode.workspace.onDidRenameFiles(async e => { + try { + LoggerOutput.log("[Events] File has been renamed. " + e.files.map(f => vscode.workspace.asRelativePath(f.oldUri) + " -> " + vscode.workspace.asRelativePath(f.newUri)).join(", ")); + + } catch (error) { + Issues.handleError(error); + } + })); + + // File delete + context.subscriptions.push(vscode.workspace.onDidDeleteFiles(async e => { + try { + LoggerOutput.log("[Events] File has been deleted. " + e.files.map(f => vscode.workspace.asRelativePath(f)).join(", ")); + + } catch (error) { + Issues.handleError(error); + } + })); + + // File create + context.subscriptions.push(vscode.workspace.onDidCreateFiles(async e => { + try { + LoggerOutput.log("[Events] File has been created. " + e.files.map(f => vscode.workspace.asRelativePath(f)).join(", ")); + + } catch (error) { + Issues.handleError(error); + } + })); + } + + + + + + private static readonly onDidGscFileParsedEvent = new vscode.EventEmitter(); + public static readonly onDidGscFileParsed = this.onDidGscFileParsedEvent.event; + + + static GscFileParsed(gscFile: GscFile) { + LoggerOutput.log("[Events] GSC file parsed", vscode.workspace.asRelativePath(gscFile.uri)); + + this.onDidGscFileParsedEvent.fire(gscFile); + } + + + + + + + private static readonly onDidGscDiagnosticChangeEvent = new vscode.EventEmitter(); + public static readonly onDidGscDiagnosticChange = this.onDidGscDiagnosticChangeEvent.event; + + public static GscDiagnosticsHasChanged(gscFile: GscFile) { + LoggerOutput.log("[Events] GSC diagnostics changed for file", vscode.workspace.asRelativePath(gscFile.uri)); + + this.onDidGscDiagnosticChangeEvent.fire(gscFile); + } + + + static GscFileCacheFileHasChanged(fileUri: vscode.Uri) { + LoggerOutput.log("[Events] GSC cache changed for file", vscode.workspace.asRelativePath(fileUri)); + + } +} \ No newline at end of file diff --git a/src/Gsc.ts b/src/Gsc.ts index 08f2a7c..1bb2369 100644 --- a/src/Gsc.ts +++ b/src/Gsc.ts @@ -9,36 +9,35 @@ import { GscStatusBar } from './GscStatusBar'; import { GscConfig } from './GscConfig'; import { GscCodeActionProvider } from './GscCodeActionProvider'; import { Issues } from './Issues'; +import { LoggerOutput } from './LoggerOutput'; export class Gsc { static async activate(context: vscode.ExtensionContext) { - console.log("------------------------------------------------------------"); - console.log("- GSC extension activated -"); - console.log("------------------------------------------------------------"); + console.log("##### GSC extension activated #####"); + LoggerOutput.log("[Gsc] Extension activated"); // Register events try { await GscConfig.activate(context); - await GscStatusBar.activate(context); - await GscFiles.activate(context); await GscDiagnosticsCollection.activate(context); + await GscFiles.activate(context); await GscCodeActionProvider.activate(context); await GscSemanticTokensProvider.activate(context); await GscCompletionItemProvider.activate(context); await GscDefinitionProvider.activate(context); await GscHoverProvider.activate(context); + await GscStatusBar.activate(context); } catch (error) { Issues.handleError(error); } } static deactivate() { - console.log("------------------------------------------------------------"); - console.log("- GSC extension deactivated -"); - console.log("------------------------------------------------------------"); + console.log("##### GSC extension deactivated #####"); + LoggerOutput.log("[Gsc] Extension deactivating"); GscFiles.deactivate(); } diff --git a/src/GscCodeActionProvider.ts b/src/GscCodeActionProvider.ts index fa51c31..75b350e 100644 --- a/src/GscCodeActionProvider.ts +++ b/src/GscCodeActionProvider.ts @@ -2,10 +2,13 @@ import * as vscode from 'vscode'; import { ConfigErrorDiagnostics, GscConfig } from './GscConfig'; import * as path from 'path'; import { Issues } from './Issues'; +import { LoggerOutput } from './LoggerOutput'; export class GscCodeActionProvider implements vscode.CodeActionProvider { - static async activate(context: vscode.ExtensionContext) { + static async activate(context: vscode.ExtensionContext) { + LoggerOutput.log("[GscCodeActionProvider] Activating"); + vscode.languages.registerCodeActionsProvider('gsc', new GscCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }); diff --git a/src/GscCompletionItemProvider.ts b/src/GscCompletionItemProvider.ts index f13aa40..c0f041e 100644 --- a/src/GscCompletionItemProvider.ts +++ b/src/GscCompletionItemProvider.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; -import { GscFile, GscFiles } from './GscFiles'; -import { GroupType, GscData, GscGroup, GscVariableDefinitionType } from './GscFileParser'; +import { GscFiles } from './GscFiles'; +import { GscFile } from './GscFile'; +import { GroupType, GscGroup, GscVariableDefinitionType } from './GscFileParser'; import { CodFunctions } from './CodFunctions'; import { GscConfig, GscGame } from './GscConfig'; import { GscFunctions, GscVariableDefinition } from './GscFunctions'; @@ -17,7 +18,9 @@ export interface CompletionConfig { export class GscCompletionItemProvider implements vscode.CompletionItemProvider { - static async activate(context: vscode.ExtensionContext) { + static async activate(context: vscode.ExtensionContext) { + LoggerOutput.log("[GscCompletionItemProvider] Activating"); + context.subscriptions.push(vscode.languages.registerCompletionItemProvider('gsc', new GscCompletionItemProvider(), '\\', '.', '[', ']')); } @@ -28,9 +31,9 @@ export class GscCompletionItemProvider implements vscode.CompletionItemProvider // This function is called when user types a character or presses ctrl+space // Get parsed file - const gscFile = await GscFiles.getFileData(document.uri); + const gscFile = await GscFiles.getFileData(document.uri, true, "provide completion items"); - const currentGame = GscConfig.getSelectedGame(document.uri); + const currentGame = gscFile.config.currentGame; const items = await GscCompletionItemProvider.getCompletionItems(gscFile, position, currentGame, undefined, document.uri); diff --git a/src/GscConfig.ts b/src/GscConfig.ts index eb4bcee..a16a2d9 100644 --- a/src/GscConfig.ts +++ b/src/GscConfig.ts @@ -1,5 +1,8 @@ import * as vscode from 'vscode'; import { LoggerOutput } from './LoggerOutput'; +import { GscDiagnosticsCollection } from './GscDiagnosticsCollection'; +import { GscFiles } from './GscFiles'; +import { GscStatusBar } from './GscStatusBar'; import { Issues } from './Issues'; // These must match with package.json settings @@ -106,7 +109,7 @@ export class GscConfig { static async activate(context: vscode.ExtensionContext) { - vscode.workspace.onDidChangeConfiguration((e) => this.onDidChangeConfiguration(e), null, context.subscriptions); + LoggerOutput.log("[GscConfig] Activating"); } @@ -114,39 +117,22 @@ export class GscConfig { * Handle vscode configuration change event. * Emit a configuration change event. This will call all subscribers in the order they were added. */ - private static async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) { + public static async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) { if (e.affectsConfiguration('gsc')) { LoggerOutput.log("[GscConfig] GSC configuration changed."); - - for (const handler of this.configChangeSubscribers) { - try { - const result = handler(); - if (result instanceof Promise) { - await result; - } - } catch (error) { - Issues.handleError(error); - } - } - } - } + // 1. Load new configuration for each workspace and assign it to cached files + GscFiles.updateConfigurationOfCachedFiles(); - /** - * 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): vscode.Disposable { - this.configChangeSubscribers.push(handler); - return vscode.Disposable.from({ - dispose: () => { - const index = this.configChangeSubscribers.indexOf(handler); - if (index > -1) { - this.configChangeSubscribers.splice(index, 1); - } - } - }); - } + // 2. Update tree view + + // 3. Update status bar in case the game has changed + await GscStatusBar.updateStatusBar("configChanged"); + + // 4. Update diagnostics for all files with new configuration + await GscDiagnosticsCollection.updateDiagnosticsForAll("config changed"); + } + } diff --git a/src/GscDefinitionProvider.ts b/src/GscDefinitionProvider.ts index 3b1b817..12c603f 100644 --- a/src/GscDefinitionProvider.ts +++ b/src/GscDefinitionProvider.ts @@ -1,12 +1,16 @@ import * as vscode from 'vscode'; -import { GscFile, GscFiles } from './GscFiles'; -import { GroupType, GscData, GscFileParser } from './GscFileParser'; +import { GscFiles } from './GscFiles'; +import { GscFile } from './GscFile'; +import { GroupType } from './GscFileParser'; import { GscFunctions } from './GscFunctions'; import { Issues } from './Issues'; +import { LoggerOutput } from './LoggerOutput'; export class GscDefinitionProvider implements vscode.DefinitionProvider { static async activate(context: vscode.ExtensionContext) { + LoggerOutput.log("[GscDefinitionProvider] Activating"); + context.subscriptions.push(vscode.languages.registerDefinitionProvider('gsc', new GscDefinitionProvider())); } @@ -18,7 +22,7 @@ export class GscDefinitionProvider implements vscode.DefinitionProvider { { try { // Get parsed file - const gscFile = await GscFiles.getFileData(document.uri); + const gscFile = await GscFiles.getFileData(document.uri, false, "provide definition"); const locations = await GscDefinitionProvider.getFunctionDefinitionLocations(gscFile, position); diff --git a/src/GscDiagnosticsCollection.ts b/src/GscDiagnosticsCollection.ts index ef35ebe..5881a08 100644 --- a/src/GscDiagnosticsCollection.ts +++ b/src/GscDiagnosticsCollection.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; -import { GscFile, GscFiles } from './GscFiles'; +import { GscFiles } from './GscFiles'; +import { GscFile } from './GscFile'; import { GroupType, GscData, GscGroup, GscToken, TokenType } from './GscFileParser'; import { CodFunctions } from './CodFunctions'; import { ConfigErrorDiagnostics, GscConfig, GscGame, GscGameRootFolder } from './GscConfig'; @@ -7,6 +8,7 @@ import { GscFunctions, GscFunctionState } from './GscFunctions'; import { assert } from 'console'; import { LoggerOutput } from './LoggerOutput'; import { Issues } from './Issues'; +import { Events } from './Events'; type DiagnosticsUpdateHandler = (gscFile: GscFile) => Promise | void; @@ -15,10 +17,8 @@ export class GscDiagnosticsCollection { private static statusBarItem: vscode.StatusBarItem | undefined; private static currentCancellationTokenSource: vscode.CancellationTokenSource | null = null; - private static diagnosticsUpdateSubscribers: DiagnosticsUpdateHandler[] = []; - - static async activate(context: vscode.ExtensionContext) { + LoggerOutput.log("[GscDiagnosticsCollection] Activating"); // Create a status bar item to show background task indicator this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); @@ -33,45 +33,14 @@ export class GscDiagnosticsCollection { // Refresh command context.subscriptions.push(vscode.commands.registerCommand('gsc.refreshDiagnosticsCollection', () => this.refreshDiagnosticsCollection())); - - // Settings changed, handle it... - GscConfig.onDidConfigChange(async () => await this.onDidConfigChange()); - } - - - /** - * Subscribe to diagnostics collection updates. - * The handler will be called whenever the diagnostics collection for any files is updated. - * Subscribers are called in the order they were added and they are not awaited. - * @param handler - */ - public static onDidDiagnosticsChange(handler: DiagnosticsUpdateHandler): vscode.Disposable { - this.diagnosticsUpdateSubscribers.push(handler); - return vscode.Disposable.from({ - dispose: () => { - const index = this.diagnosticsUpdateSubscribers.indexOf(handler); - if (index > -1) { - this.diagnosticsUpdateSubscribers.splice(index, 1); - } - } - }); } - private static notifyDiagnosticsUpdateSubscribers(gscFile: GscFile) { - for (const handler of this.diagnosticsUpdateSubscribers) { - try { - void handler(gscFile); - } catch (error) { - Issues.handleError(error); - } - } - } /** * Update diagnostics for all parsed files. Since its computation intensive, its handled in async manner. */ - static async updateDiagnosticsAll(debugText: string) { + static async updateDiagnosticsForAll(debugText: string) { LoggerOutput.log("[GscDiagnosticsCollection] Creating diagnostics for all files", "because: " + debugText); @@ -106,7 +75,7 @@ export class GscDiagnosticsCollection { } try { - this.diagnosticCollection?.clear(); + this.deleteDiagnosticsAll(); // Clear all diagnostics for (const data of files) { if (token.isCancellationRequested) { @@ -118,7 +87,7 @@ export class GscDiagnosticsCollection { this.statusBarItem.tooltip = data.uri.toString(); } - count += await this.generateDiagnostics(data); + count += await this.updateDiagnosticsForFile(data); // Check if it's time to pause for UI update const now = Date.now(); @@ -154,7 +123,7 @@ export class GscDiagnosticsCollection { * @param gscFile The GSC file to generate diagnostics for. * @returns The number of diagnostics created. */ - static async generateDiagnostics(gscFile: GscFile): Promise { + public static async updateDiagnosticsForFile(gscFile: GscFile): Promise { try { LoggerOutput.log("[GscDiagnosticsCollection] Creating diagnostics for file", vscode.workspace.asRelativePath(gscFile.uri)); @@ -167,7 +136,7 @@ export class GscDiagnosticsCollection { if (gscFile.config.errorDiagnostics === ConfigErrorDiagnostics.Disable) { this.diagnosticCollection?.set(uri, gscFile.diagnostics); // Notify subscribers - this.notifyDiagnosticsUpdateSubscribers(gscFile); + Events.GscDiagnosticsHasChanged(gscFile); LoggerOutput.log("[GscDiagnosticsCollection] Done for file, diagnostics is disabled", vscode.workspace.asRelativePath(gscFile.uri)); return 0; } @@ -343,7 +312,7 @@ export class GscDiagnosticsCollection { // Notify subscribers - this.notifyDiagnosticsUpdateSubscribers(gscFile); + Events.GscDiagnosticsHasChanged(gscFile); LoggerOutput.log("[GscDiagnosticsCollection] Done for file, diagnostics created: " + gscFile.diagnostics.length, vscode.workspace.asRelativePath(gscFile.uri)); @@ -357,13 +326,16 @@ export class GscDiagnosticsCollection { } } + private static deleteDiagnosticsAll() { + this.diagnosticCollection?.clear(); + } - static deleteDiagnostics(uri: vscode.Uri) { + public static deleteDiagnosticsForFile(uri: vscode.Uri) { this.diagnosticCollection?.delete(uri); } - static createDiagnosticsForUnsolved(group: GscGroup, parentGroup: GscGroup, nextGroup: GscGroup | undefined) { + private static createDiagnosticsForUnsolved(group: GscGroup, parentGroup: GscGroup, nextGroup: GscGroup | undefined) { // Not terminated statement if ( @@ -398,7 +370,7 @@ export class GscDiagnosticsCollection { } - static createDiagnosticsForIncludedPaths(group: GscGroup, gscFile: GscFile, includedPaths: string[]): vscode.Diagnostic | undefined { + private static createDiagnosticsForIncludedPaths(group: GscGroup, gscFile: GscFile, includedPaths: string[]): vscode.Diagnostic | undefined { assert(group.type === GroupType.Path); const tokensAsPath = group.getTokensAsString(); @@ -437,7 +409,7 @@ export class GscDiagnosticsCollection { } - static async createDiagnosticsForFunctionName( + private static async createDiagnosticsForFunctionName( group: GscGroup, gscFile: GscFile) { // Function declaration @@ -575,12 +547,8 @@ export class GscDiagnosticsCollection { } - private static async onDidConfigChange() { - await this.updateDiagnosticsAll("config changed"); - } - private static async refreshDiagnosticsCollection() { - await this.updateDiagnosticsAll("manual refresh"); + await this.updateDiagnosticsForAll("manual refresh"); } } diff --git a/src/GscFile.ts b/src/GscFile.ts new file mode 100644 index 0000000..1144604 --- /dev/null +++ b/src/GscFile.ts @@ -0,0 +1,78 @@ +import * as vscode from 'vscode'; +import { GscData } from "./GscFileParser"; +import { ConfigErrorDiagnostics, GscConfig, GscGame, GscGameConfig, GscGameRootFolder } from './GscConfig'; +import { GscWorkspaceFileData } from './GscFileCache'; + +/** + * This type holds workspace configuration for current GSC file to easily access configuration without actually reading it from config file. + * When configuration is changed, it is updated in all GscFile instances. + */ +export type GscFileConfig = { + /** All possible game root folders where GSC files can be found and referenced. */ + referenceableGameRootFolders: GscGameRootFolder[]; + /** Ignored function names */ + ignoredFunctionNames: string[]; + /** Ignored file paths */ + ignoredFilePaths: string[]; + /** Currently selected game */ + currentGame: GscGame; + /** Mode of diagnostics collection */ + errorDiagnostics: ConfigErrorDiagnostics; + /** Syntax configuration of the selected game */ + gameConfig: GscGameConfig; +}; + + + +export class GscFile { + + /** URI as lower-case string */ + id: string; + + /** URI of the file */ + uri: vscode.Uri; + + /** Configuration related to this file */ + config: GscFileConfig; + + /** Diagnostics generated for this file. @see GscFileDiagnostics.ts */ + diagnostics: vscode.Diagnostic[] = []; + + + + + constructor( + /** Parsed data */ + public data: GscData, + /** URI of the file */ + uri?: vscode.Uri, // might be undefined for tests + /** Workspace folder to which this file belongs to */ + public workspaceFolder?: vscode.WorkspaceFolder, + /** The version number of open document (it will strictly increase after each change, including undo/redo). If file is not open in editor, it will be -1 */ + public version: number = -1 + ) { + if (uri === undefined) { + uri = vscode.Uri.parse("file://undefined"); + } + this.id = uri.toString().toLowerCase(); + this.uri = uri; + + if (workspaceFolder !== undefined) { + this.config = GscWorkspaceFileData.getConfig(workspaceFolder); + } else { + this.config = { + referenceableGameRootFolders: [], + currentGame: GscGame.UniversalGame, + ignoredFunctionNames: [], + ignoredFilePaths: [], + errorDiagnostics: ConfigErrorDiagnostics.Enable, + gameConfig: GscConfig.gamesConfigs.get(GscGame.UniversalGame)! + }; + } + } + + updateData(data: GscData, version: number) { + this.data = data; + this.version = version; + } +} diff --git a/src/GscFileCache.ts b/src/GscFileCache.ts new file mode 100644 index 0000000..c9f5f5d --- /dev/null +++ b/src/GscFileCache.ts @@ -0,0 +1,135 @@ +import * as vscode from 'vscode'; +import { GscFile, GscFileConfig } from './GscFile'; +import { GscFiles } from './GscFiles'; +import { GscConfig } from './GscConfig'; +import { Events } from './Events'; +import { LoggerOutput } from './LoggerOutput'; + + + + +export class GscCachedFilesPerWorkspace { + + private cachedFilesPerWorkspace: Map = new Map(); + + createNewWorkspaceFileData(workspaceFolder: vscode.WorkspaceFolder): GscWorkspaceFileData { + const data = new GscWorkspaceFileData(workspaceFolder); + this.cachedFilesPerWorkspace.set(workspaceFolder.uri.toString(), data); + return data; + } + + getWorkspaceFileData(workspaceUri: vscode.Uri): GscWorkspaceFileData | undefined { + return this.cachedFilesPerWorkspace.get(workspaceUri.toString()); + } + + removeCachedFile(fileUri: vscode.Uri) { + // Get workspace folder where the file is located + const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri); + if (workspaceFolder === undefined) { + return; + } + let dataOfWorkspace = this.getWorkspaceFileData(workspaceFolder.uri); + if (dataOfWorkspace === undefined) { + return; + } + dataOfWorkspace.removeParsedFile(fileUri); + } + + removeWorkspaceFiles(workspaceUri: vscode.Uri) { + const workspaceData = this.getWorkspaceFileData(workspaceUri); + if (workspaceData === undefined) { + return false; + } + workspaceData.removeAllParsedFiles(); + + return this.cachedFilesPerWorkspace.delete(workspaceUri.toString()); + } + + getAllWorkspaces() { + return this.cachedFilesPerWorkspace.values(); + } + clear() { + this.cachedFilesPerWorkspace.clear(); + } + +} + +/** + * GSC files parsed in workspace folder + */ +export class GscWorkspaceFileData { + private parsedFiles: Map = new Map(); + + constructor( + public workspaceFolder: vscode.WorkspaceFolder + ) {} + + addParsedFile(gscFile: GscFile) { + if (!this.parsedFiles.has(gscFile.id)) { + LoggerOutput.log("[GscFileCache] Added file to cache", vscode.workspace.asRelativePath(gscFile.uri)); + } else { + LoggerOutput.log("[GscFileCache] Updated file in cache", vscode.workspace.asRelativePath(gscFile.uri)); + } + this.parsedFiles.set(gscFile.id, gscFile); + + Events.GscFileCacheFileHasChanged(gscFile.uri); + } + + getParsedFile(uri: vscode.Uri): GscFile | undefined { + const data = this.parsedFiles.get(uri.toString().toLowerCase()); + + return data; + } + + removeParsedFile(uri: vscode.Uri): boolean { + const removed = this.parsedFiles.delete(uri.toString().toLowerCase()); + + if (removed) { + LoggerOutput.log("[GscFileCache] Removed file from cache", vscode.workspace.asRelativePath(uri)); + + Events.GscFileCacheFileHasChanged(uri); + } else { + LoggerOutput.log("[GscFileCache] File not found in cache", vscode.workspace.asRelativePath(uri)); + } + + return removed; + } + + removeAllParsedFiles() { + const files = this.getAllParsedFileData(); + for (const file of files) { + this.removeParsedFile(file.uri); + } + } + + getAllParsedFileData(): GscFile[] { + return Array.from(this.parsedFiles.values()); + } + + updateConfiguration() { + const data = GscWorkspaceFileData.getConfig(this.workspaceFolder); + + // Loop all GscFile and update their configuration + for (const file of this.parsedFiles.values()) { + file.config.referenceableGameRootFolders = data.referenceableGameRootFolders; + file.config.currentGame = data.currentGame; + file.config.ignoredFunctionNames = data.ignoredFunctionNames; + file.config.ignoredFilePaths = data.ignoredFilePaths; + file.config.errorDiagnostics = data.errorDiagnostics; + file.config.gameConfig = data.gameConfig; + } + } + + static getConfig(workspaceFolder: vscode.WorkspaceFolder): GscFileConfig { + + // Get config for workspace folder + const referenceableGameRootFolders = GscFiles.getReferenceableGameRootFolders(workspaceFolder); + const currentGame = GscConfig.getSelectedGame(workspaceFolder.uri); + const ignoredFunctionNames = GscConfig.getIgnoredFunctionNames(workspaceFolder.uri); + const ignoredFilePaths = GscConfig.getIgnoredFilePaths(workspaceFolder.uri); + const errorDiagnostics = GscConfig.getErrorDiagnostics(workspaceFolder.uri); + const gameConfig = GscConfig.gamesConfigs.get(currentGame)!; + + return {referenceableGameRootFolders, currentGame, ignoredFunctionNames, ignoredFilePaths, errorDiagnostics, gameConfig}; + } +} diff --git a/src/GscFileParser.ts b/src/GscFileParser.ts index 9ff7c41..26a501c 100644 --- a/src/GscFileParser.ts +++ b/src/GscFileParser.ts @@ -3164,6 +3164,8 @@ export class GscData { /** Unique set of paths included via #include */ includes: Set = new Set(); content: string; + /** The version number of open document (it will strictly increase after each change, including undo/redo). If file is not open in editor, it will be -1 */ + contentVersion: number = -1; constructor(structure: GscGroup, content: string) { this.root = structure; diff --git a/src/GscFiles.ts b/src/GscFiles.ts index d9956b5..ba55cf3 100644 --- a/src/GscFiles.ts +++ b/src/GscFiles.ts @@ -1,11 +1,13 @@ import * as vscode from 'vscode'; -import { GscFileParser, GscData, GroupType } from './GscFileParser'; import * as fs from 'fs'; import * as path from 'path'; -import { ConfigErrorDiagnostics, GscConfig, GscGame, GscGameConfig, GscGameRootFolder } from './GscConfig'; +import { GscFileParser, GscData, GroupType } from './GscFileParser'; +import { GscCachedFilesPerWorkspace } from './GscFileCache'; +import { GscFile } from './GscFile'; +import { GscConfig, GscGameRootFolder } from './GscConfig'; import { LoggerOutput } from './LoggerOutput'; import { GscDiagnosticsCollection } from './GscDiagnosticsCollection'; -import { Issues } from './Issues'; +import { Events } from './Events'; /** * On startup scan every .gsc file, parse it, and save the result into memory. @@ -14,50 +16,49 @@ import { Issues } from './Issues'; */ export class GscFiles { - private static cachedFilesPerWorkspace: Map = new Map(); - - private static parseAllFiles = false; + private static cachedFiles: GscCachedFilesPerWorkspace = new GscCachedFilesPerWorkspace(); private static statusBarItem: vscode.StatusBarItem | undefined; private static debugWindow: vscode.WebviewPanel | undefined = undefined; + + private static fileWatcher: vscode.FileSystemWatcher; + static async activate(context: vscode.ExtensionContext) { - this.parseAllFiles = true; + LoggerOutput.log("[GscFiles] Activating"); // Create a status bar item to show background task indicator GscFiles.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); GscFiles.statusBarItem.text = "$(sync~spin) Parsing GSC files..."; GscFiles.statusBarItem.tooltip = "Background task in progress"; - GscFiles.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); // Example of using a theme color for error state - + GscFiles.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); // Example of using a theme color for error state context.subscriptions.push(GscFiles.statusBarItem); - context.subscriptions.push(vscode.workspace.onDidCreateFiles(this.onCreateFiles)); - context.subscriptions.push(vscode.workspace.onDidDeleteFiles(this.onDeleteFiles)); - 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))); - + // Commands context.subscriptions.push(vscode.commands.registerCommand('gsc.debugParsedGscFile', this.debugParsedGscFile)); context.subscriptions.push(vscode.commands.registerCommand('gsc.debugParsedGscFileStructure', this.debugParsedGscFileStructure)); context.subscriptions.push(vscode.commands.registerCommand('gsc.debugItemBeforeCursor', this.debugItemBeforeCursor)); 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))); + context.subscriptions.push(vscode.commands.registerCommand('gsc.parseAll', () => setTimeout(() => this.parseAllFiles(), 1))); + + // Handle file changes + this.handleFileChanges(context); - GscConfig.onDidConfigChange(async () => { await this.onDidConfigChange(); }); + // Parse all files on startup + await this.parseAllFiles(); // Restore the debug window if it was open in the last session if (context.globalState.get('debugWindowOpen')) { this.showDebugWindow(context); } - } static deactivate() { //console.log("Deactivating GscFiles"); this.closeDebugWindow(); + this.fileWatcher.dispose(); } @@ -72,40 +73,26 @@ export class GscFiles { * @param doParseNotify * @returns */ - public static async getFileData(fileUri: vscode.Uri, forceParsing: boolean = false, doParseNotify: boolean = true): Promise { + public static async getFileData(fileUri: vscode.Uri, forceParsing: boolean = false, reason: string): Promise { // Get workspace folder where the file is located const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri); - // This file is part of workspace - if (workspaceFolder !== undefined) { - // Run initial scan of all files if it is not done yet - if (this.parseAllFiles) { - this.parseAllFiles = false; - await this.initialParse(); - } - } - - const doLog = doParseNotify; - if (doLog) { - LoggerOutput.log("[GscFiles] Getting file data", vscode.workspace.asRelativePath(fileUri)); - } + LoggerOutput.log("[GscFiles] Getting parsed data of file... (" + reason + ")", vscode.workspace.asRelativePath(fileUri)); + // This file is not part of workspace, return it and do not cache it if (workspaceFolder === undefined) { 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()); - } + const gscFile = new GscFile(gsc, fileUri, workspaceFolder, gsc.version); + LoggerOutput.log("[GscFiles] Done (not part of workspace), version: " + gsc.version, fileUri.toString()); return gscFile; } // Get data of workspace that contains cached files - let dataOfWorkspace = this.cachedFilesPerWorkspace.get(workspaceFolder.uri.toString()); + let dataOfWorkspace = this.cachedFiles.getWorkspaceFileData(workspaceFolder.uri); if (dataOfWorkspace === undefined) { - dataOfWorkspace = new GscWorkspaceFileData(workspaceFolder); - this.cachedFilesPerWorkspace.set(workspaceFolder.uri.toString(), dataOfWorkspace); + dataOfWorkspace = this.cachedFiles.createNewWorkspaceFileData(workspaceFolder); } // Try to get cached file @@ -116,7 +103,12 @@ export class GscFiles { if (fileData === undefined) { // Parse the file and save it into cache const gsc = await this.parseFile(fileUri); - fileData = new GscFile(gsc, fileUri, workspaceFolder); + fileData = new GscFile(gsc, fileUri, workspaceFolder, gsc.version); + // If this is not valid gsc file, return it and do not cache it + if (!this.isValidGscFile(fileUri.fsPath)) { + LoggerOutput.log("[GscFiles] Done (not valid GSC file), version: " + gsc.version, fileUri.toString()); + return fileData; + } dataOfWorkspace.addParsedFile(fileData); bParsed = true; @@ -126,25 +118,19 @@ export class GscFiles { if (forceParsing) { // Parse the file and update the cache file data const gsc = await this.parseFile(fileUri); - fileData.updateData(gsc); + fileData.updateData(gsc, gsc.version); bParsed = true; - } else { - doParseNotify = false; // Do not notify because the files was not parsed } } // If opened, update debug window this.updateDebugCachedFilesWindow(); - - // When all files are being parsed, ignoreNotify is true - if (doParseNotify) { - await GscDiagnosticsCollection.generateDiagnostics(fileData); + if (bParsed) { + Events.GscFileParsed(fileData); } - if (doLog) { - LoggerOutput.log("[GscFiles] Done, " + (bParsed ? "was parsed" : "loaded from cache"), vscode.workspace.asRelativePath(fileUri)); - } + LoggerOutput.log("[GscFiles] Done, " + (bParsed ? "file was parsed" : "data loaded from cache") + ", version: " + fileData.version, vscode.workspace.asRelativePath(fileUri)); return fileData; } @@ -153,10 +139,10 @@ export class GscFiles { - public static async initialParse() { + public static async parseAllFiles() { if (GscFiles.statusBarItem) { GscFiles.statusBarItem.show(); - } + } this.removeAllCachedFiles(); @@ -171,27 +157,34 @@ export class GscFiles { } // Update diagnostics for all files - await GscDiagnosticsCollection.updateDiagnosticsAll("all files parsed"); - - // Notify all subscribers - this.notifyOnDidInitialParse(); + await GscDiagnosticsCollection.updateDiagnosticsForAll("all files parsed"); } /** - * Load all .gsc files opened in editor or found in workspace file system, parse them and save them into memory + * Load all .gsc files opened in editor or found in workspace file system, parse them and save them into memory. + * @param workspaceFolder Workspace folder where to search for GSC files. If not specified, all workspace folders are searched. */ - public static async parseAndCacheAllFiles() { - LoggerOutput.log("[GscFiles] Parsing all GSC files..."); + public static async parseAndCacheAllFiles(workspaceFolder?: vscode.WorkspaceFolder) { + + if (workspaceFolder) { + LoggerOutput.log("[GscFiles] Parsing all GSC files in workspace '" + workspaceFolder.name + "'..."); + } else { + LoggerOutput.log("[GscFiles] Parsing all GSC files in all workspaces..."); + } + const start = performance.now(); - // Find all GSC files in repository - var files = await vscode.workspace.findFiles('**/*.gsc'); + // Determine the search pattern based on the workspace folder + const searchPattern = workspaceFolder ? new vscode.RelativePattern(workspaceFolder, "**/*.gsc") : "**/*.gsc"; + + // Find all GSC files in the specified workspace folder or in the entire repository + var files = await vscode.workspace.findFiles(searchPattern); const poolSize = 4; // Number of files to parse concurrently let i = 0; const parseFile = async (file: vscode.Uri, index: number) => { - const gsc = await this.getFileData(file, true, false); + const gsc = await this.getFileData(file, true, "parsing all files"); if (GscFiles.statusBarItem) { GscFiles.statusBarItem.text = `$(sync~spin) Parsing GSC file ${index + 1}/${files.length}...`; GscFiles.statusBarItem.tooltip = file.fsPath; @@ -214,7 +207,7 @@ export class GscFiles { //this.debugParsedFiles(true); //console.log(this, "Files:", this.parsedFiles.size, "Total time:", elapsed, "Errors:", errors); - LoggerOutput.log("[GscFiles] All GSC files parsed, files: " + this.getCachedFiles().length + ", time: " + elapsed + "ms"); + LoggerOutput.log("[GscFiles] All GSC files parsed, files: " + files.length + ", time: " + elapsed + "ms"); } @@ -237,7 +230,7 @@ export class GscFiles { } } - let dataOfWorkspace = this.cachedFilesPerWorkspace.get(workspaceUri.toString()); + let dataOfWorkspace = this.cachedFiles.getWorkspaceFileData(workspaceUri); if (dataOfWorkspace === undefined) { return undefined; // Given workspace was not saved in memory yet, meaning no file in this workspace was parsed @@ -259,13 +252,13 @@ export class GscFiles { let allParsedFiles: GscFile[] = []; if (workspaceUris === undefined) { - for (const workspaceData of this.cachedFilesPerWorkspace.values()) { + for (const workspaceData of this.cachedFiles.getAllWorkspaces()) { allParsedFiles = allParsedFiles.concat(workspaceData.getAllParsedFileData()); } } else { for (const workspaceUri of workspaceUris) { - const workspaceData = this.cachedFilesPerWorkspace.get(workspaceUri.toString()); + const workspaceData = this.cachedFiles.getWorkspaceFileData(workspaceUri); if (workspaceData !== undefined) { allParsedFiles = allParsedFiles.concat(workspaceData.getAllParsedFileData()); } @@ -277,7 +270,7 @@ export class GscFiles { public static removeAllCachedFiles() { - this.cachedFilesPerWorkspace.clear(); + this.cachedFiles.clear(); // If opened, update debug window this.updateDebugCachedFilesWindow(); @@ -291,27 +284,30 @@ export class GscFiles { * @param fileUri Uri of file to parse * @returns Parsed data */ - public static async parseFile(fileUri: vscode.Uri): Promise { + public static async parseFile(fileUri: vscode.Uri): Promise { //console.log("Parsing " + vscode.workspace.asRelativePath(fileUri) + ""); - const start = performance.now(); + //const start = performance.now(); // Check if the file is opened in any editor const openedTextDocument = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === fileUri.toString()); var content: string; + var version: number; if (openedTextDocument) { content = openedTextDocument.getText(); + version = openedTextDocument.version; } else { const fileContent = await vscode.workspace.fs.readFile(fileUri); content = Buffer.from(fileContent).toString('utf8'); // Convert the Uint8Array content to a string + version = -1; } //const endLoading = performance.now(); try { const gscData = GscFileParser.parse(content); //const endParsing = performance.now(); //console.log(` total time: ${(endParsing - start).toFixed(1)}, loading: ${(endLoading - start).toFixed(1)}, parsing: ${(endParsing - endLoading).toFixed(1)}`); - return gscData; + return { ...gscData, version: version }; } catch (error) { const errorNew = new Error("Error while parsing file " + vscode.workspace.asRelativePath(fileUri) + ". " + error); throw errorNew; @@ -436,46 +432,239 @@ export class GscFiles { + private static debounceTimersDiagnostics: Map = new Map(); + + /** Update diagnostics collection of single file or all files, will happen after 250ms if not other call happens */ + private static updateDiagnosticsDebounced(uri: vscode.Uri | undefined, debugText: string, debounceTime: number = 250) { + const gscFile = uri ? this.getCachedFile(uri) : undefined; + + // Provided uri is not cached GSC file + if (uri && !gscFile) { + return; + } + const uriString = uri?.toString() ?? ""; + if (uri) { + LoggerOutput.log("Debouncing diagnostics update for file (" + debounceTime + "ms, " + debugText + ")", vscode.workspace.asRelativePath(uri)); + } else { + LoggerOutput.log("Debouncing diagnostics update for all (" + debounceTime + "ms, " + debugText + ")"); + } - static isValidGscFile(filePath: string): boolean { - return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile() && path.extname(filePath).toLowerCase() === '.gsc'; + // If updating all, cancel all previous timers + if (!uri) { + if (this.debounceTimersDiagnostics.size > 0) { + LoggerOutput.log("Debouncing canceling previous " + this.debounceTimersDiagnostics.size + " timers - diagnostics update for all (" + debounceTime + "ms, " + debugText + ")"); + for (const [key, value] of this.debounceTimersDiagnostics) { + clearTimeout(value); + } + this.debounceTimersDiagnostics.clear(); + } + } else { + // Cancel previous timer, if any + const existingTimer = this.debounceTimersDiagnostics.get(uriString); + if (existingTimer) { + if (uri) { + LoggerOutput.log("Debouncing canceled - diagnostics update for file (" + debounceTime + "ms, " + debugText + ")", vscode.workspace.asRelativePath(uri)); + } else { + LoggerOutput.log("Debouncing canceled - diagnostics update for all (" + debounceTime + "ms, " + debugText + ")"); + } + clearTimeout(existingTimer); + } + } + + if (gscFile) { + this.debounceTimersDiagnostics.set(uriString, setTimeout(() => { + LoggerOutput.log("Debouncing done (" + debounceTime + "ms elapsed) - diagnostics update for file (" + debugText + ")", vscode.workspace.asRelativePath(uri!)); + this.debounceTimersDiagnostics.delete(uriString); + + void GscDiagnosticsCollection.updateDiagnosticsForFile(gscFile); + }, debounceTime)); + } else { + this.debounceTimersDiagnostics.set("", setTimeout(() => { + LoggerOutput.log("Debouncing done (" + debounceTime + "ms elapsed) - diagnostics update for all (" + debugText + ")"); + this.debounceTimersDiagnostics.delete(""); + + void GscDiagnosticsCollection.updateDiagnosticsForAll(debugText); + }, debounceTime)); + } } - static onCreateFiles(e: vscode.FileCreateEvent) { - for(const file of e.files) { - if (!GscFiles.isValidGscFile(file.fsPath)) { - continue; + + + + private static debounceTimersParse: Map = new Map(); + + /** Parse single file or all files, will happen after 250ms if not other call happens */ + private static parseFileAndDiagnoseDebounced(uri: vscode.Uri | undefined, doAllDiagnosticUpdate: boolean, debugText: string, debounceTime: number = 250) { + + const uriString = uri?.toString() ?? ""; + + if (uri) { + LoggerOutput.log("Debouncing parse for file (" + debounceTime + "ms, " + debugText + ")", vscode.workspace.asRelativePath(uri)); + } else { + LoggerOutput.log("Debouncing parse for all (" + debounceTime + "ms, " + debugText + ")"); + } + + // If updating all, cancel all previous timers + if (!uri) { + if (this.debounceTimersParse.size > 0) { + LoggerOutput.log("Debouncing canceling previous " + this.debounceTimersDiagnostics.size + " timers - parse for all (" + debounceTime + "ms, " + debugText + ")"); + for (const [key, value] of this.debounceTimersParse) { + clearTimeout(value); + } + this.debounceTimersParse.clear(); + } + } else { + // Cancel previous timer, if any + const existingTimer = this.debounceTimersParse.get(uriString); + if (existingTimer) { + if (uri) { + LoggerOutput.log("Debouncing canceled - parse for file (" + debounceTime + "ms, " + debugText + ")", vscode.workspace.asRelativePath(uri)); + } else { + LoggerOutput.log("Debouncing canceled - parse for all (" + debounceTime + "ms, " + debugText + ")"); + } + clearTimeout(existingTimer); } - void GscFiles.getFileData(file); - LoggerOutput.log("[GscFiles] Added " + vscode.workspace.asRelativePath(file) + " for parsing, because new file is created"); } + + if (uri) { + this.debounceTimersParse.set(uriString, setTimeout(async () => { + LoggerOutput.log("Debouncing done (" + debounceTime + "ms elapsed) - parse file (" + debugText + ")", vscode.workspace.asRelativePath(uri!)); + this.debounceTimersParse.delete(uriString); + + await this.getFileData(uri, true, debugText); + + // Update all diagnostics if requested + // Will be true if file is changed on disc + if (doAllDiagnosticUpdate) { + // This updated file might be referenced in other files for which some errors might be solved + // Update diagnostics for all files (in debounced way if multiple files are changed) + this.updateDiagnosticsDebounced(undefined, debugText); + + // Update diagnostics only for this file + // Don't do that if there is already planned update for all files + } else { + const isAllDiagnosticsUpdatePlanned = this.debounceTimersDiagnostics.has(""); + if (!isAllDiagnosticsUpdatePlanned) { + // This file might be referenced in other files for which some errors might be solved + // Its not practical to refresh all files each time file changes + // Instead update only files opened in editor + // Other files gets updated when some of the file changes on disc (is saved) + const openedTextDocuments = vscode.workspace.textDocuments; + for (const document of openedTextDocuments) { + // Update diagnostics for all opened files (gsc file must be cached) + this.updateDiagnosticsDebounced(document.uri, debugText); + } + } + } + }, debounceTime)); + } else { + this.debounceTimersParse.set("", setTimeout(async () => { + LoggerOutput.log("Debouncing done (" + debounceTime + "ms elapsed) - parsing for all (" + debugText + ")"); + this.debounceTimersParse.delete(""); + + await this.parseAllFiles(); + + }, debounceTime)); + } + } + + + + + + private static handleFileChanges(context: vscode.ExtensionContext) { + + this.fileWatcher = vscode.workspace.createFileSystemWatcher('**/*.gsc'); + + // Subscribe to file system events + // If file is renamed, create event with new name is called first, then delete event with old name is called + this.fileWatcher.onDidDelete(this.onGscFileDelete, this, context.subscriptions); + this.fileWatcher.onDidCreate(this.onGscFileCreate, this, context.subscriptions); + this.fileWatcher.onDidChange(this.onGscFileChange, this, context.subscriptions); + } + + + private static onGscFileCreate(uri: vscode.Uri) { + LoggerOutput.log("[GscFiles] Detected new GSC file", vscode.workspace.asRelativePath(uri)); + + // Parse the new file + void GscFiles.getFileData(uri, true, "new file created"); + + // Re-diagnose all files, because this new file might solve reference errors in other files + this.updateDiagnosticsDebounced(undefined, "new file created"); + } + + private static onGscFileDelete(uri: vscode.Uri) { + LoggerOutput.log("[GscFiles] Detected deleted GSC file", vscode.workspace.asRelativePath(uri)); + + this.cachedFiles.removeCachedFile(uri); + + // Re-diagnose all files, because this file might generate reference errors in other files + this.updateDiagnosticsDebounced(undefined, "file deleted"); } - static onDeleteFiles(e: vscode.FileDeleteEvent) { - // Refresh all to ensure correct validation - LoggerOutput.log("[GscFiles] Re-parsing all because some file was deleted"); - void GscFiles.initialParse(); + + /** GSC file was modified (saved) */ + private static onGscFileChange(uri: vscode.Uri) { + LoggerOutput.log("[GscFiles] Detected GSC File change on disc", vscode.workspace.asRelativePath(uri)); + + // Force parsing the updated file and then update diagnostics for all files + this.parseFileAndDiagnoseDebounced(uri, true, "file " + vscode.workspace.asRelativePath(uri) + " changed on disc"); } - // Called when file or folder is renamed or moved - static onRenameFiles(e: vscode.FileRenameEvent) { - // Refresh all to ensure correct validation - LoggerOutput.log("[GscFiles] Re-parsing all because some file was renamed"); - void GscFiles.initialParse(); + + /** + * An event that is emitted when a text document is changed. This usually happens when the contents changes but also when other things like the dirty-state changes. + * Note: Is saved also on document save + * @param event + * @returns + */ + public static onTextDocumentChange(event: vscode.TextDocumentChangeEvent) { + + // No changes, ignore the event (file was just saved for example) + if (event.contentChanges.length === 0) { + return; + } + + const uri = event.document.uri; + + // Check if the file is GSC file + if (!GscFiles.isValidGscFile(uri.fsPath)) { + return; + } + + LoggerOutput.log("[GscFiles] Document changed", vscode.workspace.asRelativePath(uri)); + + // Parse the updated file and then update diagnostics only for opened text documents + this.parseFileAndDiagnoseDebounced(uri, false, "file " + vscode.workspace.asRelativePath(uri) + " changed in editor"); + } - static onChangeWorkspaceFolders(e: vscode.WorkspaceFoldersChangeEvent) { - // Refresh all to ensure correct validation - LoggerOutput.log("[GscFiles] Re-parsing all because workspace folders changed"); - void GscFiles.initialParse(); + + static async onChangeWorkspaceFolders(e: vscode.WorkspaceFoldersChangeEvent) { + + for (const removed of e.removed) { + LoggerOutput.log("[GscFiles] Workspace folder removed", removed.uri.toString()); + // Remove all cached files and the workspace itself + this.cachedFiles.removeWorkspaceFiles(removed.uri); + } + + for (const added of e.added) { + LoggerOutput.log("[GscFiles] Workspace folder added", added.uri.toString()); + // Parse all files in new workspace + await this.parseAndCacheAllFiles(added); + } + + // Re-diagnose all files, because this file might generate/fix reference errors in other files + this.updateDiagnosticsDebounced(undefined, "workspace folders changed"); } - private static async onDidConfigChange() { + public static updateConfigurationOfCachedFiles() { LoggerOutput.log("[GscFiles] Configuration changed, updating cached files configuration."); // Update configuration for GscFile before DiagnosticCollection is called - for (const [uriKey, workspaceData] of this.cachedFilesPerWorkspace) { + for (const workspaceData of this.cachedFiles.getAllWorkspaces()) { workspaceData.updateConfiguration(); } } @@ -487,38 +676,11 @@ export class GscFiles { } - - private static initialParseSubscribers: (() => void)[] = []; - - /** - * Subscribe to initial parse. The handler will be called when all files are parsed. - * @param handler - */ - public static onDidInitialParse(handler: () => void): vscode.Disposable { - this.initialParseSubscribers.push(handler); - return vscode.Disposable.from({ - dispose: () => { - const index = this.initialParseSubscribers.indexOf(handler); - if (index > -1) { - this.initialParseSubscribers.splice(index, 1); - } - } - }); + static isValidGscFile(filePath: string): boolean { + return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile() && path.extname(filePath).toLowerCase() === '.gsc'; } - /** - * Handle vscode configuration change event. - * Emit a configuration change event. This will call all subscribers in the order they were added. - */ - private static notifyOnDidInitialParse() { - for (const handler of this.initialParseSubscribers) { - try { - const result = handler(); - } catch (error) { - Issues.handleError(error); - } - } - } + @@ -528,7 +690,7 @@ export class GscFiles { if (vscode.window.activeTextEditor === undefined) { return; } - const gscFile = await GscFiles.getFileData(vscode.window.activeTextEditor.document.uri); + const gscFile = await GscFiles.getFileData(vscode.window.activeTextEditor.document.uri, false, "debugging parsed file"); GscFiles.debugParsedFile(gscFile); } @@ -536,7 +698,7 @@ export class GscFiles { if (vscode.window.activeTextEditor === undefined) { return; } - const gscFile = await GscFiles.getFileData(vscode.window.activeTextEditor.document.uri); + const gscFile = await GscFiles.getFileData(vscode.window.activeTextEditor.document.uri, false, "debugging parsed file structure"); console.log(GscFileParser.debugAsString(gscFile.data.root.tokensAll, gscFile.data.root, true)); } @@ -549,7 +711,7 @@ export class GscFiles { console.log("Getting item before cursor position L:" + position.line + " C:" + position.character); - const gscData = await GscFiles.getFileData(vscode.window.activeTextEditor.document.uri); + const gscData = await GscFiles.getFileData(vscode.window.activeTextEditor.document.uri, false, "debugging item before cursor"); // Get group before cursor var groupAtCursor = gscData.data.root.findGroupOnLeftAtPosition(position); @@ -741,132 +903,10 @@ export class GscFiles { -type GscFileConfig = { - /** All possible game root folders where GSC files can be found and referenced. */ - referenceableGameRootFolders: GscGameRootFolder[]; - /** Ignored function names */ - ignoredFunctionNames: string[]; - /** Ignored file paths */ - ignoredFilePaths: string[]; - /** Currently selected game */ - currentGame: GscGame; - /** Mode of diagnostics collection */ - errorDiagnostics: ConfigErrorDiagnostics; - /** Syntax configuration of the selected game */ - gameConfig: GscGameConfig; -}; - - -export class GscFile { - - /** URI as lower-case string */ - id: string; - - /** URI of the file */ - uri: vscode.Uri; - - /** Configuration related to this file */ - config: GscFileConfig; - - /** Diagnostics generated for this file. @see GscFileDiagnostics.ts */ - diagnostics: vscode.Diagnostic[] = []; - - - constructor( - /** Parsed data */ - public data: GscData, - /** URI of the file */ - uri?: vscode.Uri, // might be undefined for tests - /** Workspace folder to which this file belongs to */ - public workspaceFolder?: vscode.WorkspaceFolder, - ) { - if (uri === undefined) { - uri = vscode.Uri.parse("file://undefined"); - } - this.id = uri.toString().toLowerCase(); - this.uri = uri; - - if (workspaceFolder !== undefined) { - this.config = GscWorkspaceFileData.getConfig(workspaceFolder); - } else { - this.config = { - referenceableGameRootFolders: [], - currentGame: GscGame.UniversalGame, - ignoredFunctionNames: [], - ignoredFilePaths: [], - errorDiagnostics: ConfigErrorDiagnostics.Enable, - gameConfig: GscConfig.gamesConfigs.get(GscGame.UniversalGame)! - }; - } - } - - updateData(data: GscData) { - this.data = data; - } -} - - - - - - - - -/** - * GSC files parsed in workspace folder - */ -class GscWorkspaceFileData { - private parsedFiles: Map = new Map(); - - constructor( - public workspaceFolder: vscode.WorkspaceFolder - ) {} - - addParsedFile(data: GscFile) { - this.parsedFiles.set(data.id, data); - } - - getParsedFile(uri: vscode.Uri): GscFile | undefined { - const data = this.parsedFiles.get(uri.toString().toLowerCase()); - - return data; - } - - removeParsedFile(uri: vscode.Uri): boolean { - return this.parsedFiles.delete(uri.toString().toLowerCase()); - } - - getAllParsedFileData(): GscFile[] { - return Array.from(this.parsedFiles.values()); - } - - updateConfiguration() { - const data = GscWorkspaceFileData.getConfig(this.workspaceFolder); - // Loop all GscFile and update their configuration - for (const file of this.parsedFiles.values()) { - file.config.referenceableGameRootFolders = data.referenceableGameRootFolders; - file.config.currentGame = data.currentGame; - file.config.ignoredFunctionNames = data.ignoredFunctionNames; - file.config.ignoredFilePaths = data.ignoredFilePaths; - file.config.errorDiagnostics = data.errorDiagnostics; - file.config.gameConfig = data.gameConfig; - } - } - static getConfig(workspaceFolder: vscode.WorkspaceFolder): GscFileConfig { - // Get config for workspace folder - const referenceableGameRootFolders = GscFiles.getReferenceableGameRootFolders(workspaceFolder); - const currentGame = GscConfig.getSelectedGame(workspaceFolder.uri); - const ignoredFunctionNames = GscConfig.getIgnoredFunctionNames(workspaceFolder.uri); - const ignoredFilePaths = GscConfig.getIgnoredFilePaths(workspaceFolder.uri); - const errorDiagnostics = GscConfig.getErrorDiagnostics(workspaceFolder.uri); - const gameConfig = GscConfig.gamesConfigs.get(currentGame)!; - return {referenceableGameRootFolders, currentGame, ignoredFunctionNames, ignoredFilePaths, errorDiagnostics, gameConfig}; - } -} diff --git a/src/GscFunctions.ts b/src/GscFunctions.ts index 80836d9..1c72ff4 100644 --- a/src/GscFunctions.ts +++ b/src/GscFunctions.ts @@ -1,9 +1,7 @@ import * as vscode from 'vscode'; import { GscGroup, GscToken, GscVariableDefinitionType } from './GscFileParser'; -import { GscConfig, GscGame, GscGameRootFolder } from './GscConfig'; -import * as fs from 'fs'; -import * as path from 'path'; -import { GscFile, GscFileReferenceState, GscFiles } from './GscFiles'; +import { GscFiles, GscFileReferenceState } from './GscFiles'; +import { GscFile } from './GscFile'; import { CodFunctions } from './CodFunctions'; diff --git a/src/GscHoverProvider.ts b/src/GscHoverProvider.ts index e6cf64c..06cf8cf 100644 --- a/src/GscHoverProvider.ts +++ b/src/GscHoverProvider.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; -import { GscFile, GscFiles } from './GscFiles'; -import { GroupType, GscData } from './GscFileParser'; +import { GscFiles } from './GscFiles'; +import { GscFile } from './GscFile'; +import { GroupType } from './GscFileParser'; import { CodFunctions } from './CodFunctions'; import { ConfigErrorDiagnostics, GscConfig } from './GscConfig'; import { GscFunctions, GscFunctionState } from './GscFunctions'; @@ -9,7 +10,9 @@ import { LoggerOutput } from './LoggerOutput'; export class GscHoverProvider implements vscode.HoverProvider { - static async activate(context: vscode.ExtensionContext) { + static async activate(context: vscode.ExtensionContext) { + LoggerOutput.log("[GscHoverProvider] Activating"); + context.subscriptions.push(vscode.languages.registerHoverProvider('gsc', new GscHoverProvider())); } @@ -22,7 +25,7 @@ export class GscHoverProvider implements vscode.HoverProvider { 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 gscData = await GscFiles.getFileData(document.uri, false, "provide hover"); const hover = await GscHoverProvider.getHover(gscData, position); diff --git a/src/GscSemanticTokensProvider.ts b/src/GscSemanticTokensProvider.ts index b43951f..b62286f 100644 --- a/src/GscSemanticTokensProvider.ts +++ b/src/GscSemanticTokensProvider.ts @@ -1,8 +1,10 @@ import * as vscode from 'vscode'; import { GscFiles } from './GscFiles'; import { GroupType, GscGroup} from './GscFileParser'; -import { error } from 'console'; import { Issues } from './Issues'; +import { LoggerOutput } from './LoggerOutput'; +import { Events } from './Events'; +import { GscFile } from './GscFile'; export class GscSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider { @@ -35,6 +37,7 @@ export class GscSemanticTokensProvider implements vscode.DocumentSemanticTokensP static async activate(context: vscode.ExtensionContext) { + LoggerOutput.log("[GscSemanticTokensProvider] Activating"); context.subscriptions.push(vscode.languages.registerDocumentSemanticTokensProvider({ language: 'gsc' }, new GscSemanticTokensProvider(), GscSemanticTokensProvider.legend)); } @@ -60,12 +63,41 @@ export class GscSemanticTokensProvider implements vscode.DocumentSemanticTokensP // - when the language id of the document has changed. //vscode.window.showWarningMessage("SemanticTokensBuilder: " + document.uri.toString()); + LoggerOutput.log("[GscSemanticTokensProvider] Providing semantics...", vscode.workspace.asRelativePath(document.uri) + ", version: " + document.version); const builder = new vscode.SemanticTokensBuilder(GscSemanticTokensProvider.legend); // Get the parsed file - var gscFile = await GscFiles.getFileData(document.uri, true); - + var gscFile = await GscFiles.getCachedFile(document.uri); + + // If the cached file is not found, or cached version is not the latest, wait till the file is parsed + if (gscFile === undefined || (gscFile.version > -1 && gscFile.version !== document.version)) { + + if (gscFile === undefined) { + LoggerOutput.log("[GscSemanticTokensProvider] File not found in cache. Waiting for file to be parsed...", vscode.workspace.asRelativePath(document.uri)); + } else { + LoggerOutput.log("[GscSemanticTokensProvider] File version mismatch. parsed: " + gscFile.version + ". document: " + document.version + " waiting for file to be parsed...", vscode.workspace.asRelativePath(document.uri)); + } + + // Wait for file to be parsed + // If not parsed till specified time, parse the file by force + gscFile = await new Promise((resolve, reject) => { + const disposable = Events.onDidGscFileParsed((gscFileNew) => { + if (gscFileNew.uri.toString() === document.uri.toString()) { + disposable.dispose(); // Clean up the event listener + clearTimeout(timeoutId); // Cancel the timeout + LoggerOutput.log("[GscSemanticTokensProvider] File has been parsed: " + gscFileNew.version + ". document: " + document.version); + resolve(gscFileNew); + } + }); + const timeoutId = setTimeout(async () => { + disposable.dispose(); + LoggerOutput.log("[GscSemanticTokensProvider] Parsing file by force..."); + const gscFile = await GscFiles.getFileData(document.uri, true, "provide semantic tokens"); + resolve(gscFile); + }, 1000); + }); + } function walkGroupItems(parentGroup: GscGroup, items: GscGroup[], action: (parentGroup: GscGroup, group: GscGroup) => void) { // This object have child items, process them first @@ -142,7 +174,8 @@ export class GscSemanticTokensProvider implements vscode.DocumentSemanticTokensP }); //vscode.window.showInformationMessage("SemanticTokensBuilder done"); - + LoggerOutput.log("[GscSemanticTokensProvider] Done", vscode.workspace.asRelativePath(document.uri) + ", version: " + document.version); + return builder.build(); } }; \ No newline at end of file diff --git a/src/GscStatusBar.ts b/src/GscStatusBar.ts index fe7d05c..91ca971 100644 --- a/src/GscStatusBar.ts +++ b/src/GscStatusBar.ts @@ -6,7 +6,11 @@ import { GscFiles } from './GscFiles'; export class GscStatusBar { - static async activate(context: vscode.ExtensionContext) { + private static gameBarItem: vscode.StatusBarItem; + private static settingsBarItem: vscode.StatusBarItem; + + static async activate(context: vscode.ExtensionContext) { + LoggerOutput.log("[GscStatusBar] Activating"); // Command to open extension settings const openExtensionSettingsCommand = 'gsc.openExtensionSettings'; @@ -15,12 +19,12 @@ export class GscStatusBar { })); // Status bar item to open extension settings - const settingsBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 98); - settingsBarItem.text = "$(settings-gear) GSC Settings"; - settingsBarItem.tooltip = "Open GSC Settings"; - settingsBarItem.command = openExtensionSettingsCommand; - context.subscriptions.push(settingsBarItem); - settingsBarItem.show(); + this.settingsBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 98); + this.settingsBarItem.text = "$(settings-gear) GSC Settings"; + this.settingsBarItem.tooltip = "Open GSC Settings"; + this.settingsBarItem.command = openExtensionSettingsCommand; + context.subscriptions.push(this.settingsBarItem); + this.settingsBarItem.show(); @@ -29,68 +33,56 @@ export class GscStatusBar { // Create a new status bar item that we can manage const gameCommandID = 'gsc.changeGame'; - const gameBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99); - gameBarItem.command = gameCommandID; - gameBarItem.tooltip = 'Click to select a game'; + this.gameBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99); + this.gameBarItem.command = gameCommandID; + this.gameBarItem.tooltip = 'Click to select a game'; // Register the command to show the quick pick context.subscriptions.push(vscode.commands.registerCommand(gameCommandID, GscConfig.showGameQuickPick)); - context.subscriptions.push(gameBarItem); - - - + context.subscriptions.push(this.gameBarItem); - // Function to update the visibility of the status bar item based on the language type - const updateStatusBar = async (debugText: string) => { - const activeEditor = vscode.window.activeTextEditor; - - gameBarItem.hide(); - settingsBarItem.hide(); - if (activeEditor) { - const languageId = activeEditor.document.languageId; - if (languageId === 'gsc') { - - const uri = activeEditor.document.uri; - - // This file is not part of workspace - const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); - if (workspaceFolder === undefined) { - return; - } - - LoggerOutput.log(`[GscStatusBar] Updating status bar because: ${debugText}`, vscode.workspace.asRelativePath(uri)); + // Initial update of the status bar visibility + await this.updateStatusBar("init"); - const gscFile = GscFiles.getCachedFile(uri); - - const currentGame = (gscFile === undefined || debugText === "configChanged") ? GscConfig.getSelectedGame(workspaceFolder.uri) : gscFile.config.currentGame; - - LoggerOutput.log(`[GscStatusBar] Status bar updated with game: "${currentGame}"`, vscode.workspace.asRelativePath(uri)); - gameBarItem.text = "$(notebook-open-as-text) " + (currentGame); - - gameBarItem.show(); - settingsBarItem.show(); - } - } + } - }; - // Register event listeners to update status bar visibility - vscode.window.onDidChangeActiveTextEditor(() => updateStatusBar("activeEditorChanged"), null, context.subscriptions); + // Function to update the visibility of the status bar item based on the language type + public static async updateStatusBar(debugText: string) { + const activeEditor = vscode.window.activeTextEditor; - //vscode.workspace.onDidOpenTextDocument(updateStatusBar, null, context.subscriptions); - //vscode.workspace.onDidCloseTextDocument(updateStatusBar, null, context.subscriptions); - - GscConfig.onDidConfigChange(async () => { - await updateStatusBar("configChanged"); - }); + this.gameBarItem.hide(); + this.settingsBarItem.hide(); + + if (activeEditor) { + const languageId = activeEditor.document.languageId; + if (languageId === 'gsc') { + + const uri = activeEditor.document.uri; + + // This file is not part of workspace + const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + if (workspaceFolder === undefined) { + return; + } - // Initial update of the status bar visibility - await updateStatusBar("init"); + LoggerOutput.log(`[GscStatusBar] Updating status bar because: ${debugText}`, vscode.workspace.asRelativePath(uri)); + const gscFile = GscFiles.getCachedFile(uri); + + const currentGame = (gscFile === undefined || debugText === "configChanged") ? GscConfig.getSelectedGame(workspaceFolder.uri) : gscFile.config.currentGame; + + LoggerOutput.log(`[GscStatusBar] Status bar updated with game: "${currentGame}"`, vscode.workspace.asRelativePath(uri)); - } + this.gameBarItem.text = "$(notebook-open-as-text) " + (currentGame); + + this.gameBarItem.show(); + this.settingsBarItem.show(); + } + } + }; } \ No newline at end of file diff --git a/src/LoggerOutput.ts b/src/LoggerOutput.ts index b8ef352..4a6e91a 100644 --- a/src/LoggerOutput.ts +++ b/src/LoggerOutput.ts @@ -29,7 +29,7 @@ export class LoggerOutput { } if (spaces === undefined) { - spaces = 70; + spaces = 100; } // If there is a right message, align the left message to number of spaces and then add the right message diff --git a/src/extension.ts b/src/extension.ts index ebbee17..3aa5383 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import { Gsc } from './Gsc'; import { Issues } from './Issues'; import { Updates } from './Updates'; import { LoggerOutput } from './LoggerOutput'; +import { Events } from './Events'; export const EXTENSION_ID = 'eyza.cod-gsc'; export const GITHUB_ISSUES_URL = 'https://github.com/eyza-cod2/vscode-cod-gsc/issues'; @@ -23,29 +24,13 @@ export async function activate(context: vscode.ExtensionContext) { LoggerOutput.activate(context); Issues.activate(context); + + Events.activate(context); await Gsc.activate(context); await Updates.activate(context); - - - // Testing - /* - console.log("activate"); - - vscode.workspace.onDidSaveTextDocument((doc) => { - console.log("onDidSaveTextDocument: " + doc.uri.toString()); - }, null, context.subscriptions); - - vscode.workspace.onDidChangeWorkspaceFolders((event) => { - console.log("onDidChangeWorkspaceFolders: " + event.added.length + " " + event.removed.length); - }, null, context.subscriptions); - - context.subscriptions.push({ dispose: () => { - console.log("dispose"); - }}); -*/ } // This method is called when your extension is deactivated diff --git a/src/test/GscCompletionItemProvider.test.ts b/src/test/GscCompletionItemProvider.test.ts index 8ce9cf1..474e7c4 100644 --- a/src/test/GscCompletionItemProvider.test.ts +++ b/src/test/GscCompletionItemProvider.test.ts @@ -1,9 +1,9 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { GscFileParser, GscGroup, GroupType, GscVariableDefinitionType, GscData } from '../GscFileParser'; +import { GscFileParser, GscVariableDefinitionType, GscData } from '../GscFileParser'; import { CompletionConfig, GscCompletionItemProvider } from '../GscCompletionItemProvider'; import { GscGame } from '../GscConfig'; -import { GscFile } from '../GscFiles'; +import { GscFile } from '../GscFile'; function checkItem(gscData: GscData, items: vscode.CompletionItem[], index: number, labelName: string, kind: vscode.CompletionItemKind, types: GscVariableDefinitionType[]) { diff --git a/src/test/Tests.test.ts b/src/test/Tests.test.ts index 4676e64..26e2dd3 100644 --- a/src/test/Tests.test.ts +++ b/src/test/Tests.test.ts @@ -3,16 +3,17 @@ import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import assert from 'assert'; -import { GscData, GscVariableDefinitionType } from '../GscFileParser'; +import { GscVariableDefinitionType } from '../GscFileParser'; import { GscHoverProvider } from '../GscHoverProvider'; import { GscDefinitionProvider } from '../GscDefinitionProvider'; import { EXTENSION_ID } from '../extension'; -import { GscFile, GscFiles } from '../GscFiles'; +import { GscFiles } from '../GscFiles'; +import { GscFile } from '../GscFile'; import { GscCompletionItemProvider } from '../GscCompletionItemProvider'; import { GscCodeActionProvider } from '../GscCodeActionProvider'; import { GscFunction } from '../GscFunctions'; import { LoggerOutput } from '../LoggerOutput'; -import { GscDiagnosticsCollection } from '../GscDiagnosticsCollection'; +import { Events } from '../Events'; export const testWorkspaceDir = path.join(os.tmpdir(), 'vscode-test-workspace'); @@ -46,7 +47,7 @@ export async function loadGscFile(paths: string[]): Promise { LoggerOutput.log("[Tests] loadGscFile() " + vscode.workspace.asRelativePath(fileUri)); - var gscFile = await GscFiles.getFileData(fileUri); + var gscFile = await GscFiles.getFileData(fileUri, true, "loadGscFile"); return gscFile; } @@ -173,9 +174,28 @@ export function checkCompletions(gscFile: GscFile, items: vscode.CompletionItem[ } +export function checkCachedFile(cachedFiles: GscFile[], index: number, paths: string[]) { -export function filePathToUri(relativePath: string): vscode.Uri { - const filePath = path.join(testWorkspaceDir, relativePath); + const filePath = path.join(testWorkspaceDir, ...paths); + const fileUri = vscode.workspace.asRelativePath(vscode.Uri.file(filePath)); + + function message(message: string, current: string, expected: string) { + var debugText = cachedFiles.map((file, i) => " " + i + ": " + vscode.workspace.asRelativePath(file.uri)).join('\n'); + return message + "\n\ngscFiles[" + index + "] = \n'" + current + "'. \n\nExpected: \n'" + expected + "'. \n\nErrors:\n" + debugText + "\n\n"; + } + + var item = cachedFiles.at(index); + assert.ok(item !== undefined, message("Undefined", typeof item, "undefined")); + + assert.deepStrictEqual(vscode.workspace.asRelativePath(item.uri), fileUri, message("Unexpected uri", vscode.workspace.asRelativePath(item.uri), fileUri)); +} + + + + + +export function filePathToUri(...paths: string[]): vscode.Uri { + const filePath = path.join(testWorkspaceDir, ...paths); const fileUri = vscode.Uri.file(filePath); return fileUri; } @@ -183,11 +203,10 @@ export function filePathToUri(relativePath: string): vscode.Uri { - export function waitForDiagnosticsChange(uri: vscode.Uri, debugText: string = ""): Promise { //console.log("waitForDiagnosticsChange: " + vscode.workspace.asRelativePath(uri)); return new Promise((resolve, reject) => { - const disposable = GscDiagnosticsCollection.onDidDiagnosticsChange((gscFile) => { + const disposable = Events.onDidGscDiagnosticChange((gscFile) => { //console.log("onDidDiagnosticsChange: " + vscode.workspace.asRelativePath(gscFile.uri)); if (gscFile.uri.toString() === uri.toString()) { disposable.dispose(); // Clean up the event listener diff --git a/src/test/workspace/GscFiles.test.ts b/src/test/workspace/GscFiles.test.ts new file mode 100644 index 0000000..5ab0041 --- /dev/null +++ b/src/test/workspace/GscFiles.test.ts @@ -0,0 +1,316 @@ +import * as vscode from 'vscode'; +import * as tests from '../Tests.test'; +import assert from 'assert'; +import { GscFiles } from '../../GscFiles'; +import { LoggerOutput } from '../../LoggerOutput'; + + +suite('GscFiles', () => { + + setup(async () => { + await tests.activateExtension(); + }); + + + test('file rename', async () => { + try { + LoggerOutput.log("[Tests][GscFiles] File rename - start"); + + const gsc = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + + // Check error + tests.checkDiagnostic(gsc.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc.diagnostics.length, 1); + + // Check cached files + const cachedFiles = GscFiles.getCachedFiles([gsc.workspaceFolder!.uri]); + cachedFiles.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles.length, 2); + + + + LoggerOutput.log("[Tests][GscFiles] File rename - renaming file from 'file1.gsc' to 'file1_renamed.gsc'"); + + // Rename file in workspace + void vscode.workspace.fs.rename(gsc.uri, vscode.Uri.file(gsc.uri.fsPath.replace("file1.gsc", "file1_renamed.gsc"))); + + // Wait till new file is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1_renamed.gsc'), "(1)"); + + // Load renamed file + const gsc2 = await tests.loadGscFile(['GscFiles', 'scripts', 'file1_renamed.gsc']); + + // Check error + tests.checkDiagnostic(gsc2.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc2.diagnostics.length, 1); + + // Check cached files + const cachedFiles2 = GscFiles.getCachedFiles([gsc2.workspaceFolder!.uri]); + cachedFiles2.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles2, 0, ['GscFiles', 'scripts', 'file1_renamed.gsc']); + tests.checkCachedFile(cachedFiles2, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles2.length, 2); + + + + + + LoggerOutput.log("[Tests][GscFiles] File rename - renaming file from 'file1_renamed.gsc' to 'file1.gsc'"); + + // Rename file in workspace + void vscode.workspace.fs.rename(vscode.Uri.file(gsc.uri.fsPath.replace("file1.gsc", "file1_renamed.gsc")), gsc.uri); + + // Wait till new file is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'), "(2)"); + + // Load renamed file + const gsc3 = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + + // Check error + tests.checkDiagnostic(gsc3.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc3.diagnostics.length, 1); + + // Check cached files + const cachedFiles3 = GscFiles.getCachedFiles([gsc2.workspaceFolder!.uri]); + cachedFiles3.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles3, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles3, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles3.length, 2); + + + LoggerOutput.log("[Tests][GscFiles] File rename - done"); + + } catch (error) { + tests.printDebugInfoForError(error); + } + }); + + + + + + + test('file rename .old', async () => { + try { + LoggerOutput.log("[Tests][GscFiles] File rename - start"); + + const gsc = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + + // Check error + tests.checkDiagnostic(gsc.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc.diagnostics.length, 1); + + // Check cached files + const cachedFiles = GscFiles.getCachedFiles([gsc.workspaceFolder!.uri]); + cachedFiles.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles.length, 2); + + + + LoggerOutput.log("[Tests][GscFiles] File rename - renaming file from 'file1.gsc' to 'file1.gsc.old'"); + + // Rename file in workspace + void vscode.workspace.fs.rename(gsc.uri, vscode.Uri.file(gsc.uri.fsPath.replace("file1.gsc", "file1.gsc.old"))); + + // Wait till it is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file2.gsc'), "(1)"); + + // Check error + const diagnostics = vscode.languages.getDiagnostics(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc')); + assert.strictEqual(diagnostics.length, 0); + + // Check cached files + const cachedFiles2 = GscFiles.getCachedFiles([tests.filePathToUri('GscFiles')]); + cachedFiles2.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles2, 0, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles2.length, 1); + + + + + + LoggerOutput.log("[Tests][GscFiles] File rename - renaming file from 'file1.gsc.old' to 'file1.gsc'"); + + // Rename file in workspace + void vscode.workspace.fs.rename(vscode.Uri.file(gsc.uri.fsPath.replace("file1.gsc", "file1.gsc.old")), gsc.uri); + + // Wait till new file is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'), "(2)"); + + // Load renamed file + const gsc3 = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + + // Check error + tests.checkDiagnostic(gsc3.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc3.diagnostics.length, 1); + + // Check cached files + const cachedFiles3 = GscFiles.getCachedFiles([gsc3.workspaceFolder!.uri]); + cachedFiles3.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles3, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles3, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles3.length, 2); + + + LoggerOutput.log("[Tests][GscFiles] File rename - done"); + + } catch (error) { + tests.printDebugInfoForError(error); + } + }); + + + + + + + test('file create', async () => { + try { + LoggerOutput.log("[Tests][GscFiles] File create - start"); + + // Check cached files + const cachedFiles = GscFiles.getCachedFiles([tests.filePathToUri('GscFiles')]); + cachedFiles.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles.length, 2); + + + + LoggerOutput.log("[Tests][GscFiles] File create - 'file3.gsc'"); + + // Create file in workspace + void vscode.workspace.fs.writeFile(tests.filePathToUri('GscFiles', 'scripts', 'file3.gsc'), new TextEncoder().encode("main() { error3 }")); + + // Wait till new file is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file3.gsc'), "(1)"); + + // Load renamed file + const gsc2 = await tests.loadGscFile(['GscFiles', 'scripts', 'file3.gsc']); + + // Check error + tests.checkDiagnostic(gsc2.diagnostics, 0, "Unexpected token error3", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc2.diagnostics.length, 1); + + // Check cached files + const cachedFiles2 = GscFiles.getCachedFiles([gsc2.workspaceFolder!.uri]); + cachedFiles2.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles2, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles2, 1, ['GscFiles', 'scripts', 'file2.gsc']); + tests.checkCachedFile(cachedFiles2, 2, ['GscFiles', 'scripts', 'file3.gsc']); + assert.strictEqual(cachedFiles2.length, 3); + + + + LoggerOutput.log("[Tests][GscFiles] Delete file - 'file3.gsc'"); + + // Delete the new file + void vscode.workspace.fs.delete(tests.filePathToUri('GscFiles', 'scripts', 'file3.gsc')); + + // Wait till deleted is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file2.gsc'), "(2)"); + + // Check error + const diagnostics = vscode.languages.getDiagnostics(tests.filePathToUri('GscFiles', 'scripts', 'file3.gsc')); + assert.strictEqual(diagnostics.length, 0); + + // Check cached files + const cachedFiles3 = GscFiles.getCachedFiles([gsc2.workspaceFolder!.uri]); + cachedFiles3.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles3, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles3, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles3.length, 2); + + + LoggerOutput.log("[Tests][GscFiles] File create - done"); + + } catch (error) { + tests.printDebugInfoForError(error); + } + }); + + + + + + + test('file move', async () => { + try { + LoggerOutput.log("[Tests][GscFiles] File move - start"); + + const gsc = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + + // Check error + tests.checkDiagnostic(gsc.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc.diagnostics.length, 1); + + // Check cached files + const cachedFiles = GscFiles.getCachedFiles([gsc.workspaceFolder!.uri]); + cachedFiles.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles.length, 2); + + + + LoggerOutput.log("[Tests][GscFiles] File move - move file from 'scripts/file1.gsc' to 'scripts2/file1.gsc'"); + + // Rename file in workspace + void vscode.workspace.fs.rename(gsc.uri, tests.filePathToUri('GscFiles', 'scripts2', 'file1.gsc')); + + // Wait till it is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts2', 'file1.gsc'), "(1)"); + + // Load renamed file + const gsc2 = await tests.loadGscFile(['GscFiles', 'scripts2', 'file1.gsc']); + + // Check error + tests.checkDiagnostic(gsc2.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc2.diagnostics.length, 1); + + // Check cached files + const cachedFiles2 = GscFiles.getCachedFiles([gsc2.workspaceFolder!.uri]); + cachedFiles2.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles2, 0, ['GscFiles', 'scripts', 'file2.gsc']); + tests.checkCachedFile(cachedFiles2, 1, ['GscFiles', 'scripts2', 'file1.gsc']); + assert.strictEqual(cachedFiles2.length, 2); + + + + + + LoggerOutput.log("[Tests][GscFiles] File rename - renaming file from 'scripts2/file1.gsc' to 'scripts/file1.gsc'"); + + // Rename file in workspace + void vscode.workspace.fs.rename(tests.filePathToUri('GscFiles', 'scripts2', 'file1.gsc'), gsc.uri); + + // Wait till new file is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'), "(2)"); + + // Load renamed file + const gsc3 = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + + // Check error + tests.checkDiagnostic(gsc3.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc3.diagnostics.length, 1); + + // Check cached files + const cachedFiles3 = GscFiles.getCachedFiles([gsc3.workspaceFolder!.uri]); + cachedFiles3.sort((a, b) => a.uri.fsPath.localeCompare(b.uri.fsPath)); // since file order is not guaranteed, sort them by path + tests.checkCachedFile(cachedFiles3, 0, ['GscFiles', 'scripts', 'file1.gsc']); + tests.checkCachedFile(cachedFiles3, 1, ['GscFiles', 'scripts', 'file2.gsc']); + assert.strictEqual(cachedFiles3.length, 2); + + + LoggerOutput.log("[Tests][GscFiles] File rename - done"); + + } catch (error) { + tests.printDebugInfoForError(error); + } + }); + +}); diff --git a/src/test/workspace/GscFiles/scripts/file1.gsc b/src/test/workspace/GscFiles/scripts/file1.gsc new file mode 100644 index 0000000..34807d4 --- /dev/null +++ b/src/test/workspace/GscFiles/scripts/file1.gsc @@ -0,0 +1,3 @@ +main1() { + error1 +} \ No newline at end of file diff --git a/src/test/workspace/GscFiles/scripts/file2.gsc b/src/test/workspace/GscFiles/scripts/file2.gsc new file mode 100644 index 0000000..6824695 --- /dev/null +++ b/src/test/workspace/GscFiles/scripts/file2.gsc @@ -0,0 +1,3 @@ +main2() { + error2 +} \ No newline at end of file diff --git a/src/test/workspace/vscode-cod-gsc-tests.code-workspace b/src/test/workspace/vscode-cod-gsc-tests.code-workspace index 1ca41d2..d2da1a9 100644 --- a/src/test/workspace/vscode-cod-gsc-tests.code-workspace +++ b/src/test/workspace/vscode-cod-gsc-tests.code-workspace @@ -47,6 +47,9 @@ { "name": "GscCompletionItemProvider.NonIncludedFolder", "path": "GscCompletionItemProvider.NonIncludedFolder" + }, + { + "path": "GscFiles" } ] } \ No newline at end of file