diff --git a/examples/example.rpy b/examples/example.rpy index 4eb0d13..7a33a53 100644 --- a/examples/example.rpy +++ b/examples/example.rpy @@ -94,6 +94,8 @@ label start: e "Ren'Py is a language and engine for writing and playing visual novel games." + $ seen = 1 + e "Our goal is to allow people to be able to write the script for a game, and with very little effort, turn that script into a working game." diff --git a/examples/game/script.rpy b/examples/game/script.rpy index f4e71ca..c26e531 100644 --- a/examples/game/script.rpy +++ b/examples/game/script.rpy @@ -46,7 +46,7 @@ label start: character.e "You've created a new Ren'Py game." - # call a label + # call a label call sidebar_label @@ -96,7 +96,7 @@ screen hello_title(): # sample python code init: "Renpy code block" - + python: renpy.pause(delay) diff --git a/package-lock.json b/package-lock.json index 6e0d6c9..68fb7f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8590,21 +8590,21 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, - "requires": { } + "requires": {} }, "@webpack-cli/info": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, - "requires": { } + "requires": {} }, "@webpack-cli/serve": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, - "requires": { } + "requires": {} }, "@xtuc/ieee754": { "version": "1.2.0", @@ -8638,14 +8638,14 @@ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, - "requires": { } + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "requires": { } + "requires": {} }, "acorn-walk": { "version": "8.2.0", @@ -8706,7 +8706,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "requires": { } + "requires": {} }, "ansi-regex": { "version": "5.0.1", @@ -12619,7 +12619,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, - "requires": { } + "requires": {} }, "ts-loader": { "version": "9.5.1", diff --git a/package.json b/package.json index c3119cc..473d639 100644 --- a/package.json +++ b/package.json @@ -226,32 +226,45 @@ "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.warnOnObsoleteMethods": { + "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, "description": "Enable obsolete method warnings. If checked (true), obsolete methods (e.g., im.Crop) will be marked with a warning in the editor." }, - "renpy.warnOnUndefinedPersistents": { + "renpy.diagnostics.warnOnUndefinedPersistents": { "type": "boolean", "default": true, "description": "Enable undefined persistent warnings. If checked (true), persistent variables will be marked with a warning in the editor if they haven't been defaulted/defined." }, - "renpy.warnOnUndefinedStoreVariables": { + "renpy.diagnostics.warnOnUndefinedStoreVariables": { "type": "boolean", "default": true, "description": "Enable undefined store variable warnings. If checked (true), store variables will be marked with a warning in the editor if they haven't been defaulted/defined." }, - "renpy.warnOnReservedVariableNames": { + "renpy.diagnostics.warnOnReservedVariableNames": { "type": "boolean", "default": true, "description": "Enable reserved variable warnings. If checked (true), variables will be marked with an error in the editor if they are in the list of names reserved by Python." }, - "renpy.warnOnInvalidVariableNames": { + "renpy.diagnostics.warnOnInvalidVariableNames": { "type": "boolean", "default": true, "description": "Enable invalid variable errors. Variables must begin with a letter or number. They may contain a '_' but may not begin with '_'. If set to true, variables will be flagged in the editor if they do not meet Ren'Py's specifications." }, - "renpy.warnOnIndentationAndSpacingIssues": { + "renpy.diagnostics.warnOnIndentationAndSpacingIssues": { "type": "string", "default": "Error", "enum": [ @@ -266,7 +279,7 @@ ], "description": "Enable indentation and inconsistent spacing checks. If set to Error or Warning, tab characters and inconsistent indentation spacing will be marked in the editor. If set to Disabled, indentation issues will be ignored." }, - "renpy.warnOnInvalidFilenameIssues": { + "renpy.diagnostics.warnOnInvalidFilenameIssues": { "type": "string", "default": "Error", "enum": [ diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 2971649..2d49467 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -1,6 +1,7 @@ // Diagnostics (warnings and errors) -import { Diagnostic, DiagnosticCollection, DiagnosticSeverity, ExtensionContext, 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) @@ -50,18 +51,51 @@ 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[] = []; + +/** + * Init diagnostics + * @param context extension context + */ +export function diagnosticsInit(context: ExtensionContext) { + const diagnostics = languages.createDiagnosticCollection("renpy"); + context.subscriptions.push(diagnostics); + + // custom command - refresh diagnostics + const refreshDiagnosticsCommand = commands.registerCommand("renpy.refreshDiagnostics", () => { + if (window.activeTextEditor) { + refreshDiagnostics(window.activeTextEditor.document, diagnostics); + } + }); + context.subscriptions.push(refreshDiagnosticsCommand); + + // 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); +} + /** * Analyzes the text document for problems. * @param doc text document to analyze * @param diagnostics diagnostic collection */ -export function refreshDiagnostics(doc: TextDocument, diagnosticCollection: DiagnosticCollection): void { +function refreshDiagnostics(doc: TextDocument, diagnosticCollection: DiagnosticCollection): void { if (doc.languageId !== "renpy") { return; } const diagnostics: Diagnostic[] = []; - const config = workspace.getConfiguration("renpy"); + const config = workspace.getConfiguration("renpy.diagnostics"); //Filenames must begin with a letter or number, //and may not begin with "00", as Ren'Py uses such files for its own purposes. @@ -165,22 +199,55 @@ export function refreshDiagnostics(doc: TextDocument, diagnosticCollection: Diag diagnosticCollection.set(doc.uri, diagnostics); } -export 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/extension.ts b/src/extension.ts index 1324d18..ac3682e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,7 @@ import { ExtensionContext, languages, commands, window, TextDocument, Position, import { colorProvider } from "./color"; import { getStatusBarText, NavigationData } from "./navigation-data"; import { cleanUpPath, getAudioFolder, getImagesFolder, getNavigationJsonFilepath, getWorkspaceFolder, stripWorkspaceFromFile } from "./workspace"; -import { refreshDiagnostics, subscribeToDocumentChanges } from "./diagnostics"; +import { diagnosticsInit } from "./diagnostics"; import { semanticTokensProvider } from "./semantics"; import { hoverProvider } from "./hover"; import { completionProvider } from "./completion"; @@ -49,7 +49,6 @@ export async function activate(context: ExtensionContext): Promise { // diagnostics (errors and warnings) const diagnostics = languages.createDiagnosticCollection("renpy"); context.subscriptions.push(diagnostics); - subscribeToDocumentChanges(context, diagnostics); // A TextDocument was saved context.subscriptions.push( @@ -85,6 +84,9 @@ export async function activate(context: ExtensionContext): Promise { }), ); + // diagnostics (errors and warnings) + diagnosticsInit(context); + // custom command - refresh data const refreshCommand = commands.registerCommand("renpy.refreshNavigationData", async () => { updateStatusBar("$(sync~spin) Refreshing Ren'Py navigation data..."); @@ -132,14 +134,6 @@ export async function activate(context: ExtensionContext): Promise { }); context.subscriptions.push(migrateOldFilesCommand); - // custom command - refresh diagnostics - const refreshDiagnosticsCommand = commands.registerCommand("renpy.refreshDiagnostics", () => { - if (window.activeTextEditor) { - refreshDiagnostics(window.activeTextEditor.document, diagnostics); - } - }); - context.subscriptions.push(refreshDiagnosticsCommand); - // custom command - toggle token debug view let isShowingTokenDebugView = false; const toggleTokenDebugViewCommand = commands.registerCommand("renpy.toggleTokenDebugView", async () => { 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; +}