From ee7d41b3da738fe263497ade0185cb62ab42e871 Mon Sep 17 00:00:00 2001 From: eyza Date: Mon, 28 Oct 2024 17:56:54 +0100 Subject: [PATCH] Improved detection of GSC file changes, folder renames, folder move, etc... Added new Output window "GSC File Logs" to diagnose changes to file system. --- src/Events.ts | 18 +- src/GscFile.ts | 4 - src/GscFileCache.ts | 75 +++- src/GscFiles.ts | 168 +++++++-- src/Issues.ts | 14 +- src/LoggerOutput.ts | 52 ++- src/test/Tests.test.ts | 23 +- src/test/workspace/GscFiles.test.ts | 351 +++++++++++++----- .../workspace/GscFiles/scripts3/file3.gsc | 3 + .../scripts3/subscripts3/subfile3.gsc | 3 + 10 files changed, 565 insertions(+), 146 deletions(-) create mode 100644 src/test/workspace/GscFiles/scripts3/file3.gsc create mode 100644 src/test/workspace/GscFiles/scripts3/subscripts3/subfile3.gsc diff --git a/src/Events.ts b/src/Events.ts index 370195f..ba13848 100644 --- a/src/Events.ts +++ b/src/Events.ts @@ -105,7 +105,7 @@ export class Events { // 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(", ")); + LoggerOutput.log("[Events] File / folder has been renamed. " + e.files.map(f => vscode.workspace.asRelativePath(f.oldUri) + " -> " + vscode.workspace.asRelativePath(f.newUri)).join(", ")); } catch (error) { Issues.handleError(error); @@ -181,6 +181,22 @@ export class Events { } + + + + + private static readonly onDidFileSystemChangeEvent = new vscode.EventEmitter<{type: ("create" | "delete" | "change"), uri: vscode.Uri, manual: boolean}>(); + public static readonly onDidFileSystemChange = this.onDidFileSystemChangeEvent.event; + + public static FileSystemChanged(type: "create" | "delete" | "change", uri: vscode.Uri, manual: boolean = false) { + this.onDidFileSystemChangeEvent.fire({type, uri, manual}); + } + + + + + + static GscFileCacheFileHasChanged(fileUri: vscode.Uri) { LoggerOutput.log("[Events] GSC cache changed for file", vscode.workspace.asRelativePath(fileUri)); diff --git a/src/GscFile.ts b/src/GscFile.ts index 6649deb..904f22b 100644 --- a/src/GscFile.ts +++ b/src/GscFile.ts @@ -12,9 +12,6 @@ import { GscFiles } from './GscFiles'; export class GscFile { - /** URI as lower-case string */ - id: string; - /** URI of the file */ uri: vscode.Uri; @@ -43,7 +40,6 @@ export class GscFile { if (uri === undefined) { uri = vscode.Uri.parse("file://undefined"); } - this.id = uri.toString().toLowerCase(); this.uri = uri; if (workspaceFolder !== undefined) { diff --git a/src/GscFileCache.ts b/src/GscFileCache.ts index 28b4c6b..e509c4e 100644 --- a/src/GscFileCache.ts +++ b/src/GscFileCache.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as os from 'os'; import { GscFile } from './GscFile'; import { GscFiles } from './GscFiles'; import { ConfigErrorDiagnostics, GscConfig, GscGame, GscGameConfig, GscGameRootFolder } from './GscConfig'; @@ -34,18 +35,50 @@ export type GscFilesConfig = { }; +export class GscFileCache { + + /** + * Generates a normalized identifier (ID) from a given vscode.Uri. + * + * This function creates a consistent and comparable ID from a vscode.Uri by: + * 1. Using the `scheme`, `authority`, and `path` components of the URI. + * 2. Converting the path to lowercase on Windows to ensure that file comparisons are case-insensitive + * (since Windows filesystems are typically case-insensitive). + * 3. Normalizing the path separators (`/`) to ensure consistency across platforms. + * + * @param uri - The URI to generate an ID from. + * @returns A string that represents a consistent ID for the given URI, usable for cross-platform file comparison. + */ + public static getUriId(uri: vscode.Uri): string { + let normalizedPath = uri.path; // it uses forward slashes + + // Normalize the path for case-insensitive platforms (e.g., Windows) + if (os.platform() === 'win32') { + normalizedPath = normalizedPath.toLowerCase(); + } + + // Include the authority and other relevant parts of the Uri + const id = `${uri.scheme}://${uri.authority}${normalizedPath}`; + + return id; + } +} + + export class GscCachedFilesPerWorkspace { private cachedFilesPerWorkspace: Map = new Map(); createNewWorkspace(workspaceFolder: vscode.WorkspaceFolder): GscWorkspaceFileData { const data = new GscWorkspaceFileData(workspaceFolder); - this.cachedFilesPerWorkspace.set(workspaceFolder.uri.toString(), data); + const id = GscFileCache.getUriId(workspaceFolder.uri); + this.cachedFilesPerWorkspace.set(id, data); return data; } getWorkspace(workspaceUri: vscode.Uri): GscWorkspaceFileData | undefined { - return this.cachedFilesPerWorkspace.get(workspaceUri.toString()); + const id = GscFileCache.getUriId(workspaceUri); + return this.cachedFilesPerWorkspace.get(id); } removeCachedFile(fileUri: vscode.Uri) { @@ -61,6 +94,19 @@ export class GscCachedFilesPerWorkspace { dataOfWorkspace.removeParsedFile(fileUri); } + getParsedFilesByFileOrFolderPath(uri: vscode.Uri): GscFile[] | undefined { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + if (workspaceFolder === undefined) { + return; + } + const workspaceData = this.getWorkspace(workspaceFolder.uri); + if (workspaceData === undefined) { + return; + } + const files = workspaceData.getParsedFilesByFileOrFolderPath(uri); + return files; + } + removeWorkspace(workspaceUri: vscode.Uri) { const workspaceData = this.getWorkspace(workspaceUri); if (workspaceData === undefined) { @@ -68,7 +114,8 @@ export class GscCachedFilesPerWorkspace { } workspaceData.removeAllParsedFiles(); - return this.cachedFilesPerWorkspace.delete(workspaceUri.toString()); + const id = GscFileCache.getUriId(workspaceUri); + return this.cachedFilesPerWorkspace.delete(id); } getAllWorkspaces() { @@ -95,24 +142,38 @@ export class GscWorkspaceFileData { } addParsedFile(gscFile: GscFile) { - if (!this.parsedFiles.has(gscFile.id)) { + const id = GscFileCache.getUriId(gscFile.uri); + if (!this.parsedFiles.has(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); + this.parsedFiles.set(id, gscFile); Events.GscFileCacheFileHasChanged(gscFile.uri); } getParsedFile(uri: vscode.Uri): GscFile | undefined { - const data = this.parsedFiles.get(uri.toString().toLowerCase()); + const id = GscFileCache.getUriId(uri); + const data = this.parsedFiles.get(id); return data; } + getParsedFilesByFileOrFolderPath(uri: vscode.Uri): GscFile[] { + const files: GscFile[] = []; + const id = GscFileCache.getUriId(uri); + for (const [fileId, file] of this.parsedFiles) { + if (fileId === id || fileId.startsWith(id + "/")) { + files.push(file); + } + } + return files; + } + removeParsedFile(uri: vscode.Uri): boolean { - const removed = this.parsedFiles.delete(uri.toString().toLowerCase()); + const id = GscFileCache.getUriId(uri); + const removed = this.parsedFiles.delete(id); if (removed) { LoggerOutput.log("[GscFileCache] Removed file from cache", vscode.workspace.asRelativePath(uri)); diff --git a/src/GscFiles.ts b/src/GscFiles.ts index 310cb70..c835686 100644 --- a/src/GscFiles.ts +++ b/src/GscFiles.ts @@ -8,6 +8,7 @@ import { GscConfig, GscGameRootFolder } from './GscConfig'; import { LoggerOutput } from './LoggerOutput'; import { GscDiagnosticsCollection } from './GscDiagnosticsCollection'; import { Events } from './Events'; +import { Issues } from './Issues'; /** * On startup scan every .gsc file, parse it, and save the result into memory. @@ -657,44 +658,159 @@ export class GscFiles { private static handleFileChanges(context: vscode.ExtensionContext) { - this.fileWatcher = vscode.workspace.createFileSystemWatcher('**/*.gsc'); + this.fileWatcher = vscode.workspace.createFileSystemWatcher('**'); // 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); + this.fileWatcher.onDidDelete((uri) => this.onFileNotification("delete", uri), this, context.subscriptions); + this.fileWatcher.onDidCreate((uri) => this.onFileNotification("create", uri), this, context.subscriptions); + this.fileWatcher.onDidChange((uri) => this.onFileNotification("change", uri), this, context.subscriptions); } + private static foldersWaitingForFileNotifications: Map = new Map(); - private static onGscFileCreate(uri: vscode.Uri) { - LoggerOutput.log("[GscFiles] Detected new GSC file", vscode.workspace.asRelativePath(uri)); + private static async onFileNotification(type: "create" | "delete" | "change", uri: vscode.Uri, manual: boolean = false) { + try { + // Notify about change + Events.FileSystemChanged(type, uri, manual); + + // Delete of file / directory + if (type === "delete") { + LoggerOutput.logFile("[GscFiles] Detected '" + type + "' of file / folder", vscode.workspace.asRelativePath(uri)); + + // Remove files from cache that starts with this path + // At this point it might be directory or file + // If it is directory, it might contain GSC files (or other files) + // If it is file, it might be GSC file (or other file) + const filesToRemove = this.cachedFiles.getParsedFilesByFileOrFolderPath(uri); + + if (filesToRemove && filesToRemove.length > 0) { + LoggerOutput.logFile("[GscFiles] - cached GSC files to remove: " + filesToRemove.length); + for (const file of filesToRemove) { + LoggerOutput.logFile("[GscFiles] - " + vscode.workspace.asRelativePath(file.uri)); + } - // Parse the new file - void GscFiles.getFileData(uri, true, "new file created"); + for (const file of filesToRemove) { + this.cachedFiles.removeCachedFile(file.uri); + } - // Re-diagnose all files, because this new file might solve reference errors in other files - this.updateDiagnosticsDebounced(undefined, "new file created"); - } + // Re-diagnose all files, because this file might generate reference errors in other files + this.updateDiagnosticsDebounced(undefined, "file deleted"); + } else { + LoggerOutput.logFile("[GscFiles] - no cached GSC files found to remove"); + } + + // Create or change of file / directory + } else { + var stat: vscode.FileStat; + try { + stat = await vscode.workspace.fs.stat(uri); + } catch (error) { + // Stat might fail if the file is deleted / renamed before the stat is called, or if the file is not accessible + // Ignore the error + LoggerOutput.logFile("[GscFiles] Detected '" + type + "', but unable to get file stats" + (manual ? ", manually triggered" : ""), vscode.workspace.asRelativePath(uri)); + LoggerOutput.logFile("[GscFiles] - error: " + error); + return; + } + + const isValidGscFile = this.isValidGscFile(uri.fsPath, stat); + const isDirectory = stat.type === vscode.FileType.Directory; + + LoggerOutput.logFile("[GscFiles] Detected '" + type + "'" + (isValidGscFile ? ", GSC file" : "") + (isDirectory ? ", directory" : "") + (manual ? ", manually triggered" : ""), vscode.workspace.asRelativePath(uri)); + + if (isValidGscFile) { + if (type === "create") { + + // Check if there is timer waiting for files in this directory (check path prefix) + for (const [pathPrefix, timer] of this.foldersWaitingForFileNotifications) { + if (uri.fsPath.startsWith(pathPrefix)) { + clearTimeout(timer); + this.foldersWaitingForFileNotifications.delete(pathPrefix); + LoggerOutput.logFile("[GscFiles] - canceled timer for directory check"); + } + } - private static onGscFileDelete(uri: vscode.Uri) { - LoggerOutput.log("[GscFiles] Detected deleted GSC file", vscode.workspace.asRelativePath(uri)); + LoggerOutput.logFile("[GscFiles] - running immediately forced file parsing + debounced diagnostics update for all", vscode.workspace.asRelativePath(uri)); - this.cachedFiles.removeCachedFile(uri); + // Parse the new file + void GscFiles.getFileData(uri, true, "new file created"); - // Re-diagnose all files, because this file might generate reference errors in other files - this.updateDiagnosticsDebounced(undefined, "file deleted"); - } + // Re-diagnose all files, because this new file might solve reference errors in other files + this.updateDiagnosticsDebounced(undefined, "new file created"); + + } else if (type === "change") { + LoggerOutput.logFile("[GscFiles] - running debounced file parsing + diagnostics update for all", vscode.workspace.asRelativePath(uri)); - /** 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"); + } - // Force parsing the updated file and then update diagnostics for all files - this.parseFileAndDiagnoseDebounced(uri, true, "file " + vscode.workspace.asRelativePath(uri) + " changed on disc"); + // Its directory + } else if (isDirectory) { + if (type === "create") { + // New directory might contain GSC files (if the folder was moved) + // There is a bug when folder is moved - create event is not called for each file in the folder + // but when the folder is copied (so create folder even is also called), then create event is called for each file in the folder + // Solution: when folder is created, prepare timed callback that loads GSC files manually unless create event is called for each file in the folder + + this.foldersWaitingForFileNotifications.set(uri.fsPath, setTimeout(async () => { + try { + this.foldersWaitingForFileNotifications.delete(uri.fsPath); + + LoggerOutput.logFile("[GscFiles] Timer elapsed - check files in directory", vscode.workspace.asRelativePath(uri)); + + // Get all files in the new directory + const gscFileUris: vscode.Uri[] = []; + async function readDirectoryRecursive(directoryUri: vscode.Uri) { + const files = await vscode.workspace.fs.readDirectory(directoryUri); + for (const [file, fileType] of files) { + const fileUri = vscode.Uri.joinPath(directoryUri, file); + if (fileType === vscode.FileType.File && GscFiles.isValidGscFile(fileUri.fsPath)) { + gscFileUris.push(fileUri); + } else if (fileType === vscode.FileType.Directory) { + await readDirectoryRecursive(fileUri); // Recursive call + } + } + } + await readDirectoryRecursive(uri); + + LoggerOutput.logFile("[GscFiles] - GSC files in directory: " + gscFileUris.length); + for (const gscFile of gscFileUris) { + LoggerOutput.logFile("[GscFiles] - " + vscode.workspace.asRelativePath(gscFile)); + } + + for (const gscFile of gscFileUris) { + void this.onFileNotification("create", gscFile, true); + } + } catch (error) { + Issues.handleError(new Error("Error while processing files in directory. " + error)); + throw error; + } + }, 200)); + + LoggerOutput.logFile("[GscFiles] - added timer to check the directory for files"); + } + + // Non-gsc file + } else { + LoggerOutput.logFile("[GscFiles] - not GSC file or directory, ignoring"); + } + } + } catch (error) { + Issues.handleError(new Error("Error while processing file change notification. " + error)); + throw error; + } + } + + static isValidGscFile(filePath: string, stats?: vscode.FileStat): boolean { + if (stats) { + return stats.type === vscode.FileType.File && path.extname(filePath).toLowerCase() === '.gsc'; + } else { + return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile() && path.extname(filePath).toLowerCase() === '.gsc'; + } } + /** * 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 @@ -756,12 +872,6 @@ export class GscFiles { } - static isValidGscFile(filePath: string): boolean { - return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile() && path.extname(filePath).toLowerCase() === '.gsc'; - } - - - diff --git a/src/Issues.ts b/src/Issues.ts index e0c14ac..d5a45d1 100644 --- a/src/Issues.ts +++ b/src/Issues.ts @@ -10,6 +10,7 @@ export class Issues { static lastError: Error | undefined; static lastErrorCode: string | undefined; + static lastErrorCount: number = 0; static activate(context: vscode.ExtensionContext) { @@ -78,7 +79,7 @@ export class Issues { } errorDetails.push('Log (5min):'); - errorDetails.push(LoggerOutput.getLogs().join('\n')); + errorDetails.push(LoggerOutput.getLogs(undefined, 5 * 60).join('\n')); // Set the webview's HTML content panel.webview.html = Issues.getWebviewContent(errorDetails.join('\n')); @@ -95,6 +96,7 @@ export class Issues { // Save the error this.lastError = error; this.lastErrorCode = vscode.window.activeTextEditor ? this.getEditorCode(vscode.window.activeTextEditor) : undefined; + this.lastErrorCount += 1; this.statusBarItem.show(); @@ -298,4 +300,14 @@ export class Issues { return buffer.join('\n'); } + + + public static errorCheckCount = 0; + /** Check if new error is reported. For tests */ + public static checkForNewError(): void { + if (this.lastErrorCount > this.errorCheckCount) { + this.errorCheckCount = this.lastErrorCount; + throw this.lastError; + } + } } \ No newline at end of file diff --git a/src/LoggerOutput.ts b/src/LoggerOutput.ts index 2d77d51..ba92b69 100644 --- a/src/LoggerOutput.ts +++ b/src/LoggerOutput.ts @@ -6,7 +6,8 @@ interface LogEntry { } export class LoggerOutput { - private static outputChannel: vscode.OutputChannel; + private static outputChannel: vscode.OutputChannel | undefined; + private static outputChannelFiles: vscode.OutputChannel | undefined; private static isDisposed = false; private static logBuffer: LogEntry[] = []; // Internal log buffer @@ -15,7 +16,9 @@ export class LoggerOutput { if (!this.outputChannel) { this.outputChannel = vscode.window.createOutputChannel('GSC Logs'); } - + if (!this.outputChannelFiles) { + this.outputChannelFiles = vscode.window.createOutputChannel('GSC File Logs'); + } // Ensure the output channel is disposed of when the extension is deactivated context.subscriptions.push({ dispose: () => this.dispose(), @@ -25,7 +28,7 @@ export class LoggerOutput { // Log a message to the custom output channel static log(message: string, rightMessage?: string, spaces?: number) { if (!this.outputChannel) { - this.outputChannel = vscode.window.createOutputChannel('GSC Logs'); + return; } if (spaces === undefined) { @@ -54,10 +57,37 @@ export class LoggerOutput { this.cleanUpOldLogs(); } + + // Log a message to the custom output channel + static logFile(message: string, rightMessage?: string, spaces?: number) { + if (!this.outputChannelFiles) { + return; + } + + this.log(message, rightMessage, spaces); + + if (spaces === undefined) { + spaces = 100; + } + + // 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 outputChannelFiles + this.outputChannelFiles.appendLine(fullMessage); + } + + // Method to retrieve the logs from the last 5 minutes, from latest to oldest - static getLogs(): string[] { + static getLogs(maxLines: number | undefined = undefined, maxSecondsAgo: number): string[] { const now = new Date(); - const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago + const fiveMinutesAgo = new Date(now.getTime() - maxSecondsAgo * 1000); // 5 minutes ago const recentLogs: string[] = []; @@ -67,7 +97,11 @@ export class LoggerOutput { if (logEntry.timestamp >= fiveMinutesAgo) { recentLogs.push(logEntry.message); } else { - // Since logs are chronological, no need to check earlier logs + recentLogs.push('...'); // Indicate that there are older logs + break; + } + if (maxLines !== undefined && recentLogs.length >= maxLines) { + recentLogs.push('...'); // Indicate that there are more logs break; } } @@ -112,8 +146,10 @@ export class LoggerOutput { // Disposes of the output channel when no longer needed static dispose() { - if (!this.isDisposed && this.outputChannel) { - this.outputChannel.dispose(); + if (!this.isDisposed) { + this.outputChannel?.dispose(); + this.outputChannelFiles?.dispose(); + this.isDisposed = true; } } diff --git a/src/test/Tests.test.ts b/src/test/Tests.test.ts index 9f8ec88..7c76a13 100644 --- a/src/test/Tests.test.ts +++ b/src/test/Tests.test.ts @@ -204,7 +204,7 @@ export function checkCachedFile(cachedFiles: GscFile[], index: number, paths: st } var item = cachedFiles.at(index); - assert.ok(item !== undefined, message("Undefined", typeof item, "undefined")); + assert.ok(item !== undefined, message("Undefined cached file at index " + index, typeof item, "undefined")); assert.deepStrictEqual(vscode.workspace.asRelativePath(item.uri), fileUri, message("Unexpected uri", vscode.workspace.asRelativePath(item.uri), fileUri)); } @@ -249,6 +249,25 @@ export function waitForDiagnosticsChange(uri: vscode.Uri, debugText: string = "" +export function waitForFileSystemChange(type: "create" | "delete" | "change", uri: vscode.Uri, debugText: string = ""): Promise { + return new Promise((resolve, reject) => { + const disposable = Events.onDidFileSystemChange((data) => { + //console.log("onDidDiagnosticsChange: " + vscode.workspace.asRelativePath(gscFile.uri)); + if (data.type === type, data.uri.toString() === uri.toString()) { + disposable.dispose(); // Clean up the event listener + resolve(); + } + }); + setTimeout(() => { + disposable.dispose(); + reject(new Error('Timeout waiting for file system change. Uri: ' + vscode.workspace.asRelativePath(uri) + ". " + debugText)); + }, 5000); // Adjust the timeout as needed + }); +} + + + + /** @@ -364,7 +383,7 @@ export function printDebugInfoForError(err: unknown) { } } - const log = LoggerOutput.getLogs().join("\n"); + const log = LoggerOutput.getLogs(500, 60).join("\n"); appendToBuffer(` `); appendToBuffer(` `); diff --git a/src/test/workspace/GscFiles.test.ts b/src/test/workspace/GscFiles.test.ts index 5ab0041..6134441 100644 --- a/src/test/workspace/GscFiles.test.ts +++ b/src/test/workspace/GscFiles.test.ts @@ -3,6 +3,7 @@ import * as tests from '../Tests.test'; import assert from 'assert'; import { GscFiles } from '../../GscFiles'; import { LoggerOutput } from '../../LoggerOutput'; +import { Issues } from '../../Issues'; suite('GscFiles', () => { @@ -11,30 +12,38 @@ suite('GscFiles', () => { await tests.activateExtension(); }); + async function checkInitialState() { + 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']); + tests.checkCachedFile(cachedFiles, 2, ['GscFiles', 'scripts3', 'file3.gsc']); + tests.checkCachedFile(cachedFiles, 3, ['GscFiles', 'scripts3', 'subscripts3', 'subfile3.gsc']); + assert.strictEqual(cachedFiles.length, 4); + } + 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); + await checkInitialState(); LoggerOutput.log("[Tests][GscFiles] File rename - renaming file from 'file1.gsc' to 'file1_renamed.gsc'"); + const file1 = tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'); + // Rename file in workspace - void vscode.workspace.fs.rename(gsc.uri, vscode.Uri.file(gsc.uri.fsPath.replace("file1.gsc", "file1_renamed.gsc"))); + void vscode.workspace.fs.rename(file1, vscode.Uri.file(file1.fsPath.replace("file1.gsc", "file1_renamed.gsc"))); // Wait till new file is processed await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1_renamed.gsc'), "(1)"); @@ -51,37 +60,29 @@ suite('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', 'file1_renamed.gsc']); tests.checkCachedFile(cachedFiles2, 1, ['GscFiles', 'scripts', 'file2.gsc']); - assert.strictEqual(cachedFiles2.length, 2); - - + tests.checkCachedFile(cachedFiles2, 2, ['GscFiles', 'scripts3', 'file3.gsc']); + tests.checkCachedFile(cachedFiles2, 3, ['GscFiles', 'scripts3', 'subscripts3', 'subfile3.gsc']); + assert.strictEqual(cachedFiles2.length, 4); 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); + void vscode.workspace.fs.rename(vscode.Uri.file(file1.fsPath.replace("file1.gsc", "file1_renamed.gsc")), file1); // 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); + + + + await checkInitialState(); LoggerOutput.log("[Tests][GscFiles] File rename - done"); + Issues.checkForNewError(); } catch (error) { tests.printDebugInfoForError(error); } @@ -96,25 +97,16 @@ suite('GscFiles', () => { 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); + await checkInitialState(); LoggerOutput.log("[Tests][GscFiles] File rename - renaming file from 'file1.gsc' to 'file1.gsc.old'"); + const file1 = tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'); + // Rename file in workspace - void vscode.workspace.fs.rename(gsc.uri, vscode.Uri.file(gsc.uri.fsPath.replace("file1.gsc", "file1.gsc.old"))); + void vscode.workspace.fs.rename(file1, vscode.Uri.file(file1.fsPath.replace("file1.gsc", "file1.gsc.old"))); // Wait till it is processed await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file2.gsc'), "(1)"); @@ -127,7 +119,9 @@ suite('GscFiles', () => { 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); + tests.checkCachedFile(cachedFiles2, 1, ['GscFiles', 'scripts3', 'file3.gsc']); + tests.checkCachedFile(cachedFiles2, 2, ['GscFiles', 'scripts3', 'subscripts3', 'subfile3.gsc']); + assert.strictEqual(cachedFiles2.length, 3); @@ -136,28 +130,20 @@ suite('GscFiles', () => { 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); + void vscode.workspace.fs.rename(vscode.Uri.file(file1.fsPath.replace("file1.gsc", "file1.gsc.old")), file1); // 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); + + + await checkInitialState(); LoggerOutput.log("[Tests][GscFiles] File rename - done"); + Issues.checkForNewError(); } catch (error) { tests.printDebugInfoForError(error); } @@ -172,12 +158,7 @@ suite('GscFiles', () => { 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); + await checkInitialState(); @@ -202,7 +183,9 @@ suite('GscFiles', () => { 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); + tests.checkCachedFile(cachedFiles2, 3, ['GscFiles', 'scripts3', 'file3.gsc']); + tests.checkCachedFile(cachedFiles2, 4, ['GscFiles', 'scripts3', 'subscripts3', 'subfile3.gsc']); + assert.strictEqual(cachedFiles2.length, 5); @@ -218,16 +201,16 @@ suite('GscFiles', () => { 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); + + + + + await checkInitialState(); LoggerOutput.log("[Tests][GscFiles] File create - done"); + Issues.checkForNewError(); } catch (error) { tests.printDebugInfoForError(error); } @@ -242,25 +225,16 @@ suite('GscFiles', () => { 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); + await checkInitialState(); LoggerOutput.log("[Tests][GscFiles] File move - move file from 'scripts/file1.gsc' to 'scripts2/file1.gsc'"); + const file1 = tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'); + // Rename file in workspace - void vscode.workspace.fs.rename(gsc.uri, tests.filePathToUri('GscFiles', 'scripts2', 'file1.gsc')); + void vscode.workspace.fs.rename(file1, tests.filePathToUri('GscFiles', 'scripts2', 'file1.gsc')); // Wait till it is processed await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts2', 'file1.gsc'), "(1)"); @@ -277,7 +251,9 @@ suite('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']); tests.checkCachedFile(cachedFiles2, 1, ['GscFiles', 'scripts2', 'file1.gsc']); - assert.strictEqual(cachedFiles2.length, 2); + tests.checkCachedFile(cachedFiles2, 2, ['GscFiles', 'scripts3', 'file3.gsc']); + tests.checkCachedFile(cachedFiles2, 3, ['GscFiles', 'scripts3', 'subscripts3', 'subfile3.gsc']); + assert.strictEqual(cachedFiles2.length, 4); @@ -286,31 +262,218 @@ suite('GscFiles', () => { 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); + void vscode.workspace.fs.rename(tests.filePathToUri('GscFiles', 'scripts2', 'file1.gsc'), file1); // Wait till new file is processed await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'), "(2)"); + + + + + + await checkInitialState(); + + + LoggerOutput.log("[Tests][GscFiles] File rename - done"); + + Issues.checkForNewError(); + } catch (error) { + tests.printDebugInfoForError(error); + } + }); + + + + + + + test('folder rename', async () => { + try { + LoggerOutput.log("[Tests][GscFiles] Folder rename - start"); + + await checkInitialState(); + + + + LoggerOutput.log("[Tests][GscFiles] Folder rename - renaming folder 'scripts3' to 'scripts3_new'"); + + const scriptsUri = tests.filePathToUri('GscFiles', 'scripts3'); + const scriptsNewUri = tests.filePathToUri('GscFiles', 'scripts3_new'); + + // Rename file in workspace + void vscode.workspace.fs.rename(scriptsUri, scriptsNewUri); + + // Wait till it is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts3_new', 'file3.gsc'), "(1)"); + // Load renamed file - const gsc3 = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + const gsc2 = await tests.loadGscFile(['GscFiles', 'scripts3_new', 'file3.gsc']); // Check error - tests.checkDiagnostic(gsc3.diagnostics, 0, "Unexpected token error1", vscode.DiagnosticSeverity.Error); - assert.strictEqual(gsc3.diagnostics.length, 1); + tests.checkDiagnostic(gsc2.diagnostics, 0, "Unexpected token error3", vscode.DiagnosticSeverity.Error); + assert.strictEqual(gsc2.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); + 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', 'scripts3_new', 'file3.gsc']); + tests.checkCachedFile(cachedFiles2, 3, ['GscFiles', 'scripts3_new', 'subscripts3', 'subfile3.gsc']); + assert.strictEqual(cachedFiles2.length, 4); - LoggerOutput.log("[Tests][GscFiles] File rename - done"); + + + LoggerOutput.log("[Tests][GscFiles] Folder rename - back"); + + // Rename file in workspace + void vscode.workspace.fs.rename(scriptsNewUri, scriptsUri); + + // Wait till new file is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts3', 'file3.gsc'), "(2)"); + + + + + + await checkInitialState(); + + + LoggerOutput.log("[Tests][GscFiles] Folder rename - done"); + + Issues.checkForNewError(); + } catch (error) { + tests.printDebugInfoForError(error); + } + }); + + + + + + test('non-gsc file', async () => { + try { + LoggerOutput.log("[Tests][GscFiles] Non-GSC - start"); + + await checkInitialState(); + + + + // Promise till diagnostics change + var updated = false; + void tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'), "(1)").then(() => { + updated = true; + });; + + + // Create file in workspace + LoggerOutput.log("[Tests][GscFiles] Non-GSC - create 'file3.txt'"); + void vscode.workspace.fs.writeFile(tests.filePathToUri('GscFiles', 'scripts', 'file3.txt'), new TextEncoder().encode("main() { error3 }")); + await tests.waitForFileSystemChange("create", tests.filePathToUri('GscFiles', 'scripts', 'file3.txt')); + + // Rename + LoggerOutput.log("[Tests][GscFiles] Non-GSC - rename 'file3.txt' to 'file3_new.txt'"); + void vscode.workspace.fs.rename(tests.filePathToUri('GscFiles', 'scripts', 'file3.txt'), tests.filePathToUri('GscFiles', 'scripts', 'file3_new.txt')); + //await tests.waitForFileSystemChange("delete", tests.filePathToUri('GscFiles', 'scripts', 'file3.txt')); + await tests.waitForFileSystemChange("create", tests.filePathToUri('GscFiles', 'scripts', 'file3_new.txt')); + + // Move + LoggerOutput.log("[Tests][GscFiles] Non-GSC - move 'scripts/file3_new.txt' to 'scripts3/file3_new.txt'"); + void vscode.workspace.fs.rename(tests.filePathToUri('GscFiles', 'scripts', 'file3_new.txt'), tests.filePathToUri('GscFiles', 'scripts3', 'file3_new.txt')); + //await tests.waitForFileSystemChange("delete", tests.filePathToUri('GscFiles', 'scripts', 'file3_new.txt')); + await tests.waitForFileSystemChange("create", tests.filePathToUri('GscFiles', 'scripts3', 'file3_new.txt')); + + // Delete + LoggerOutput.log("[Tests][GscFiles] Non-GSC - delete 'scripts3/file3_new.txt'"); + void vscode.workspace.fs.delete(tests.filePathToUri('GscFiles', 'scripts3', 'file3_new.txt')); + await tests.waitForFileSystemChange("delete", tests.filePathToUri('GscFiles', 'scripts3', 'file3_new.txt')); + + + // Wait for potential diagnostics change + await new Promise(resolve => setTimeout(resolve, 500)); + + // Text files should nto trigger diagnostics + assert.strictEqual(updated, false, "Text files should not trigger diagnostics update"); + + + + await checkInitialState(); + + + LoggerOutput.log("[Tests][GscFiles] File create - done"); + + Issues.checkForNewError(); } catch (error) { tests.printDebugInfoForError(error); } }); + + + + + + test('file update', async () => { + try { + LoggerOutput.log("[Tests][GscFiles] File update - start"); + + await checkInitialState(); + + + + LoggerOutput.log("[Tests][GscFiles] File update - update 'file1.gsc'"); + + const file1 = tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'); + + const contentOriginal = await vscode.workspace.fs.readFile(file1); + + // Change + void vscode.workspace.fs.writeFile(file1, new TextEncoder().encode("//main() { }")); + + const promiseFileChange = tests.waitForFileSystemChange("change", file1); + const promiseDiagUpdate = tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'), "(1)"); + await Promise.all([promiseFileChange, promiseDiagUpdate]); + + // Load renamed file + const gsc2 = await tests.loadGscFile(['GscFiles', 'scripts', 'file1.gsc']); + + // Check error + assert.strictEqual(gsc2.diagnostics.length, 0); + + // 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', 'scripts3', 'file3.gsc']); + tests.checkCachedFile(cachedFiles2, 3, ['GscFiles', 'scripts3', 'subscripts3', 'subfile3.gsc']); + assert.strictEqual(cachedFiles2.length, 4); + + + + LoggerOutput.log("[Tests][GscFiles] File update - back"); + + // Rename file in workspace + void vscode.workspace.fs.writeFile(file1, contentOriginal); + await tests.waitForFileSystemChange("change", file1); + + // Wait till new file is processed + await tests.waitForDiagnosticsChange(tests.filePathToUri('GscFiles', 'scripts', 'file1.gsc'), "(2)"); + + + + + await checkInitialState(); + + + LoggerOutput.log("[Tests][GscFiles] File update - done"); + + Issues.checkForNewError(); + } catch (error) { + tests.printDebugInfoForError(error); + } + }); }); diff --git a/src/test/workspace/GscFiles/scripts3/file3.gsc b/src/test/workspace/GscFiles/scripts3/file3.gsc new file mode 100644 index 0000000..d8c0e28 --- /dev/null +++ b/src/test/workspace/GscFiles/scripts3/file3.gsc @@ -0,0 +1,3 @@ +main3() { + error3 +} \ No newline at end of file diff --git a/src/test/workspace/GscFiles/scripts3/subscripts3/subfile3.gsc b/src/test/workspace/GscFiles/scripts3/subscripts3/subfile3.gsc new file mode 100644 index 0000000..83a311f --- /dev/null +++ b/src/test/workspace/GscFiles/scripts3/subscripts3/subfile3.gsc @@ -0,0 +1,3 @@ +submain3() { + suberror3 +} \ No newline at end of file