diff --git a/package.json b/package.json index 298cb36..52bd3ae 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,19 @@ "default": true, "description": "Show Automatic Images in the displayable auto-completion list. If not checked (false), only images defined in the script will be shown. If checked (true), both script-defined images and images detected in the images folders will be shown." }, + "renpy.diagnostics.diagnosticMode": { + "type": "string", + "default": "openFilesOnly", + "enum": [ + "openFilesOnly", + "workspace" + ], + "enumDescriptions": [ + "Only diagnoses open files.", + "Diagnose all Ren'Py files in the workspace. This mode applies recursive expressions that can be resource-intensive in projects with a large codebase." + ], + "description": "Select whether to analyze all Ren'Py files in the workspace or only open files (default)." + }, "renpy.diagnostics.warnOnObsoleteMethods": { "type": "boolean", "default": true, diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 58e87d2..cd3ca47 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -1,8 +1,9 @@ // Diagnostics (warnings and errors) "use strict"; -import { commands, Diagnostic, DiagnosticCollection, DiagnosticSeverity, ExtensionContext, languages, Range, TextDocument, window, workspace } from "vscode"; +import { commands, Diagnostic, DiagnosticCollection, DiagnosticSeverity, Disposable, ExtensionContext, FileType, languages, Range, TextDocument, Uri, window, workspace } from "vscode"; import { NavigationData } from "./navigation-data"; +import { getAllOpenTabInputTextUri } from "./utilities/functions"; import { extractFilename } from "./workspace"; // Renpy Store Variables (https://www.renpy.org/doc/html/store_variables.html) @@ -52,8 +53,10 @@ const rxStoreCheck = /\s+store\.(\w+)[^a-zA-Z_]?/g; const rxTabCheck = /^(\t+)/g; const rsComparisonCheck = /\s+(if|while)\s+(\w+)\s*(=)\s*(\w+)\s*/g; +const diagnosticModeEvents: Disposable[] = []; + /** - * Set up diagnostics + * Init diagnostics * @param context extension context */ export function diagnosticsInit(context: ExtensionContext) { @@ -68,7 +71,19 @@ export function diagnosticsInit(context: ExtensionContext) { }); context.subscriptions.push(refreshDiagnosticsCommand); - subscribeToDocumentChanges(context, diagnostics); + // Listen to diagnosticMode changes + context.subscriptions.push( + workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("renpy.diagnostics.diagnosticMode")) { + updateDiagnosticMode(context, diagnostics); + } + }) + ); + + const onDidChangeTextDocument = workspace.onDidChangeTextDocument((doc) => refreshDiagnostics(doc.document, diagnostics)); + context.subscriptions.push(onDidChangeTextDocument); + + updateDiagnosticMode(context, diagnostics); } /** @@ -186,22 +201,55 @@ function refreshDiagnostics(doc: TextDocument, diagnosticCollection: DiagnosticC diagnosticCollection.set(doc.uri, diagnostics); } -function subscribeToDocumentChanges(context: ExtensionContext, diagnostics: DiagnosticCollection): void { - if (window.activeTextEditor) { - refreshDiagnostics(window.activeTextEditor.document, diagnostics); - } +function refreshOpenDocuments(diagnosticCollection: DiagnosticCollection) { + diagnosticCollection.clear(); + const tabInputTextUris = getAllOpenTabInputTextUri(); + tabInputTextUris.forEach((uri) => { + diagnoseFromUri(uri, diagnosticCollection); + }); +} - context.subscriptions.push( - window.onDidChangeActiveTextEditor((editor) => { - if (editor) { - refreshDiagnostics(editor.document, diagnostics); - } - }) - ); +function diagnoseFromUri(uri: Uri, diagnosticCollection: DiagnosticCollection) { + workspace.fs.stat(uri).then((stat) => { + if (stat.type === FileType.File) { + workspace.openTextDocument(uri).then((document) => refreshDiagnostics(document, diagnosticCollection)); + } + }); +} - context.subscriptions.push(workspace.onDidChangeTextDocument((e) => refreshDiagnostics(e.document, diagnostics))); +function onDeleteFromWorkspace(uri: Uri, diagnosticCollection: DiagnosticCollection) { + diagnosticCollection.forEach((diagnosticUri) => { + if (diagnosticUri.fsPath.startsWith(uri.fsPath)) { + diagnosticCollection.delete(diagnosticUri); + } + }); +} - context.subscriptions.push(workspace.onDidCloseTextDocument((doc) => diagnostics.delete(doc.uri))); +function updateDiagnosticMode(context: ExtensionContext, diagnosticCollection: DiagnosticCollection): void { + diagnosticModeEvents.forEach((e) => e.dispose()); + if (workspace.getConfiguration("renpy.diagnostics").get("diagnosticMode") === "openFilesOnly") { + context.subscriptions.push(window.onDidChangeVisibleTextEditors(() => refreshOpenDocuments(diagnosticCollection), undefined, diagnosticModeEvents)); + // There is no guarantee that this event fires when an editor tab is closed + context.subscriptions.push( + workspace.onDidCloseTextDocument( + (doc) => { + if (diagnosticCollection.has(doc.uri)) { + diagnosticCollection.delete(doc.uri); + } + }, + undefined, + diagnosticModeEvents + ) + ); + refreshOpenDocuments(diagnosticCollection); + } else { + const fsWatcher = workspace.createFileSystemWatcher("**/*"); + diagnosticModeEvents.push(fsWatcher); + fsWatcher.onDidChange((uri) => diagnoseFromUri(uri, diagnosticCollection)); + fsWatcher.onDidCreate((uri) => diagnoseFromUri(uri, diagnosticCollection)); + fsWatcher.onDidDelete((uri) => onDeleteFromWorkspace(uri, diagnosticCollection)); + workspace.findFiles("**/*.rpy").then((uris) => uris.forEach((uri) => diagnoseFromUri(uri, diagnosticCollection))); + } } function checkObsoleteMethods(diagnostics: Diagnostic[], line: string, lineIndex: number) { diff --git a/src/utilities/functions.ts b/src/utilities/functions.ts new file mode 100644 index 0000000..dd48639 --- /dev/null +++ b/src/utilities/functions.ts @@ -0,0 +1,17 @@ +import { TabInputText, Uri, window } from "vscode"; + +/** + * Gets the URIs of the tabs opened in the editor whose input is of type TabInputText. + * @returns An array of URIs + */ +export function getAllOpenTabInputTextUri(): Uri[] { + const uris: Uri[] = []; + const tabGroups = window.tabGroups.all; + const tabs = tabGroups.flatMap((group) => group.tabs.map((tab) => tab)); + tabs.forEach((tab) => { + if (tab.input instanceof TabInputText) { + uris.push(tab.input.uri); + } + }); + return uris; +}